MySQL 数据库
概述
「云函数」提供了多种方式连接和操作 MySQL 数据库,满足不同场景的应用需求。本文档详细介绍以下几种连接方式:
| 连接方式 | 使用场景 | 优势 |
|---|---|---|
| 公网连接 | 连接任意可公网访问的 MySQL 实例 | 灵活性高,适用范围广 |
| 内网互联 | 连接腾讯云上海区域的 MySQL 实例 | 内网直连,安全性高,性能好 |
📄️ 公网连接
学习如何通过公网连接任意 MySQL 数据库实例
📄️ 内网互联
使用内网互联连接腾讯云 MySQL 实例,获得更好的性能和安全性
📄️ 最佳实践
数据库连接的性能优化、安全配置和错误处理建议
📄️ 常见问题
数据库连接和使用过程中的常见问题及解决方案
通过公网连接数据库
准备工作
- 确保您的 MySQL 数据库已开启公网访问,并设置了合适的访问控制规则
- 准备好数据库连接信息(主机地址、端口、用户名、密码、数据库名)
安全提醒
公网连接存在一定的安全风险,请确保:
- 使用强密码
- 配置防火墙规则,限制访问来源
- 定期更新数据库版本和安全补丁
实现步骤
1. 创建数据库和表结构
首先,在您的 MySQL 数据库中创建一个名为 dev 的数据库,并创建用户表:
CREATE DATABASE IF NOT EXISTS dev;
USE dev;
CREATE TABLE persons (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
age INT NOT NULL,
email VARCHAR(255) UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 可选:插入一些测试数据
INSERT INTO persons (name, age, email) VALUES
('张三', 25, 'zhangsan@example.com'),
('李四', 30, 'lisi@example.com'),
('王五', 35, 'wangwu@example.com');
表结构说明
id: 自增主键,唯一标识每条记录name: 人名,VARCHAR 类型,最长 100 个字符,不允许为空age: 年龄,INT 类型,不允许为空email: 邮箱地址,VARCHAR 类型,具有唯一约束created_at: 创建时间,自动设置为当前时间updated_at: 更新时间,记录更新时自动更新
2. 安装数据库驱动
- npm
- yarn
- pnpm
npm install mysql2 --save
yarn add mysql2
pnpm add mysql2
提示
推荐使用 mysql2 而非 mysql,因为前者支持 Promise API 和更好的性能
3. 编写数据库连接代码
- Express 应用
- Koa 应用
- 云函数
打开 routes/index.js 文件,修改以下内容:
const express = require('express');
const mysql = require('mysql2/promise');
const router = express.Router();
// 创建 MySQL 连接池
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
port: process.env.DB_PORT ? Number(process.env.DB_PORT) : 3306,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
acquireTimeout: 60000,
timeout: 60000,
reconnect: true,
charset: 'utf8mb4'
});
// 健康检查接口
router.get('/health', async (req, res) => {
try {
const connection = await pool.getConnection();
await connection.ping();
connection.release();
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
} catch (error) {
console.error('数据库健康检查失败:', error);
res.status(500).json({ status: 'unhealthy', error: error.message });
}
});
// 获取用户列表
router.get('/', async (req, res) => {
try {
const { page = 1, limit = 10 } = req.query;
const offset = (page - 1) * limit;
const connection = await pool.getConnection();
try {
// 查询总数
const [countResult] = await connection.query('SELECT COUNT(*) as total FROM persons');
const total = countResult[0].total;
// 分页查询
const [rows] = await connection.query(
'SELECT * FROM persons ORDER BY created_at DESC LIMIT ? OFFSET ?',
[parseInt(limit), parseInt(offset)]
);
res.json({
success: true,
data: rows,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
} finally {
connection.release();
}
} catch (err) {
console.error('数据库查询错误:', err);
res.status(500).json({ success: false, error: '数据库查询失败' });
}
});
// 创建用户
router.post('/users', async (req, res) => {
const { name, age, email } = req.body;
// 参数验证
if (!name || !age || !email) {
return res.status(400).json({
success: false,
error: '缺少必要参数:name, age, email'
});
}
try {
const connection = await pool.getConnection();
try {
const [result] = await connection.query(
'INSERT INTO persons (name, age, email) VALUES (?, ?, ?)',
[name, age, email]
);
res.status(201).json({
success: true,
data: {
id: result.insertId,
name,
age,
email
}
});
} finally {
connection.release();
}
} catch (err) {
console.error('创建用户失败:', err);
// 处理重复邮箱错误
if (err.code === 'ER_DUP_ENTRY') {
return res.status(409).json({
success: false,
error: '邮箱地址已存在'
});
}
res.status(500).json({ success: false, error: '创建用户失败' });
}
});
// 更新用户
router.put('/users/:id', async (req, res) => {
const { id } = req.params;
const { name, age, email } = req.body;
try {
const connection = await pool.getConnection();
try {
const [result] = await connection.query(
'UPDATE persons SET name = ?, age = ?, email = ? WHERE id = ?',
[name, age, email, id]
);
if (result.affectedRows === 0) {
return res.status(404).json({
success: false,
error: '用户不存在'
});
}
res.json({ success: true, message: '用户更新成功' });
} finally {
connection.release();
}
} catch (err) {
console.error('更新用户失败:', err);
res.status(500).json({ success: false, error: '更新用户失败' });
}
});
// 删除用户
router.delete('/users/:id', async (req, res) => {
const { id } = req.params;
try {
const connection = await pool.getConnection();
try {
const [result] = await connection.query('DELETE FROM persons WHERE id = ?', [id]);
if (result.affectedRows === 0) {
return res.status(404).json({
success: false,
error: '用户不存在'
});
}
res.json({ success: true, message: '用户删除成功' });
} finally {
connection.release();
}
} catch (err) {
console.error('删除用户失败:', err);
res.status(500).json({ success: false, error: '删除用户失败' });
}
});
module.exports = router;
const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');
const mysql = require('mysql2/promise');
const app = new Koa();
const router = new Router();
// 创建 MySQL 连接池
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
port: process.env.DB_PORT ? Number(process.env.DB_PORT) : 3306,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// 获取用户列表
router.get('/users', async (ctx) => {
try {
const connection = await pool.getConnection();
try {
const [rows] = await connection.query('SELECT * FROM persons LIMIT 10');
ctx.body = { success: true, data: rows };
} finally {
connection.release();
}
} catch (err) {
console.error('数据库查询错误:', err);
ctx.status = 500;
ctx.body = { success: false, error: '数据库查询失败' };
}
});
app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());
module.exports = app;
const mysql = require('mysql2/promise');
// 全局连接池,避免每次调用都创建新连接
let pool;
function getPool() {
if (!pool) {
pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
port: process.env.DB_PORT ? Number(process.env.DB_PORT) : 3306,
waitForConnections: true,
connectionLimit: 5, // 云函数环境建议较小的连接数
queueLimit: 0,
acquireTimeout: 60000,
timeout: 60000
});
}
return pool;
}
exports.main = async (event, context) => {
const { action, data } = event;
try {
const pool = getPool();
const connection = await pool.getConnection();
try {
let result;
switch (action) {
case 'list':
const [rows] = await connection.query('SELECT * FROM persons LIMIT 10');
result = { success: true, data: rows };
break;
case 'create':
const { name, age, email } = data;
const [insertResult] = await connection.query(
'INSERT INTO persons (name, age, email) VALUES (?, ?, ?)',
[name, age, email]
);
result = {
success: true,
data: { id: insertResult.insertId, name, age, email }
};
break;
default:
result = { success: false, error: '不支持的操作' };
}
return result;
} finally {
connection.release();
}
} catch (error) {
console.error('数据库操作失败:', error);
return { success: false, error: error.message };
}
};
4. 配置环境变量并部署
在云托管服务中配置以下环境变量:
| 环境变量 | 说明 | 示例值 |
|---|---|---|
DB_HOST | 数据库主机地址 | mysql-example.mysql.database.tencentcloud.com |
DB_USER | 数据库用户名 | root |
DB_PASSWORD | 数据库密码 | your_password |
DB_NAME | 数据库名称 | dev |
DB_PORT | 数据库端口 | 3306 |
安全提示
敏感信息如数据库密码应使用云托管的环境变量功能存储,避免硬编码在代码中
5. 测试连接
部署完成后,您可以通过以下方式测试数据库连接:
- cURL 测试
- JavaScript 测试
# 测试健康检查
curl https://your-app-domain.com/health
# 获取用户列表
curl https://your-app-domain.com/
# 创建新用户
curl -X POST https://your-app-domain.com/users \
-H "Content-Type: application/json" \
-d '{
"name": "测试用户",
"age": 28,
"email": "test@example.com"
}'
// 测试获取用户列表
async function testGetUsers() {
try {
const response = await fetch('https://your-app-domain.com/');
const result = await response.json();
console.log('用户列表:', result);
} catch (error) {
console.error('请求失败:', error);
}
}
// 测试创建用户
async function testCreateUser() {
try {
const response = await fetch('https://your-app-domain.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: '新用户',
age: 25,
email: 'newuser@example.com'
})
});
const result = await response.json();
console.log('创建结果:', result);
} catch (error) {
console.error('创建失败:', error);
}
}
通过内网互联连接腾讯数据库
适用场景
该方式适用于连接在腾讯云上海区域购买的 MySQL 数据库实例,通过内网互联可以实现更高效、更安全的数据库访问。
配置步骤
1. 购买腾讯云 MySQL 实例
- 登录 腾讯云控制台
- 选择数据库 MySQL 服务
- 购买时选择上海地域
- 完成购买并初始化数据库
2. 配置内网互联
- 在云托管控制台中,选择网络配置 > 内网互联
- 点击配置 VPC 连接
- 选择目标 VPC 网络(您的 MySQL 实例所在的 VPC)
- 保存配置
3. 使用内网地址连接
- 获取 MySQL 实例的内网地址(可在腾讯云 MySQL 控制台查看)
- 在云托管服务的环境变量中配置数据库连接信息,使用内网地址
- 使用与公网连接相同的代码,但连接地址改为内网地址
优势
- 安全性:数据库流量不经过公网,降低安全风险
- 性能:内网连接延迟低,吞吐量高
- 成本:避免公网流量费用
- 稳定性:内网环境更加稳定可靠
注意
内网互联功能仅支持连接同一账号、同一地域的资源。如需跨地域连接,请考虑使用公网连接方式。
最佳实践
连接池配置优化
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
port: process.env.DB_PORT ? Number(process.env.DB_PORT) : 3306,
// 连接池配置
connectionLimit: 10, // 最大连接数
queueLimit: 0, // 队列限制,0 表示无限制
acquireTimeout: 60000, // 获取连接超时时间
timeout: 60000, // 查询超时时间
// 重连配置
reconnect: true, // 自动重连
// 字符集配置
charset: 'utf8mb4', // 支持 emoji 和特殊字符
// SSL 配置(生产环境推荐)
ssl: process.env.NODE_ENV === 'production' ? {
rejectUnauthorized: false
} : false,
// 时区配置
timezone: '+08:00'
});
错误处理和重试机制
// 带重试的数据库操作
async function executeWithRetry(operation, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
lastError = error;
// 判断是否为可重试的错误
if (isRetryableError(error) && i < maxRetries - 1) {
const delay = Math.pow(2, i) * 1000; // 指数退避
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
throw lastError;
}
function isRetryableError(error) {
const retryableCodes = [
'ECONNRESET',
'ETIMEDOUT',
'ENOTFOUND',
'ER_LOCK_WAIT_TIMEOUT',
'PROTOCOL_CONNECTION_LOST'
];
return retryableCodes.includes(error.code);
}
// 使用示例
router.get('/users', async (req, res) => {
try {
const result = await executeWithRetry(async () => {
const connection = await pool.getConnection();
try {
const [rows] = await connection.query('SELECT * FROM persons');
return rows;
} finally {
connection.release();
}
});
res.json({ success: true, data: result });
} catch (error) {
console.error('查询失败:', error);
res.status(500).json({ success: false, error: '查询失败' });
}
});
事务处理
// 事务处理示例
async function transferMoney(fromUserId, toUserId, amount) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
// 检查发送方余额
const [fromUser] = await connection.query(
'SELECT balance FROM users WHERE id = ? FOR UPDATE',
[fromUserId]
);
if (fromUser[0].balance < amount) {
throw new Error('余额不足');
}
// 扣除发送方余额
await connection.query(
'UPDATE users SET balance = balance - ? WHERE id = ?',
[amount, fromUserId]
);
// 增加接收方余额
await connection.query(
'UPDATE users SET balance = balance + ? WHERE id = ?',
[amount, toUserId]
);
// 记录转账日志
await connection.query(
'INSERT INTO transfer_logs (from_user_id, to_user_id, amount, created_at) VALUES (?, ?, ?, NOW())',
[fromUserId, toUserId, amount]
);
await connection.commit();
return { success: true, message: '转账成功' };
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
}
性能监控
// 查询性能监控
function withPerformanceMonitoring(queryFunction) {
return async function(...args) {
const startTime = Date.now();
const queryId = Math.random().toString(36).substr(2, 9);
console.log(`[${queryId}] 查询开始:`, args[0]);
try {
const result = await queryFunction.apply(this, args);
const duration = Date.now() - startTime;
console.log(`[${queryId}] 查询完成: ${duration}ms`);
// 记录慢查询
if (duration > 1000) {
console.warn(`[${queryId}] 慢查询检测: ${duration}ms`, {
sql: args[0],
params: args[1]
});
}
return result;
} catch (error) {
const duration = Date.now() - startTime;
console.error(`[${queryId}] 查询失败: ${duration}ms`, {
error: error.message,
sql: args[0]
});
throw error;
}
};
}
// 包装连接的查询方法
const originalQuery = pool.query.bind(pool);
pool.query = withPerformanceMonitoring(originalQuery);
常见问题
连接问题
连接超时怎么办?
可能原因:
- 网络延迟过高
- 数据库服务器负载过高
- 防火墙阻止连接
解决方案:
// 增加超时时间
const pool = mysql.createPool({
// ... 其他配置
acquireTimeout: 120000, // 增加到 2 分钟
timeout: 120000, // 查询超时 2 分钟
connectTimeout: 60000 // 连接超时 1 分钟
});
连接数过多怎么处理?
解决方案:
- 优化连接池配置
- 及时释放连接
- 使用连接监控
// 监控连接池状态
setInterval(() => {
console.log('连接池状态:', {
总连接数: pool._allConnections.length,
空闲连接数: pool._freeConnections.length,
使用中连接数: pool._acquiringConnections.length
});
}, 30000);
性能问题
查询速度慢怎么优化?
优化策略:
- 添加适当的索引
- 优化 SQL 查询
- 使用连接池
- 实施查询缓存
-- 添加索引示例
CREATE INDEX idx_persons_email ON persons(email);
CREATE INDEX idx_persons_age ON persons(age);
CREATE INDEX idx_persons_created_at ON persons(created_at);
安全问题
如何防止 SQL 注入?
防护措施:
- 始终使用参数化查询
- 验证输入数据
- 使用最小权限原则
// 正确的参数化查询
const [rows] = await connection.query(
'SELECT * FROM persons WHERE name = ? AND age > ?',
[userName, minAge]
);
// 错误的字符串拼接(容易受到 SQL 注入攻击)
// const query = `SELECT * FROM persons WHERE name = '${userName}'`;
性能建议
- 在生产环境中启用连接池以提高性能
- 对于频繁查询的字段添加索引
- 使用事务确保数据一致性
- 定期监控数据库性能和慢查询日志
安全提醒
- 避免在代码中硬编码数据库密码
- 使用参数化查询防止 SQL 注入
- 定期更新数据库版本和安全补丁
- 配置适当的防火墙规则