课程导航:学习路径 | Boost.System | Boost.Asio | Boost.Beast | Boost.JSON | Boost.MySQL
前置知识
- 课程 1: Boost.System(
error_code、system_error) - 课程 2: Boost.Asio(
io_context、协程、co_await+use_awaitable) - SQL 基础(SELECT/INSERT/UPDATE/DELETE、事务)
- MySQL 数据库基本操作
学习目标
完成本课程后,你将能够:
- 理解 Boost.MySQL 的类型擦除连接模型(
any_connection) - 使用 C++20 协程执行异步数据库操作
- 掌握参数化查询和 PreparedStatement 防 SQL 注入
- 理解结果集类型体系(
results/static_results) - 实现事务控制(BEGIN/COMMIT/ROLLBACK)
- 读懂 Hical 的连接池、Statement 缓存和数据库中间件设计
目录
1. 核心概念
1.1 Boost.MySQL 的定位
Boost.MySQL 是一个纯异步的 MySQL 客户端库,直接实现 MySQL 客户端/服务器协议(不依赖 libmysqlclient),天然集成 Boost.Asio 的异步模型。
| |
与 libmysqlclient 的关键区别:
| 特性 | Boost.MySQL | libmysqlclient (C API) |
|---|---|---|
| 异步模型 | 原生 Asio 异步(协程/回调) | 同步阻塞(或自行封装线程池) |
| 依赖 | 仅 Boost + OpenSSL | MySQL 官方 C 库 |
| 分配器 | 支持 PMR / 自定义 | 内部分配 |
| 编译 | Header-only(大部分) | 需链接 .so/.dll |
| 类型安全 | static_results 编译期校验 | 运行时手动转换 |
| 协程集成 | co_await 一等公民 | 无 |
| SSL | 共享 Asio 的 ssl::context | 独立 SSL 配置 |
1.2 连接类型体系
Boost.MySQL 提供两类连接:
| |
Hical 选择:使用 any_connection,因为框架需要在运行时根据配置决定是否启用 SSL,且 any_connection 是连接池的前提。
1.3 查询执行模型
Boost.MySQL 支持三种查询方式:
| |
Hical 选择:参数化查询强制使用 PreparedStatement,配合 StmtCache 消除重复 prepare 开销。
1.4 结果集类型体系
| |
2. 基础用法
2.1 建立连接
| |
SSL 模式说明:
| 模式 | 说明 |
|---|---|
ssl_mode::enable | 优先 SSL,服务器不支持时降级为明文 |
ssl_mode::require | 强制 SSL,不支持则报错 |
ssl_mode::disable | 禁用 SSL,始终明文 |
2.2 执行文本查询
| |
⚠ 安全警告:文本查询不要拼接用户输入!
"WHERE name = '" + userName + "'"是经典的 SQL 注入漏洞。
2.3 参数化查询(客户端格式化)
Boost 1.85+ 引入了 with_params,在客户端安全格式化 SQL:
| |
特点:一次网络往返、自动转义、语法类似 std::format。
2.4 PreparedStatement
PreparedStatement 是最安全的查询方式——参数在服务器端绑定,从协议层面杜绝注入:
| |
多参数绑定:
| |
2.5 结果集遍历
| |
field_view 类型访问速查表:
| MySQL 类型 | field_view 方法 | C++ 类型 |
|---|---|---|
| TINYINT, SMALLINT, INT … | as_int64() | int64_t |
| INT UNSIGNED … | as_uint64() | uint64_t |
| FLOAT, DOUBLE | as_double() | double |
| VARCHAR, TEXT | as_string() | string_view |
| BLOB, BINARY | as_blob() | blob_view |
| DATE | as_date() | mysql::date |
| DATETIME, TIMESTAMP | as_datetime() | mysql::datetime |
| TIME | as_time() | mysql::time |
| NULL | is_null() | bool |
2.6 事务控制
| |
游戏服务器要点:涉及经济系统(金币、道具)的操作必须在事务中执行,且需要防重入保护——避免同一玩家并发触发导致数据不一致。
3. 进阶主题
3.1 类型擦除连接 any_connection
any_connection 是 Boost 1.84 引入的推荐连接类型,它将传输层(TCP/SSL/Unix)的选择推迟到运行时:
| |
与模板化连接的对比:
| 特性 | connection<Stream> | any_connection |
|---|---|---|
| 传输层选择 | 编译期 | 运行时 |
| SSL context | 用户传入并管理 | 内部自动创建 |
| 连接池支持 | ❌ | ✅(connection_pool) |
| 自动重连 | ❌ | ✅ |
| 运行时开销 | 零(静态分发) | 极小(虚函数) |
| 推荐场景 | 嵌入式、极致性能 | 应用服务器、Web 框架 |
3.2 静态类型结果集 static_results
static_results 让查询结果在编译期与 C++ 结构体绑定:
| |
优势:列数量/类型不匹配时编译报错,不必运行时手动 as_int64() / as_string()。
3.3 多结果集(存储过程)
MySQL 存储过程可以返回多个结果集:
| |
3.4 连接池 connection_pool
Boost 1.85+ 内置了 connection_pool,管理 any_connection 的生命周期:
| |
为什么 Hical 没用内置 connection_pool? 因为 Hical 的
DbConnectionPool需要:(1)DbConnection抽象接口(支持未来切换 PostgreSQL 等后端);(2) 与框架中间件深度集成(请求属性注入);(3) 自定义 LIFO 复用策略和 PreparedStatement 缓存联动。内置 pool 更适合简单场景。
3.5 错误处理与诊断
| |
错误分类:
| 异常类型 | 来源 | 场景 |
|---|---|---|
mysql::error_with_diagnostics | MySQL 服务器 | SQL 语法错误、表不存在、权限不足 |
boost::system::system_error | 网络层 / Asio | 连接断开、超时、DNS 解析失败 |
4. Hical 实战解读
4.1 MysqlConnection:any_connection 的框架封装
Hical 的 MysqlConnection(src/db/MysqlConnection.h)封装了 boost::mysql::any_connection,实现 DbConnection 抽象接口:
| |
关键设计 1:工厂模式解耦
| |
连接池通过工厂函数创建连接,完全不依赖 MysqlConnection 类型——未来替换为 PostgreSQL 只需提供新工厂。
关键设计 2:参数化查询流程
| |
关键设计 3:字符集安全验证
| |
4.2 StmtCache:LRU PreparedStatement 缓存
StmtCache(src/db/StmtCache.h)是每连接的 LRU 缓存,避免同一 SQL 重复 prepare:
| |
为什么需要 Statement 缓存?
每次 async_prepare_statement 都是一次到 MySQL 服务器的网络往返。对于 Web 框架来说,同一 SQL 模板(如 SELECT * FROM users WHERE id = ?)会被大量请求反复使用。缓存后,只有首次执行需要 prepare,后续直接复用。
| |
4.3 DbConnectionPool:协程式连接池
Hical 的连接池(src/db/DbConnectionPool.h)是为协程设计的——不使用 condition_variable(会阻塞事件循环线程),而是用 steady_timer 作为协程信号量:
| |
为什么用 steady_timer 当信号量?
| |
LIFO vs FIFO 策略:
连接池使用 LIFO(后进先出)而非 FIFO 复用空闲连接:
- 最近归还的连接更可能还在 TCP keepalive 内、MySQL 线程缓存热
- 不活跃的连接自然沉底,被
idleCheckLoop回收 - 减少总活跃连接数——更少的服务器资源消耗
4.4 DbMiddleware:请求级连接生命周期
DbMiddleware(src/db/DbMiddleware.h)遵循 Hical 的洋葱模型,管理每个 HTTP 请求的数据库连接:
| |
使用方式:
| |
4.5 DbQueryLog:查询日志装饰器
DbQueryLog(src/db/DbQueryLog.h)使用装饰器模式,透明地包装真实连接:
| |
业务代码完全无感——getDbConnection(req) 返回的是装饰后的连接,所有 query/execute 调用都被自动记录。
4.6 完整请求处理流程
以 GET /api/user/42 为例,数据在各层间的流转:
| |
5. 练习题
练习 1:协程式 CRUD
编写一个完整的协程式 CRUD 程序,对 users 表执行增删改查:
| |
| |
预期输出:
| |
练习 2:参数化查询实战
对比文本查询和 PreparedStatement 的安全性:
| |
| |
关键解释:
文本拼接:
"SELECT ... WHERE name = '' ; DROP TABLE users; --'"—— 分号让 MySQL 执行第二条 SQL,--注释掉尾部引号。Boost.MySQL 的async_execute默认不支持多语句,因此这个特定攻击可能失败,但其他注入形式(如' OR 1=1 --)仍然有效。PreparedStatement:参数值在 MySQL 协议层面作为数据传输,而非 SQL 语句的一部分。无论用户输入什么内容(包含引号、分号、注释符),都只会被当成
name列的查找值,永远不会被解释为 SQL 语法。with_paramsvs PreparedStatement:with_params在客户端做转义后拼接成完整 SQL 发送,安全性依赖转义逻辑的正确性。PreparedStatement 在协议层面分离 SQL 结构和数据,是更根本的安全保障。对于处理用户输入的场景,PreparedStatement 始终是首选。
练习 3:事务与错误处理
实现一个转账功能,要求正确处理各种异常场景:
| |
| |
预期输出:
| |
防并发超额扣款的关键:SELECT ... FOR UPDATE 会对选中的行加排他锁。其他事务在试图锁定同一行时会被阻塞,直到当前事务提交或回滚。按 ID 顺序加锁可以避免 A→B 和 B→A 并发时的死锁。
练习 4:LRU 缓存设计
参考 Hical 的 StmtCache,实现一个通用 LRU 缓存:
| |
| |
预期输出:
| |
核心数据结构选择:std::list(双向链表)+ std::unordered_map(哈希表)组合。链表提供 O(1) 的头插和尾删,哈希表提供 O(1) 的查找。splice() 是 std::list 的成员函数,可以 O(1) 将节点移动到头部,不涉及分配和释放。
练习 5(挑战):连接池实现
设计一个简化版的协程式连接池:
| |
| |
思考题解答:
为什么不能用 condition_variable?
condition_variable::wait()会阻塞当前线程。在1 Thread : 1 io_context模型中,线程被阻塞意味着该 io_context 上的所有协程都无法推进——包括负责归还连接的那个协程。结果是死锁。steady_timer+co_await只挂起当前协程,线程继续运行其他协程。LIFO 比 FIFO 好在哪里? LIFO 让最近归还的连接优先被复用,TCP 状态更热(keepalive 内)、MySQL 端的线程缓存命中率更高。不活跃的连接沉底,被空闲回收循环自然清理。FIFO 会均匀使用所有连接,导致更多连接处于活跃状态,消耗更多 MySQL 服务器资源。
如何处理连接断开? 三层防护:(a)
acquire()时 ping 检活,死连接直接丢弃;(b) 后台healthCheckLoop定期 ping 空闲连接,剔除死连接并补充到 minSize;(c)MysqlConnection::query()执行失败后自动重试(PreparedStatement 失效场景)。
6. 总结与拓展阅读
核心 API 速查表
| API | 说明 | 返回类型 |
|---|---|---|
any_connection(executor) | 创建类型擦除连接 | — |
async_connect(params) | 异步连接服务器 | awaitable<void> |
async_execute(query, res) | 执行查询 | awaitable<void> |
async_prepare_statement(sql) | 预编译 SQL | awaitable<statement> |
stmt.bind(args...) | 绑定参数 | bound statement |
async_close_statement(stmt) | 关闭 PreparedStatement | awaitable<void> |
async_close() | 关闭连接 | awaitable<void> |
async_ping() | 检测连接存活 | awaitable<void> |
results.rows() | 获取行集合 | rows_view |
results.meta() | 获取列元信息 | metadata_collection_view |
results.affected_rows() | 影响行数 | uint64_t |
results.last_insert_id() | 最后插入 ID | uint64_t |
field_view.as_int64() | 取整数值 | int64_t |
field_view.as_string() | 取字符串值 | string_view |
field_view.is_null() | 是否为 NULL | bool |
with_params(fmt, args...) | 客户端安全格式化 | formattable query |
查询方式对比
| 方式 | 安全性 | 网络往返 | 可复用 | 适用场景 |
|---|---|---|---|---|
| 文本查询 | ❌ 低 | 1 次 | — | DDL、SET、静态 SQL |
| 客户端格式化 | ✅ 中 | 1 次 | — | 简单动态查询 |
| PreparedStatement | ✅✅ 高 | 2 次* | ✅ | 带用户输入的业务查询 |
* 配合 StmtCache 后首次 2 次,后续 1 次。
拓展阅读
- Boost.MySQL 官方文档
- Boost.MySQL GitHub
- MySQL 客户端/服务器协议
- C++ 协程与数据库:设计模式
- Boost.Describe 反射库(
static_results的基础)
课程回顾
本系列 5 门课程的知识依赖关系:
| |
回顾各课程核心要点:
- 课程 1 — Boost.System:
error_code+error_category体系,I/O 错误处理的基石 - 课程 2 — Boost.Asio:
io_context+ C++20 协程,异步编程的核心引擎 - 课程 3 — Boost.Beast:HTTP/WebSocket 协议层,在 Asio 之上构建应用协议
- 课程 4 — Boost.JSON:值类型操作、PMR 高性能分配、反射自动序列化
- 课程 5 — Boost.MySQL:协程式数据库访问、连接池、PreparedStatement 缓存
有兴趣可查看 Hical 框架源码地址:github.com/Hical61/Hical