📌 问题概述
这段代码在高并发场景下会发生死锁,原因是不合理的 FOR UPDATE 查询 结合后续的 INSERT 操作,导致间隙锁、插入意向锁之间的冲突。
表结构
private_chat表

chat_thread表

聊天界面

📌死锁源码
bool MysqlDao::CreatePrivateChat(int user1_id, int user2_id, int& thread_id){auto con = pool_->getConnection();if (!con) {return false;}Defer defer([this, &con]() {pool_->returnConnection(std::move(con));});auto& conn = con->_con;try {// 开启事务conn->setAutoCommit(false);// 1. 查询是否已存在私聊并加行级锁int uid1 = std::min(user1_id, user2_id);int uid2 = std::max(user1_id, user2_id);std::string check_sql ="SELECT thread_id FROM private_chat ""WHERE (user1_id = ? AND user2_id = ?) ""FOR UPDATE;";std::unique_ptr<sql::PreparedStatement> pstmt(conn->prepareStatement(check_sql));pstmt->setInt64(1, uid1);pstmt->setInt64(2, uid2);std::unique_ptr<sql::ResultSet> res(pstmt->executeQuery());if (res->next()) {// 如果已存在,返回该 thread_idthread_id = res->getInt("thread_id");conn->commit(); // 提交事务return true;}// 2. 如果未找到,创建新的 chat_thread 和 private_chat 记录// 在 chat_thread 表插入新记录std::string insert_chat_thread_sql ="INSERT INTO chat_thread (type, created_at) VALUES ('private', NOW());";std::unique_ptr<sql::PreparedStatement> pstmt_insert_thread(conn->prepareStatement(insert_chat_thread_sql));pstmt_insert_thread->executeUpdate();// 获取新插入的 thread_idstd::string get_last_insert_id_sql = "SELECT LAST_INSERT_ID();";std::unique_ptr<sql::PreparedStatement> pstmt_last_insert_id(conn->prepareStatement(get_last_insert_id_sql));std::unique_ptr<sql::ResultSet> res_last_id(pstmt_last_insert_id->executeQuery());res_last_id->next();thread_id = res_last_id->getInt(1);// 3. 在 private_chat 表插入新记录std::string insert_private_chat_sql ="INSERT INTO private_chat (thread_id, user1_id, user2_id, created_at) ""VALUES (?, ?, ?, NOW());";std::unique_ptr<sql::PreparedStatement> pstmt_insert_private(conn->prepareStatement(insert_private_chat_sql));pstmt_insert_private->setInt64(1, thread_id);pstmt_insert_private->setInt64(2, uid1);pstmt_insert_private->setInt64(3, uid2);pstmt_insert_private->executeUpdate();// 提交事务conn->commit();return true;}catch (sql::SQLException& e) {std::cerr << "SQLException: " << e.what() << std::endl;conn->rollback();return false;}return false;}

InnoDB 的四种主要锁类型
按粒度分类
MySQL InnoDB存储引擎中这四种重要的锁类型:
记录锁(Record Lock)
记录锁是最基本的行锁类型,它锁定的是索引记录本身。当你使用唯一索引或主键进行等值查询并且找到记录时,InnoDB会使用记录锁。
例如:
SELECT * FROM users WHERE id = 10 FOR UPDATE,这会在id=10的索引记录上加记录锁。临键锁(Next-Key Lock)
临键锁是记录锁和间隙锁的组合,锁定了一个范围,包括记录本身。这是InnoDB默认的行锁算法,主要用于解决幻读问题。
临键锁的范围是左开右闭区间,比如索引值为10, 20, 30的记录,临键锁可能是:
- (-∞, 10]
- (10, 20]
- (20, 30]
- (30, +∞)
当进行范围查询时,InnoDB通常会使用临键锁来锁定扫描到的索引范围,防止其他事务在这个范围内插入新记录
插入意向锁(Insert Intention Lock)
插入意向锁是一种特殊的间隙锁,在INSERT操作之前设置。它的特点是:
- 不会阻塞其他插入意向锁:多个事务可以同时在同一个间隙中获取插入意向锁,只要它们插入的位置不同
- 会被间隙锁和临键锁阻塞:如果间隙已经被其他事务的间隙锁或临键锁锁定,插入操作会等待
- 提高并发性:允许多个事务并发地向同一间隙插入不同的记录
例如,如果索引中有记录10和20,两个事务可以同时在(10, 20)这个间隙中插入不同的值(如12和15),因为插入意向锁之间不冲突。
这三种锁机制共同协作,在保证数据一致性的同时,尽可能提高并发性能。
间隙锁 (Gap Lock) │
• 锁定索引记录之间的间隙 │
• 不包括记录本身 │
• 例如: 记录3和记录7之间的空隙
临键锁如何工作
让我用具体的例子来解释临键锁是如何”组合”记录锁和间隙锁的。
先理解间隙锁(Gap Lock)
间隙锁锁定的是两个索引记录之间的间隙,不包括记录本身。它的作用是防止其他事务在这个间隙中插入新记录。
临键锁的组合特性
假设有一张表,索引列有值:10, 20, 30
索引值: 10 20 30间隙: (∞,10) (10,20) (20,30) (30,∞)
临键锁 = 间隙锁 + 记录锁,具体来说:
临键锁 (10, 20] 意味着:
- 间隙锁部分:锁定 (10, 20) 这个开区间,防止插入11-19的记录
- 记录锁部分:锁定值为20的记录本身
实际场景举例
-- 假设表中有记录: id = 10, 20, 30SELECT * FROM users WHERE id >= 20 FOR UPDATE;
这个查询会加临键锁:
- (10, 20] - 锁住间隙(10,20)和记录20
- (20, 30] - 锁住间隙(20,30)和记录30
- (30, +∞) - 锁住30之后的所有间隙
为什么要这样组合?
这种组合设计巧妙地解决了幻读问题:
-- 事务ASELECT * FROM users WHERE id >= 20 FOR UPDATE;-- 结果:找到id=20, 30两条记录-- 如果只有记录锁,事务B可以:INSERT INTO users VALUES (25, 'New'); -- 成功插入!-- 事务A再次查询SELECT * FROM users WHERE id >= 20 FOR UPDATE;-- 结果:找到id=20, 25, 30三条记录 ← 出现幻读!
但有了临键锁的间隙锁部分,事务B的插入会被阻塞,因为25落在了(20,30]的间隙范围内。
总结理解
临键锁 = 锁住一个范围的两个部分:
- 记录锁部分:锁住右边界的那条记录(比如20)
- 间隙锁部分:锁住左边界到右边界之间的间隙(比如10到20之间)
这样既保护了记录本身不被修改,又保护了间隙不被插入新记录,从而在可重复读隔离级别下避免幻读。
按类型分类
共享锁(Shared Lock,S锁)- 读锁,允许多个事务同时持有
排他锁(Exclusive Lock,X锁)- 写锁,独占访问
它们的组合关系
记录锁可以是共享锁,也可以是排他锁:
-- 记录锁 + 共享锁SELECT * FROM users WHERE id = 10 FOR SHARE;-- 在id=10的记录上加"共享记录锁"-- 记录锁 + 排他锁SELECT * FROM users WHERE id = 10 FOR UPDATE;-- 在id=10的记录上加"排他记录锁"UPDATE users SET name = 'John' WHERE id = 10;-- 也是加"排他记录锁"
形象的比喻
把记录锁想象成一个房间,而排他锁/共享锁是门锁的类型:
- 共享锁(S锁):像”阅览室”,多个人可以同时进来看书(多个事务可以同时读)
- 排他锁(X锁):像”编辑室”,只能一个人进去修改文档(只有一个事务可以写,其他读写都被阻塞)
完整的描述方式
所以准确的说法应该是:
- “在这条记录上加了排他的记录锁”
- “在这条记录上加了共享的记录锁”
🔑 间隙锁的特性
特性1: 间隙锁之间相互兼容┌──────────────────────────────────┐│ 事务A: 持有 Gap(1,5) ││ 事务B: 可以获取 Gap(1,5) ✅ ││ 事务C: 可以获取 Gap(1,5) ✅ │└──────────────────────────────────┘特性2: 间隙锁阻止插入┌──────────────────────────────────┐│ 事务A: 持有 Gap(1,5) ││ 事务B: INSERT id=3 ❌ ││ (需要插入意向锁,被阻塞) │└──────────────────────────────────┘特性3: 仅在 RR 隔离级别下生效┌──────────────────────────────────┐│ REPEATABLE READ: 有间隙锁 ✅ ││ READ COMMITTED: 无间隙锁 ✅ │└──────────────────────────────────┘
🔑 插入意向锁的特性
特性1: 与间隙锁互斥┌──────────────────────────────────┐│ 事务A: 持有 Gap Lock(1,5) ││ 事务B: INSERT id=3 ││ ↓ ││ 需要 Insert Intention ││ ❌ 被A的间隙锁阻塞 │└──────────────────────────────────┘特性2: 插入意向锁之间可以兼容(插入不同行时)┌──────────────────────────────────┐│ 事务A: INSERT id=2 (等待中) ││ 事务B: INSERT id=4 (等待中) ││ ↓ ││ 如果没有间隙锁阻塞 ││ 两者可以并发执行 ✅ │└──────────────────────────────────┘
问题代码结构
bool CreatePrivateChat(int user1_id, int user2_id, int& thread_id){conn->setAutoCommit(false); // 开启事务// ⚠️ 第一步: SELECT ... FOR UPDATE// 如果未找到记录 → 获取间隙锁std::string check_sql ="SELECT thread_id FROM private_chat ""WHERE (user1_id = ? AND user2_id = ?) ""FOR UPDATE;";if (res->next()) {// 找到记录,返回thread_id = res->getInt("thread_id");conn->commit();return true;}// ⚠️ 第二步: INSERT 新记录// 需要获取插入意向锁 → 与间隙锁冲突std::string insert_sql ="INSERT INTO private_chat ""(thread_id, user1_id, user2_id, created_at) ""VALUES (?, ?, ?, NOW());";conn->commit();}
表结构假设
CREATE TABLE private_chat (thread_id INT,user1_id INT,user2_id INT,created_at DATETIME,INDEX idx_users (user1_id, user2_id) -- 复合索引);-- 当前数据┌──────────┬──────────┬──────────┐│ user1_id │ user2_id │thread_id │├──────────┼──────────┼──────────┤│ 1 │ 3 │ 10 ││ 2 │ 4 │ 20 ││ 5 │ 8 │ 30 │└──────────┴──────────┴──────────┘-- 索引树的间隙Gap: (-∞, (1,3))Gap: ((1,3), (2,4)) ← 死锁发生的间隙Gap: ((2,4), (5,8))Gap: ((5,8), +∞)
第三部分:死锁发生的完整流程
并发场景
两个线程同时调用: CreatePrivateChat(1, 2, thread_id)
┌─────────────────────────────────────────────────────┐│ 并发请求: ││ • 线程1: CreatePrivateChat(1, 2, thread_id_A) ││ • 线程2: CreatePrivateChat(1, 2, thread_id_B) ││ ││ 目标: 创建 user1_id=1, user2_id=2 的私聊 ││ 索引位置: 落在间隙 ((1,3), (2,4)) 之间 │└─────────────────────────────────────────────────────┘
详细时间线
时间 事务 A 事务 B────────────────────────────────────────────────────────────T0 BEGIN BEGIN↓ ↓T1 SELECT ... FOR UPDATEWHERE user1_id=1 AND user2_id=2 (等待中)↓检查索引: (1,2)↓未找到记录↓T2 获取间隙锁 Gap((1,3), (2,4)) ✅ SELECT ... FOR UPDATE(锁定查询区间) WHERE user1_id=1 AND user2_id=2↓检查索引: (1,2)↓未找到记录↓T3 准备 INSERT 获取间隙锁 Gap((1,3), (2,4)) ✅thread_id = 100 ⚠️ 间隙锁兼容! 成功获取!↓ ↓T4 INSERT INTO private_chat 准备 INSERTVALUES (100, 1, 2, NOW()) thread_id = 101↓ ↓需要插入意向锁Insert Intention Lock↓T5 检测到 B 持有 Gap Lock INSERT INTO private_chat❌ 插入意向锁与间隙锁冲突 VALUES (101, 1, 2, NOW())⏳ 等待 B 释放间隙锁... ↓需要插入意向锁Insert Intention Lock↓T6 (继续等待) 检测到 A 持有 Gap Lock持有: Gap Lock ❌ 插入意向锁与间隙锁冲突等待: B 释放 Gap Lock ⏳ 等待 A 释放间隙锁...↓持有: Gap Lock等待: A 释放 Gap LockT7 💀 DEADLOCK DETECTED!─────────────────────────────────────────────MySQL 检测到循环等待:• A 持有间隙锁, 等待 B 释放间隙锁• B 持有间隙锁, 等待 A 释放间隙锁─────────────────────────────────────────────↓回滚事务 B (选择代价较小的事务)↓事务 A 继续执行INSERT 成功 ✅COMMIT ✅
第四部分:死锁的核心机制
锁兼容性矩阵
┌───────────────────────────────────────────────────┐│ 锁类型兼容性(RR隔离级别) │├─────────────┬────────┬──────────┬───────────────┤│ 持有\请求 │ 间隙锁 │ 记录锁 │ 插入意向锁 │├─────────────┼────────┼──────────┼───────────────┤│ 间隙锁 │ ✅兼容 │ ✅兼容 │ ❌ 冲突 ││ 记录锁 │ ✅兼容 │ ❌ 冲突 │ ❌ 冲突 ││ 插入意向锁 │ ❌冲突 │ ❌ 冲突 │ ✅兼容(不同行)│└─────────────┴────────┴──────────┴───────────────┘⚠️ 核心规则:• 间隙锁 + 间隙锁 = ✅ 兼容(可以同时持有)• 间隙锁 + 插入意向锁 = ❌ 冲突(互相阻塞)
死锁的四个必要条件
┌──────────────────────────────────────────────┐│ 本案例中的死锁条件分析 │├──────────────────────────────────────────────┤│ ││ 1️⃣ 互斥条件 ✅ ││ 插入意向锁 与 间隙锁 互斥 ││ ││ 2️⃣ 持有并等待 ✅ ││ A 持有间隙锁, 等待获取插入意向锁 ││ B 持有间隙锁, 等待获取插入意向锁 ││ ││ 3️⃣ 不可剥夺 ✅ ││ 间隙锁只能在事务提交/回滚时释放 ││ ││ 4️⃣ 循环等待 ✅ ││ A 等待 B → B 等待 A ││ │└──────────────────────────────────────────────┘
可视化流程图
┌────────────────────────────────────────────────────┐│ 死锁形成的循环依赖图 │└────────────────────────────────────────────────────┘事务 A 事务 B│ ││ │[持有间隙锁] [持有间隙锁]Gap(1,3)~(2,4) Gap(1,3)~(2,4)│ ││ │▼ ▼需要插入意向锁 需要插入意向锁Insert Intention Insert Intention│ ││◄─────────┐ ┌───────────►││ │ │ ││ 等待B释放│ 等待A释放 ││ 间隙锁 │ 间隙锁 ││ │ │ │└──────────┘ └────────────┘💀 循环等待解释:• 两个事务都在 T2-T3 成功获取了间隙锁(兼容)• 两个事务都在 T5-T6 尝试获取插入意向锁• 插入意向锁被对方的间隙锁阻塞• 形成 A→B→A 的循环依赖
第五部分:为什么会发生死锁?
根本原因分析
┌─────────────────────────────────────────────────┐│ 死锁的三个关键因素 │└─────────────────────────────────────────────────┘因素1: FOR UPDATE 的锁定范围─────────────────────────────当 SELECT ... FOR UPDATE 未找到记录时:• 不仅扫描索引范围• 还会对扫描的间隙加间隙锁• 目的: 防止其他事务插入满足条件的记录(防止幻读)查询: WHERE user1_id=1 AND user2_id=2索引: idx_users(user1_id, user2_id)加锁: Gap Lock on ((1,3), (2,4))因素2: 间隙锁的兼容性─────────────────────────────两个事务可以同时持有同一间隙的间隙锁:• 事务A: Gap Lock ✅• 事务B: Gap Lock ✅ (不冲突)⚠️ 这是死锁的前提条件!因素3: 插入意向锁的互斥性─────────────────────────────INSERT 操作需要获取插入意向锁:• 检查目标间隙是否有间隙锁• 如果有 → 等待间隙锁释放事务A: INSERT → 需要插入意向锁 → 被B的间隙锁阻塞事务B: INSERT → 需要插入意向锁 → 被A的间隙锁阻塞💀 循环等待形成!
死锁可以回滚还需要修复吗
🚨 问题1: 用户体验差
用户请求流程:┌─────────────────────────────────────────┐│ 用户A: 创建与用户B的私聊 ││ ↓ ││ 发起请求... ││ ↓ ││ 💀 死锁! 事务被回滚 ││ ↓ ││ 返回 false ││ ↓ ││ ❌ 用户看到"创建失败" ││ ↓ ││ 😤 用户体验: 明明是正常操作,为什么失败?│└─────────────────────────────────────────┘
问题: 虽然没有数据错误,但用户需要重试,体验不好。
🚨 问题2: 高并发下的性能损耗
高并发场景 (100个并发请求):┌──────────────────────────────────────────┐│ 100 个并发请求创建私聊 ││ ↓ ││ • 50 个事务获取间隙锁 ✅ ││ • 50 个事务也获取间隙锁 ✅ ││ ↓ ││ • 所有事务尝试 INSERT ││ • 💀 大量死锁! ││ ↓ ││ • 50% 的事务被回滚 ││ • 这些事务需要重试 ││ ↓ ││ • 数据库压力翻倍 ││ • CPU 消耗在死锁检测上 ││ • 大量事务日志写入 │└──────────────────────────────────────────┘性能损失:• 死锁检测: 消耗 CPU• 回滚操作: 消耗 IO• 客户端重试: 消耗网络和数据库连接
第六部分:解决方案
✅先INSERT后SELECT(乐观策略)
原理: 先尝试插入,如果失败说明已存在,再查询
// 在 private_chat 表上添加唯一索引// CREATE UNIQUE INDEX uk_user_pair ON private_chat(user1_id, user2_id);bool CreatePrivateChat(int user1_id, int user2_id, int& thread_id){auto con = pool_->getConnection();Defer defer([this, &con]() { pool_->returnConnection(std::move(con)); });auto& conn = con->_con;int uid1 = std::min(user1_id, user2_id);int uid2 = std::max(user1_id, user2_id);try {conn->setAutoCommit(false);// 先创建 chat_threadstd::string insert_thread_sql ="INSERT INTO chat_thread (type, created_at) VALUES ('private', NOW());";std::unique_ptr<sql::PreparedStatement> pstmt_thread(conn->prepareStatement(insert_thread_sql));pstmt_thread->executeUpdate();// 获取 thread_idstd::unique_ptr<sql::PreparedStatement> pstmt_id(conn->prepareStatement("SELECT LAST_INSERT_ID();"));std::unique_ptr<sql::ResultSet> res_id(pstmt_id->executeQuery());res_id->next();thread_id = res_id->getInt(1);// ✅ 直接尝试插入std::string insert_sql ="INSERT INTO private_chat (thread_id, user1_id, user2_id, created_at) ""VALUES (?, ?, ?, NOW());";std::unique_ptr<sql::PreparedStatement> pstmt_insert(conn->prepareStatement(insert_sql));pstmt_insert->setInt64(1, thread_id);pstmt_insert->setInt64(2, uid1);pstmt_insert->setInt64(3, uid2);pstmt_insert->executeUpdate();conn->commit();return true;}catch (sql::SQLException& e) {conn->rollback();// 如果是唯一键冲突 (错误码 1062)if (e.getErrorCode() == 1062) {// ✅ 查询已存在的记录try {std::string query_sql ="SELECT thread_id FROM private_chat ""WHERE user1_id = ? AND user2_id = ?;";std::unique_ptr<sql::PreparedStatement> pstmt_query(conn->prepareStatement(query_sql));pstmt_query->setInt64(1, uid1);pstmt_query->setInt64(2, uid2);std::unique_ptr<sql::ResultSet> res(pstmt_query->executeQuery());if (res->next()) {thread_id = res->getInt("thread_id");return true;}}catch (sql::SQLException& e2) {std::cerr << "Query error: " << e2.what() << std::endl;}}std::cerr << "SQLException: " << e.what() << std::endl;return false;}}
优点:
- ✅ 避免死锁
- ✅ 大部分情况下性能好(只有一次数据库操作)
缺点:
- ⚠️ 需要唯一索引
- ⚠️ 依赖异常处理
记录锁死锁分析

bool MysqlDao::AddFriend(const int& from, const int& to, std::string back_name,std::vector<std::shared_ptr<AddFriendMsg>>& chat_datas) {auto con = pool_->getConnection();if (con == nullptr) {return false;}Defer defer([this, &con]() {pool_->returnConnection(std::move(con));});try {//开始事务con->_con->setAutoCommit(false);std::string reverse_back;std::string apply_desc;{// 1. 锁定并读取std::unique_ptr<sql::PreparedStatement> selStmt(con->_con->prepareStatement("SELECT back_name, descs ""FROM friend_apply ""WHERE from_uid = ? AND to_uid = ? ""FOR UPDATE"));selStmt->setInt(1, to);selStmt->setInt(2, from);std::unique_ptr<sql::ResultSet> rsSel(selStmt->executeQuery());if (rsSel->next()) {reverse_back = rsSel->getString("back_name");apply_desc = rsSel->getString("descs");}else {// 没有对应的申请记录,直接 rollback 并返回失败con->_con->rollback();return false;}}{// 2. 执行真正的更新std::unique_ptr<sql::PreparedStatement> updStmt(con->_con->prepareStatement("UPDATE friend_apply ""SET status = 1 ""WHERE from_uid = ? AND to_uid = ?"));updStmt->setInt(1, to);updStmt->setInt(2, from);if (updStmt->executeUpdate() != 1) {// 更新行数不对,回滚con->_con->rollback();return false;}}{// 3. 准备第一个SQL语句, 插入认证方好友数据std::unique_ptr<sql::PreparedStatement> pstmt(con->_con->prepareStatement("INSERT IGNORE INTO friend(self_id, friend_id, back) ""VALUES (?, ?, ?) "));//反过来的申请时from,验证时topstmt->setInt(1, from); // from idpstmt->setInt(2, to);pstmt->setString(3, back_name);// 执行更新int rowAffected = pstmt->executeUpdate();if (rowAffected < 0) {con->_con->rollback();return false;}//准备第二个SQL语句,插入申请方好友数据std::unique_ptr<sql::PreparedStatement> pstmt2(con->_con->prepareStatement("INSERT IGNORE INTO friend(self_id, friend_id, back) ""VALUES (?, ?, ?) "));//反过来的申请时from,验证时topstmt2->setInt(1, to); // from idpstmt2->setInt(2, from);pstmt2->setString(3, reverse_back);// 执行更新int rowAffected2 = pstmt2->executeUpdate();if (rowAffected2 < 0) {con->_con->rollback();return false;}}// 4. 创建 chat_threadlong long threadId = 0;{std::unique_ptr<sql::PreparedStatement> threadStmt(con->_con->prepareStatement("INSERT INTO chat_thread (type, created_at) VALUES ('private', NOW());"));threadStmt->executeUpdate();std::unique_ptr<sql::Statement> stmt(con->_con->createStatement());std::unique_ptr<sql::ResultSet> rs(stmt->executeQuery("SELECT LAST_INSERT_ID()"));if (rs->next()) {threadId = rs->getInt64(1);}else {return false;}}// 5. 插入 private_chat{std::unique_ptr<sql::PreparedStatement> pcStmt(con->_con->prepareStatement("INSERT INTO private_chat(thread_id, user1_id, user2_id) VALUES (?, ?, ?)"));pcStmt->setInt64(1, threadId);pcStmt->setInt(2, from);pcStmt->setInt(3, to);if (pcStmt->executeUpdate() < 0) return false;}// 6. 可选:插入初始消息到 chat_messageif (apply_desc.empty() == false){std::unique_ptr<sql::PreparedStatement> msgStmt(con->_con->prepareStatement("INSERT INTO chat_message(thread_id, sender_id, recv_id, content,created_at, updated_at, status) VALUES (?, ?, ?, ?,NOW(),NOW(),?)"));msgStmt->setInt64(1, threadId);msgStmt->setInt(2, to);msgStmt->setInt(3, from);msgStmt->setString(4, apply_desc);msgStmt->setInt(5, 2);if (msgStmt->executeUpdate() < 0) { return false; }std::unique_ptr<sql::Statement> stmt(con->_con->createStatement());std::unique_ptr<sql::ResultSet> rs(stmt->executeQuery("SELECT LAST_INSERT_ID()"));if (rs->next()) {auto messageId = rs->getInt64(1);auto tx_data = std::make_shared<AddFriendMsg>();tx_data->set_sender_id(to);tx_data->set_msg_id(messageId);tx_data->set_msgcontent(apply_desc);tx_data->set_thread_id(threadId);tx_data->set_unique_id("");tx_data->set_status(2);std::cout << "addfriend insert message success" << std::endl;chat_datas.push_back(tx_data);}else {return false;}}{std::unique_ptr<sql::PreparedStatement> msgStmt(con->_con->prepareStatement("INSERT INTO chat_message(thread_id, sender_id, recv_id, content, created_at, updated_at, status) VALUES (?, ?, ?, ?,NOW(),NOW(),?)"));msgStmt->setInt64(1, threadId);msgStmt->setInt(2, from);msgStmt->setInt(3, to);msgStmt->setString(4, "We are friends now!");msgStmt->setInt(5, 2);if (msgStmt->executeUpdate() < 0) { return false; }std::unique_ptr<sql::Statement> stmt(con->_con->createStatement());std::unique_ptr<sql::ResultSet> rs(stmt->executeQuery("SELECT LAST_INSERT_ID()"));if (rs->next()) {auto messageId = rs->getInt64(1);auto tx_data = std::make_shared<AddFriendMsg>();tx_data->set_sender_id(from);tx_data->set_msg_id(messageId);tx_data->set_msgcontent("We are friends now!");tx_data->set_thread_id(threadId);tx_data->set_unique_id("");tx_data->set_status(2);chat_datas.push_back(tx_data);}else {return false;}}// 提交事务con->_con->commit();std::cout << "addfriend insert friends success" << std::endl;return true;}catch (sql::SQLException& e) {// 如果发生错误,回滚事务if (con) {con->_con->rollback();}std::cerr << "SQLException: " << e.what();std::cerr << " (MySQL error code: " << e.getErrorCode();std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;return false;}return true;}
分析
这段代码死锁的关键点在于同意好友好插入用户顺序不同,导致死锁。
{// 3. 准备第一个SQL语句, 插入认证方好友数据std::unique_ptr<sql::PreparedStatement> pstmt(con->_con->prepareStatement("INSERT IGNORE INTO friend(self_id, friend_id, back) ""VALUES (?, ?, ?) "));//反过来的申请时from,验证时topstmt->setInt(1, from); // from idpstmt->setInt(2, to);pstmt->setString(3, back_name);// 执行更新int rowAffected = pstmt->executeUpdate();if (rowAffected < 0) {con->_con->rollback();return false;}//准备第二个SQL语句,插入申请方好友数据std::unique_ptr<sql::PreparedStatement> pstmt2(con->_con->prepareStatement("INSERT IGNORE INTO friend(self_id, friend_id, back) ""VALUES (?, ?, ?) "));//反过来的申请时from,验证时topstmt2->setInt(1, to); // from idpstmt2->setInt(2, from);pstmt2->setString(3, reverse_back);// 执行更新int rowAffected2 = pstmt2->executeUpdate();if (rowAffected2 < 0) {con->_con->rollback();return false;}}
解决方案
保证插入顺序一致即可
bool MysqlDao::AddFriend(const int& from, const int& to, std::string back_name,std::vector<std::shared_ptr<AddFriendMsg>>& chat_datas) {auto con = pool_->getConnection();if (con == nullptr) {return false;}Defer defer([this, &con]() {pool_->returnConnection(std::move(con));});try {// 开始事务con->_con->setAutoCommit(false);std::string reverse_back;std::string apply_desc;{// 1. 锁定并读取std::unique_ptr<sql::PreparedStatement> selStmt(con->_con->prepareStatement("SELECT back_name, descs ""FROM friend_apply ""WHERE from_uid = ? AND to_uid = ? ""FOR UPDATE"));selStmt->setInt(1, to);selStmt->setInt(2, from);std::unique_ptr<sql::ResultSet> rsSel(selStmt->executeQuery());if (rsSel->next()) {reverse_back = rsSel->getString("back_name");apply_desc = rsSel->getString("descs");}else {// 没有对应的申请记录,直接 rollback 并返回失败con->_con->rollback();return false;}}{// 2. 执行真正的更新std::unique_ptr<sql::PreparedStatement> updStmt(con->_con->prepareStatement("UPDATE friend_apply ""SET status = 1 ""WHERE from_uid = ? AND to_uid = ?"));updStmt->setInt(1, to);updStmt->setInt(2, from);if (updStmt->executeUpdate() != 1) {// 更新行数不对,回滚con->_con->rollback();return false;}}{// 3. 插入好友关系 - 关键改进:按照固定顺序插入避免死锁// 确定插入顺序:始终按照 uid 大小顺序int smaller_uid = std::min(from, to);int larger_uid = std::max(from, to);// 第一次插入:较小的 uid 作为 self_idstd::unique_ptr<sql::PreparedStatement> pstmt(con->_con->prepareStatement("INSERT IGNORE INTO friend(self_id, friend_id, back) ""VALUES (?, ?, ?)"));if (from == smaller_uid) {pstmt->setInt(1, from);pstmt->setInt(2, to);pstmt->setString(3, back_name);}else {pstmt->setInt(1, to);pstmt->setInt(2, from);pstmt->setString(3, reverse_back);}int rowAffected = pstmt->executeUpdate();if (rowAffected < 0) {con->_con->rollback();return false;}// 第二次插入:较大的 uid 作为 self_idstd::unique_ptr<sql::PreparedStatement> pstmt2(con->_con->prepareStatement("INSERT IGNORE INTO friend(self_id, friend_id, back) ""VALUES (?, ?, ?)"));if (from == larger_uid) {pstmt2->setInt(1, from);pstmt2->setInt(2, to);pstmt2->setString(3, back_name);}else {pstmt2->setInt(1, to);pstmt2->setInt(2, from);pstmt2->setString(3, reverse_back);}int rowAffected2 = pstmt2->executeUpdate();if (rowAffected2 < 0) {con->_con->rollback();return false;}}// 4. 创建 chat_threadlong long threadId = 0;{std::unique_ptr<sql::PreparedStatement> threadStmt(con->_con->prepareStatement("INSERT INTO chat_thread (type, created_at) VALUES ('private', NOW())"));threadStmt->executeUpdate();std::unique_ptr<sql::Statement> stmt(con->_con->createStatement());std::unique_ptr<sql::ResultSet> rs(stmt->executeQuery("SELECT LAST_INSERT_ID()"));if (rs->next()) {threadId = rs->getInt64(1);}else {con->_con->rollback();return false;}}// 5. 插入 private_chat{std::unique_ptr<sql::PreparedStatement> pcStmt(con->_con->prepareStatement("INSERT INTO private_chat(thread_id, user1_id, user2_id) VALUES (?, ?, ?)"));pcStmt->setInt64(1, threadId);pcStmt->setInt(2, from);pcStmt->setInt(3, to);if (pcStmt->executeUpdate() < 0) {con->_con->rollback();return false;}}// 6. 插入初始消息(申请描述)if (!apply_desc.empty()){std::unique_ptr<sql::PreparedStatement> msgStmt(con->_con->prepareStatement("INSERT INTO chat_message(thread_id, sender_id, recv_id, content, created_at, updated_at, status) ""VALUES (?, ?, ?, ?, NOW(), NOW(), ?)"));msgStmt->setInt64(1, threadId);msgStmt->setInt(2, to);msgStmt->setInt(3, from);msgStmt->setString(4, apply_desc);msgStmt->setInt(5, 2);if (msgStmt->executeUpdate() < 0) {con->_con->rollback();return false;}std::unique_ptr<sql::Statement> stmt(con->_con->createStatement());std::unique_ptr<sql::ResultSet> rs(stmt->executeQuery("SELECT LAST_INSERT_ID()"));if (rs->next()) {auto messageId = rs->getInt64(1);auto tx_data = std::make_shared<AddFriendMsg>();tx_data->set_sender_id(to);tx_data->set_msg_id(messageId);tx_data->set_msgcontent(apply_desc);tx_data->set_thread_id(threadId);tx_data->set_unique_id("");tx_data->set_status(2);std::cout << "addfriend insert message success" << std::endl;chat_datas.push_back(tx_data);}else {con->_con->rollback();return false;}}// 7. 插入成为好友的消息{std::unique_ptr<sql::PreparedStatement> msgStmt(con->_con->prepareStatement("INSERT INTO chat_message(thread_id, sender_id, recv_id, content, created_at, updated_at, status) ""VALUES (?, ?, ?, ?, NOW(), NOW(), ?)"));msgStmt->setInt64(1, threadId);msgStmt->setInt(2, from);msgStmt->setInt(3, to);msgStmt->setString(4, "We are friends now!");msgStmt->setInt(5, 2);if (msgStmt->executeUpdate() < 0) {con->_con->rollback();return false;}std::unique_ptr<sql::Statement> stmt(con->_con->createStatement());std::unique_ptr<sql::ResultSet> rs(stmt->executeQuery("SELECT LAST_INSERT_ID()"));if (rs->next()) {auto messageId = rs->getInt64(1);auto tx_data = std::make_shared<AddFriendMsg>();tx_data->set_sender_id(from);tx_data->set_msg_id(messageId);tx_data->set_msgcontent("We are friends now!");tx_data->set_thread_id(threadId);tx_data->set_unique_id("");tx_data->set_status(2);chat_datas.push_back(tx_data);}else {con->_con->rollback();return false;}}// 提交事务con->_con->commit();std::cout << "addfriend insert friends success" << std::endl;return true;}catch (sql::SQLException& e) {// 如果发生错误,回滚事务if (con) {con->_con->rollback();}std::cerr << "SQLException: " << e.what();std::cerr << " (MySQL error code: " << e.getErrorCode();std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;// 如果是死锁错误(1213),可以考虑重试if (e.getErrorCode() == 1213) {std::cerr << "Deadlock detected, consider retry" << std::endl;}return false;}return true;}