必须为每个线程分配独立数据库连接并原子执行完整事务,共享连接会导致崩溃或数据错乱;SQLite报SQLITE_BUSY,MySQL可能静默失败;WAL模式和重试机制可缓解但不替代线程隔离。

多线程环境下直接共享同一个数据库连接并执行事务,几乎必然导致崩溃或数据错乱——SQLite 会报 SQLITE_BUSY,MySQL 可能静默失败或返回乱序结果。必须为每个线程分配独立连接,并在事务边界内完成全部操作。
每个线程必须用独立数据库连接
SQLite 和 MySQL 的 C/C++ 驱动(如 sqlite3_open、mysql_init)本身不保证跨线程安全的连接复用。即使驱动标称“线程安全”,也仅指多个线程可同时调用不同连接句柄,而非共用一个句柄。
- 错误做法:
sqlite3* db全局单例,多个线程调用sqlite3_exec(db, "BEGIN", ...) - 正确做法:在线程入口处调用
sqlite3_open()或从连接池取专属连接,事务结束后立即sqlite3_close()或归还 - 例外:若使用 WAL 模式 +
sqlite3_prepare_v2+ 绑定参数,且严格避免多线程共用同一sqlite3_stmt*,可放宽但不推荐
事务必须在同一线程内原子完成
事务的 BEGIN、DML 操作、COMMIT/ROLLBACK 必须由同一个线程在同一个连接上连续执行。跨线程移交事务控制权没有标准 API 支持,强行传递连接句柄极易引发未定义行为。
- 常见陷阱:主线程
BEGIN,子线程执行INSERT,再由主线程COMMIT—— SQLite 会拒绝,MySQL 可能提交空事务 - 正确结构:把整个事务逻辑封装成函数,作为线程入口点传入,例如
std::thread{[conn_ptr]{ transaction_worker(conn_ptr); }} - 注意:WAL 模式下
PRAGMA journal_mode=WAL必须在连接打开后、任何 SQL 执行前设置,否则无效
并发写冲突时的重试策略不能省略
即使每个线程有独立连接,SQLite 在写密集场景仍会因锁竞争返回 SQLITE_BUSY;MySQL 在 RR 隔离级别下可能触发死锁检测并回滚某一方。硬编码 sleep(1) 不可靠,需带退避的循环重试。
立即学习“C++免费学习笔记(深入)”;
- 示例逻辑:执行
sqlite3_exec(... "BEGIN IMMEDIATE"),若返回SQLITE_BUSY,等待10ms → 20ms → 40ms后重试,最多 5 次 - 避免在持有锁期间做耗时操作(如网络请求、文件读写),否则放大阻塞时间
- MySQL 场景下,捕获
ER_LOCK_DEADLOCK错误码后必须重新执行整个事务,不能只重试单条语句
std::mutex 无法替代数据库级锁
用 std::mutex 包裹数据库操作看似“线程安全”,实则治标不治本:它只防止多线程同时调用你的 C++ 函数,但无法阻止底层数据库引擎因并发写入产生的物理锁冲突。两者作用域完全不同。
- 典型误用:全局
std::mutex db_mutex,所有数据库调用前lock_guard—— 这会让多线程退化为单线程,吞吐量反不如单线程 - 合理组合:用
std::mutex保护连接池的取/还操作,用数据库自身事务和 WAL 模式处理并发读写,二者分工明确 - 特别注意:SQLite 的
shared_cache模式(sqlite3_enable_shared_cache(1))在多线程下极难控制,建议禁用,默认每个连接私有缓存
最易被忽略的一点:事务的 ACID 保障只存在于单个连接内部。一旦涉及跨表关联更新、触发器或外键约束,而这些对象又被其他线程并发修改,即使你用了 WAL 和重试,也无法绕过数据库引擎自身的隔离级别限制——这时需要设计层面规避,比如拆分事务粒度或引入应用层乐观锁版本号。


















