跳到主要内容

MySQL 数据库

概述

「云函数」提供了多种方式连接和操作 MySQL 数据库,满足不同场景的应用需求。本文档详细介绍以下几种连接方式:

连接方式使用场景优势
公网连接连接任意可公网访问的 MySQL 实例灵活性高,适用范围广
内网互联连接腾讯云上海区域的 MySQL 实例内网直连,安全性高,性能好

通过公网连接数据库

准备工作

  1. 确保您的 MySQL 数据库已开启公网访问,并设置了合适的访问控制规则
  2. 准备好数据库连接信息(主机地址、端口、用户名、密码、数据库名)
安全提醒

公网连接存在一定的安全风险,请确保:

  • 使用强密码
  • 配置防火墙规则,限制访问来源
  • 定期更新数据库版本和安全补丁

实现步骤

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 install mysql2 --save
提示

推荐使用 mysql2 而非 mysql,因为前者支持 Promise API 和更好的性能

3. 编写数据库连接代码

打开 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;

4. 配置环境变量并部署

在云托管服务中配置以下环境变量:

环境变量说明示例值
DB_HOST数据库主机地址mysql-example.mysql.database.tencentcloud.com
DB_USER数据库用户名root
DB_PASSWORD数据库密码your_password
DB_NAME数据库名称dev
DB_PORT数据库端口3306
安全提示

敏感信息如数据库密码应使用云托管的环境变量功能存储,避免硬编码在代码中

5. 测试连接

部署完成后,您可以通过以下方式测试数据库连接:

# 测试健康检查
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"
}'

通过内网互联连接腾讯数据库

适用场景

该方式适用于连接在腾讯云上海区域购买的 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);

常见问题

连接问题

连接超时怎么办?

可能原因:

  1. 网络延迟过高
  2. 数据库服务器负载过高
  3. 防火墙阻止连接

解决方案:

// 增加超时时间
const pool = mysql.createPool({
// ... 其他配置
acquireTimeout: 120000, // 增加到 2 分钟
timeout: 120000, // 查询超时 2 分钟
connectTimeout: 60000 // 连接超时 1 分钟
});
连接数过多怎么处理?

解决方案:

  1. 优化连接池配置
  2. 及时释放连接
  3. 使用连接监控
// 监控连接池状态
setInterval(() => {
console.log('连接池状态:', {
总连接数: pool._allConnections.length,
空闲连接数: pool._freeConnections.length,
使用中连接数: pool._acquiringConnections.length
});
}, 30000);

性能问题

查询速度慢怎么优化?

优化策略:

  1. 添加适当的索引
  2. 优化 SQL 查询
  3. 使用连接池
  4. 实施查询缓存
-- 添加索引示例
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 注入?

防护措施:

  1. 始终使用参数化查询
  2. 验证输入数据
  3. 使用最小权限原则
// 正确的参数化查询
const [rows] = await connection.query(
'SELECT * FROM persons WHERE name = ? AND age > ?',
[userName, minAge]
);

// 错误的字符串拼接(容易受到 SQL 注入攻击)
// const query = `SELECT * FROM persons WHERE name = '${userName}'`;
性能建议
  • 在生产环境中启用连接池以提高性能
  • 对于频繁查询的字段添加索引
  • 使用事务确保数据一致性
  • 定期监控数据库性能和慢查询日志
安全提醒
  • 避免在代码中硬编码数据库密码
  • 使用参数化查询防止 SQL 注入
  • 定期更新数据库版本和安全补丁
  • 配置适当的防火墙规则