私聊功能代码中的死锁分析

📌 问题概述

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

表结构

private_chat表

image-20251126232246544

chat_thread表

image-20251126232625472

聊天界面

image.png

📌死锁源码

  1. bool MysqlDao::CreatePrivateChat(int user1_id, int user2_id, int& thread_id)
  2. {
  3. auto con = pool_->getConnection();
  4. if (!con) {
  5. return false;
  6. }
  7. Defer defer([this, &con]() {
  8. pool_->returnConnection(std::move(con));
  9. });
  10. auto& conn = con->_con;
  11. try {
  12. // 开启事务
  13. conn->setAutoCommit(false);
  14. // 1. 查询是否已存在私聊并加行级锁
  15. int uid1 = std::min(user1_id, user2_id);
  16. int uid2 = std::max(user1_id, user2_id);
  17. std::string check_sql =
  18. "SELECT thread_id FROM private_chat "
  19. "WHERE (user1_id = ? AND user2_id = ?) "
  20. "FOR UPDATE;";
  21. std::unique_ptr<sql::PreparedStatement> pstmt(conn->prepareStatement(check_sql));
  22. pstmt->setInt64(1, uid1);
  23. pstmt->setInt64(2, uid2);
  24. std::unique_ptr<sql::ResultSet> res(pstmt->executeQuery());
  25. if (res->next()) {
  26. // 如果已存在,返回该 thread_id
  27. thread_id = res->getInt("thread_id");
  28. conn->commit(); // 提交事务
  29. return true;
  30. }
  31. // 2. 如果未找到,创建新的 chat_thread 和 private_chat 记录
  32. // 在 chat_thread 表插入新记录
  33. std::string insert_chat_thread_sql =
  34. "INSERT INTO chat_thread (type, created_at) VALUES ('private', NOW());";
  35. std::unique_ptr<sql::PreparedStatement> pstmt_insert_thread(conn->prepareStatement(insert_chat_thread_sql));
  36. pstmt_insert_thread->executeUpdate();
  37. // 获取新插入的 thread_id
  38. std::string get_last_insert_id_sql = "SELECT LAST_INSERT_ID();";
  39. std::unique_ptr<sql::PreparedStatement> pstmt_last_insert_id(conn->prepareStatement(get_last_insert_id_sql));
  40. std::unique_ptr<sql::ResultSet> res_last_id(pstmt_last_insert_id->executeQuery());
  41. res_last_id->next();
  42. thread_id = res_last_id->getInt(1);
  43. // 3. 在 private_chat 表插入新记录
  44. std::string insert_private_chat_sql =
  45. "INSERT INTO private_chat (thread_id, user1_id, user2_id, created_at) "
  46. "VALUES (?, ?, ?, NOW());";
  47. std::unique_ptr<sql::PreparedStatement> pstmt_insert_private(conn->prepareStatement(insert_private_chat_sql));
  48. pstmt_insert_private->setInt64(1, thread_id);
  49. pstmt_insert_private->setInt64(2, uid1);
  50. pstmt_insert_private->setInt64(3, uid2);
  51. pstmt_insert_private->executeUpdate();
  52. // 提交事务
  53. conn->commit();
  54. return true;
  55. }
  56. catch (sql::SQLException& e) {
  57. std::cerr << "SQLException: " << e.what() << std::endl;
  58. conn->rollback();
  59. return false;
  60. }
  61. return false;
  62. }

image-20251204130749212

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操作之前设置。它的特点是:

  1. 不会阻塞其他插入意向锁:多个事务可以同时在同一个间隙中获取插入意向锁,只要它们插入的位置不同
  2. 会被间隙锁和临键锁阻塞:如果间隙已经被其他事务的间隙锁或临键锁锁定,插入操作会等待
  3. 提高并发性:允许多个事务并发地向同一间隙插入不同的记录

例如,如果索引中有记录10和20,两个事务可以同时在(10, 20)这个间隙中插入不同的值(如12和15),因为插入意向锁之间不冲突。

这三种锁机制共同协作,在保证数据一致性的同时,尽可能提高并发性能。

间隙锁 (Gap Lock)
• 锁定索引记录之间的间隙 │
• 不包括记录本身 │
• 例如: 记录3和记录7之间的空隙

临键锁如何工作

让我用具体的例子来解释临键锁是如何”组合”记录锁和间隙锁的。

先理解间隙锁(Gap Lock)

间隙锁锁定的是两个索引记录之间的间隙,不包括记录本身。它的作用是防止其他事务在这个间隙中插入新记录。

临键锁的组合特性

假设有一张表,索引列有值:10, 20, 30

  1. 索引值: 10 20 30
  2. 间隙: (∞,10) (10,20) (20,30) (30,∞)

临键锁 = 间隙锁 + 记录锁,具体来说:

临键锁 (10, 20] 意味着:

  • 间隙锁部分:锁定 (10, 20) 这个开区间,防止插入11-19的记录
  • 记录锁部分:锁定值为20的记录本身

实际场景举例

  1. -- 假设表中有记录: id = 10, 20, 30
  2. SELECT * FROM users WHERE id >= 20 FOR UPDATE;

这个查询会加临键锁:

  • (10, 20] - 锁住间隙(10,20)和记录20
  • (20, 30] - 锁住间隙(20,30)和记录30
  • (30, +∞) - 锁住30之后的所有间隙

为什么要这样组合?

这种组合设计巧妙地解决了幻读问题

  1. -- 事务A
  2. SELECT * FROM users WHERE id >= 20 FOR UPDATE;
  3. -- 结果:找到id=20, 30两条记录
  4. -- 如果只有记录锁,事务B可以:
  5. INSERT INTO users VALUES (25, 'New'); -- 成功插入!
  6. -- 事务A再次查询
  7. SELECT * FROM users WHERE id >= 20 FOR UPDATE;
  8. -- 结果:找到id=20, 25, 30三条记录 出现幻读!

但有了临键锁的间隙锁部分,事务B的插入会被阻塞,因为25落在了(20,30]的间隙范围内。

总结理解

临键锁 = 锁住一个范围的两个部分

  1. 记录锁部分:锁住右边界的那条记录(比如20)
  2. 间隙锁部分:锁住左边界到右边界之间的间隙(比如10到20之间)

这样既保护了记录本身不被修改,又保护了间隙不被插入新记录,从而在可重复读隔离级别下避免幻读。

按类型分类

共享锁(Shared Lock,S锁)- 读锁,允许多个事务同时持有

排他锁(Exclusive Lock,X锁)- 写锁,独占访问

它们的组合关系

记录锁可以是共享锁,也可以是排他锁

  1. -- 记录锁 + 共享锁
  2. SELECT * FROM users WHERE id = 10 FOR SHARE;
  3. -- id=10的记录上加"共享记录锁"
  4. -- 记录锁 + 排他锁
  5. SELECT * FROM users WHERE id = 10 FOR UPDATE;
  6. -- id=10的记录上加"排他记录锁"
  7. UPDATE users SET name = 'John' WHERE id = 10;
  8. -- 也是加"排他记录锁"

形象的比喻

记录锁想象成一个房间,而排他锁/共享锁门锁的类型

  • 共享锁(S锁):像”阅览室”,多个人可以同时进来看书(多个事务可以同时读)
  • 排他锁(X锁):像”编辑室”,只能一个人进去修改文档(只有一个事务可以写,其他读写都被阻塞)

完整的描述方式

所以准确的说法应该是:

  • “在这条记录上加了排他的记录锁”
  • “在这条记录上加了共享的记录锁”

🔑 间隙锁的特性

  1. 特性1: 间隙锁之间相互兼容
  2. ┌──────────────────────────────────┐
  3. 事务A: 持有 Gap(1,5)
  4. 事务B: 可以获取 Gap(1,5)
  5. 事务C: 可以获取 Gap(1,5)
  6. └──────────────────────────────────┘
  7. 特性2: 间隙锁阻止插入
  8. ┌──────────────────────────────────┐
  9. 事务A: 持有 Gap(1,5)
  10. 事务B: INSERT id=3
  11. (需要插入意向锁,被阻塞)
  12. └──────────────────────────────────┘
  13. 特性3: 仅在 RR 隔离级别下生效
  14. ┌──────────────────────────────────┐
  15. REPEATABLE READ: 有间隙锁
  16. READ COMMITTED: 无间隙锁
  17. └──────────────────────────────────┘

🔑 插入意向锁的特性

  1. 特性1: 与间隙锁互斥
  2. ┌──────────────────────────────────┐
  3. 事务A: 持有 Gap Lock(1,5)
  4. 事务B: INSERT id=3
  5. 需要 Insert Intention
  6. A的间隙锁阻塞
  7. └──────────────────────────────────┘
  8. 特性2: 插入意向锁之间可以兼容(插入不同行时)
  9. ┌──────────────────────────────────┐
  10. 事务A: INSERT id=2 (等待中)
  11. 事务B: INSERT id=4 (等待中)
  12. 如果没有间隙锁阻塞
  13. 两者可以并发执行
  14. └──────────────────────────────────┘

问题代码结构

  1. bool CreatePrivateChat(int user1_id, int user2_id, int& thread_id)
  2. {
  3. conn->setAutoCommit(false); // 开启事务
  4. // ⚠️ 第一步: SELECT ... FOR UPDATE
  5. // 如果未找到记录 → 获取间隙锁
  6. std::string check_sql =
  7. "SELECT thread_id FROM private_chat "
  8. "WHERE (user1_id = ? AND user2_id = ?) "
  9. "FOR UPDATE;";
  10. if (res->next()) {
  11. // 找到记录,返回
  12. thread_id = res->getInt("thread_id");
  13. conn->commit();
  14. return true;
  15. }
  16. // ⚠️ 第二步: INSERT 新记录
  17. // 需要获取插入意向锁 → 与间隙锁冲突
  18. std::string insert_sql =
  19. "INSERT INTO private_chat "
  20. "(thread_id, user1_id, user2_id, created_at) "
  21. "VALUES (?, ?, ?, NOW());";
  22. conn->commit();
  23. }

表结构假设

  1. CREATE TABLE private_chat (
  2. thread_id INT,
  3. user1_id INT,
  4. user2_id INT,
  5. created_at DATETIME,
  6. INDEX idx_users (user1_id, user2_id) -- 复合索引
  7. );
  8. -- 当前数据
  9. ┌──────────┬──────────┬──────────┐
  10. user1_id user2_id thread_id
  11. ├──────────┼──────────┼──────────┤
  12. 1 3 10
  13. 2 4 20
  14. 5 8 30
  15. └──────────┴──────────┴──────────┘
  16. -- 索引树的间隙
  17. Gap: (-∞, (1,3))
  18. Gap: ((1,3), (2,4)) 死锁发生的间隙
  19. Gap: ((2,4), (5,8))
  20. Gap: ((5,8), +∞)

第三部分:死锁发生的完整流程

并发场景

两个线程同时调用: CreatePrivateChat(1, 2, thread_id)

  1. ┌─────────────────────────────────────────────────────┐
  2. 并发请求:
  3. 线程1: CreatePrivateChat(1, 2, thread_id_A)
  4. 线程2: CreatePrivateChat(1, 2, thread_id_B)
  5. 目标: 创建 user1_id=1, user2_id=2 的私聊
  6. 索引位置: 落在间隙 ((1,3), (2,4)) 之间
  7. └─────────────────────────────────────────────────────┘

详细时间线

  1. 时间 事务 A 事务 B
  2. ────────────────────────────────────────────────────────────
  3. T0 BEGIN BEGIN
  4. T1 SELECT ... FOR UPDATE
  5. WHERE user1_id=1 AND user2_id=2 (等待中)
  6. 检查索引: (1,2)
  7. 未找到记录
  8. T2 获取间隙锁 Gap((1,3), (2,4)) SELECT ... FOR UPDATE
  9. (锁定查询区间) WHERE user1_id=1 AND user2_id=2
  10. 检查索引: (1,2)
  11. 未找到记录
  12. T3 准备 INSERT 获取间隙锁 Gap((1,3), (2,4))
  13. thread_id = 100 ⚠️ 间隙锁兼容! 成功获取!
  14. T4 INSERT INTO private_chat 准备 INSERT
  15. VALUES (100, 1, 2, NOW()) thread_id = 101
  16. 需要插入意向锁
  17. Insert Intention Lock
  18. T5 检测到 B 持有 Gap Lock INSERT INTO private_chat
  19. 插入意向锁与间隙锁冲突 VALUES (101, 1, 2, NOW())
  20. 等待 B 释放间隙锁...
  21. 需要插入意向锁
  22. Insert Intention Lock
  23. T6 (继续等待) 检测到 A 持有 Gap Lock
  24. 持有: Gap Lock 插入意向锁与间隙锁冲突
  25. 等待: B 释放 Gap Lock 等待 A 释放间隙锁...
  26. 持有: Gap Lock
  27. 等待: A 释放 Gap Lock
  28. T7 💀 DEADLOCK DETECTED!
  29. ─────────────────────────────────────────────
  30. MySQL 检测到循环等待:
  31. A 持有间隙锁, 等待 B 释放间隙锁
  32. B 持有间隙锁, 等待 A 释放间隙锁
  33. ─────────────────────────────────────────────
  34. 回滚事务 B (选择代价较小的事务)
  35. 事务 A 继续执行
  36. INSERT 成功
  37. COMMIT

第四部分:死锁的核心机制

锁兼容性矩阵

  1. ┌───────────────────────────────────────────────────┐
  2. 锁类型兼容性(RR隔离级别)
  3. ├─────────────┬────────┬──────────┬───────────────┤
  4. 持有\请 间隙锁 记录锁 插入意向锁
  5. ├─────────────┼────────┼──────────┼───────────────┤
  6. 间隙锁 ✅兼容 ✅兼容 冲突
  7. 记录锁 ✅兼容 冲突 冲突
  8. 插入意向锁 ❌冲突 冲突 ✅兼容(不同行)│
  9. └─────────────┴────────┴──────────┴───────────────┘
  10. ⚠️ 核心规则:
  11. 间隙锁 + 间隙锁 = 兼容(可以同时持有)
  12. 间隙锁 + 插入意向锁 = 冲突(互相阻塞)

死锁的四个必要条件

  1. ┌──────────────────────────────────────────────┐
  2. 本案例中的死锁条件分析
  3. ├──────────────────────────────────────────────┤
  4. 1️⃣ 互斥条件
  5. 插入意向锁 间隙锁 互斥
  6. 2️⃣ 持有并等待
  7. A 持有间隙锁, 等待获取插入意向锁
  8. B 持有间隙锁, 等待获取插入意向锁
  9. 3️⃣ 不可剥夺
  10. 间隙锁只能在事务提交/回滚时释放
  11. 4️⃣ 循环等待
  12. A 等待 B B 等待 A
  13. └──────────────────────────────────────────────┘

可视化流程图

  1. ┌────────────────────────────────────────────────────┐
  2. 死锁形成的循环依赖图
  3. └────────────────────────────────────────────────────┘
  4. 事务 A 事务 B
  5. [持有间隙锁] [持有间隙锁]
  6. Gap(1,3)~(2,4) Gap(1,3)~(2,4)
  7. 需要插入意向锁 需要插入意向锁
  8. Insert Intention Insert Intention
  9. │◄─────────┐ ┌───────────►│
  10. 等待B释放│ 等待A释放
  11. 间隙锁 间隙锁
  12. └──────────┘ └────────────┘
  13. 💀 循环等待
  14. 解释:
  15. 两个事务都在 T2-T3 成功获取了间隙锁(兼容)
  16. 两个事务都在 T5-T6 尝试获取插入意向锁
  17. 插入意向锁被对方的间隙锁阻塞
  18. 形成 ABA 的循环依赖

第五部分:为什么会发生死锁?

根本原因分析

  1. ┌─────────────────────────────────────────────────┐
  2. 死锁的三个关键因素
  3. └─────────────────────────────────────────────────┘
  4. 因素1: FOR UPDATE 的锁定范围
  5. ─────────────────────────────
  6. SELECT ... FOR UPDATE 未找到记录时:
  7. 不仅扫描索引范围
  8. 还会对扫描的间隙加间隙锁
  9. 目的: 防止其他事务插入满足条件的记录(防止幻读)
  10. 查询: WHERE user1_id=1 AND user2_id=2
  11. 索引: idx_users(user1_id, user2_id)
  12. 加锁: Gap Lock on ((1,3), (2,4))
  13. 因素2: 间隙锁的兼容性
  14. ─────────────────────────────
  15. 两个事务可以同时持有同一间隙的间隙锁:
  16. 事务A: Gap Lock
  17. 事务B: Gap Lock (不冲突)
  18. ⚠️ 这是死锁的前提条件!
  19. 因素3: 插入意向锁的互斥性
  20. ─────────────────────────────
  21. INSERT 操作需要获取插入意向锁:
  22. 检查目标间隙是否有间隙锁
  23. 如果有 等待间隙锁释放
  24. 事务A: INSERT 需要插入意向锁 B的间隙锁阻塞
  25. 事务B: INSERT 需要插入意向锁 A的间隙锁阻塞
  26. 💀 循环等待形成!

死锁可以回滚还需要修复吗

🚨 问题1: 用户体验差

  1. 用户请求流程:
  2. ┌─────────────────────────────────────────┐
  3. 用户A: 创建与用户B的私聊
  4. 发起请求...
  5. 💀 死锁! 事务被回滚
  6. 返回 false
  7. 用户看到"创建失败"
  8. 😤 用户体验: 明明是正常操作,为什么失败?│
  9. └─────────────────────────────────────────┘

问题: 虽然没有数据错误,但用户需要重试,体验不好。


🚨 问题2: 高并发下的性能损耗

  1. 高并发场景 (100个并发请求):
  2. ┌──────────────────────────────────────────┐
  3. 100 个并发请求创建私聊
  4. 50 个事务获取间隙锁
  5. 50 个事务也获取间隙锁
  6. 所有事务尝试 INSERT
  7. 💀 大量死锁!
  8. 50% 的事务被回滚
  9. 这些事务需要重试
  10. 数据库压力翻倍
  11. CPU 消耗在死锁检测上
  12. 大量事务日志写入
  13. └──────────────────────────────────────────┘
  14. 性能损失:
  15. 死锁检测: 消耗 CPU
  16. 回滚操作: 消耗 IO
  17. 客户端重试: 消耗网络和数据库连接

第六部分:解决方案

✅先INSERT后SELECT(乐观策略)

原理: 先尝试插入,如果失败说明已存在,再查询

  1. // 在 private_chat 表上添加唯一索引
  2. // CREATE UNIQUE INDEX uk_user_pair ON private_chat(user1_id, user2_id);
  3. bool CreatePrivateChat(int user1_id, int user2_id, int& thread_id)
  4. {
  5. auto con = pool_->getConnection();
  6. Defer defer([this, &con]() { pool_->returnConnection(std::move(con)); });
  7. auto& conn = con->_con;
  8. int uid1 = std::min(user1_id, user2_id);
  9. int uid2 = std::max(user1_id, user2_id);
  10. try {
  11. conn->setAutoCommit(false);
  12. // 先创建 chat_thread
  13. std::string insert_thread_sql =
  14. "INSERT INTO chat_thread (type, created_at) VALUES ('private', NOW());";
  15. std::unique_ptr<sql::PreparedStatement> pstmt_thread(
  16. conn->prepareStatement(insert_thread_sql));
  17. pstmt_thread->executeUpdate();
  18. // 获取 thread_id
  19. std::unique_ptr<sql::PreparedStatement> pstmt_id(
  20. conn->prepareStatement("SELECT LAST_INSERT_ID();"));
  21. std::unique_ptr<sql::ResultSet> res_id(pstmt_id->executeQuery());
  22. res_id->next();
  23. thread_id = res_id->getInt(1);
  24. // ✅ 直接尝试插入
  25. std::string insert_sql =
  26. "INSERT INTO private_chat (thread_id, user1_id, user2_id, created_at) "
  27. "VALUES (?, ?, ?, NOW());";
  28. std::unique_ptr<sql::PreparedStatement> pstmt_insert(
  29. conn->prepareStatement(insert_sql));
  30. pstmt_insert->setInt64(1, thread_id);
  31. pstmt_insert->setInt64(2, uid1);
  32. pstmt_insert->setInt64(3, uid2);
  33. pstmt_insert->executeUpdate();
  34. conn->commit();
  35. return true;
  36. }
  37. catch (sql::SQLException& e) {
  38. conn->rollback();
  39. // 如果是唯一键冲突 (错误码 1062)
  40. if (e.getErrorCode() == 1062) {
  41. // ✅ 查询已存在的记录
  42. try {
  43. std::string query_sql =
  44. "SELECT thread_id FROM private_chat "
  45. "WHERE user1_id = ? AND user2_id = ?;";
  46. std::unique_ptr<sql::PreparedStatement> pstmt_query(
  47. conn->prepareStatement(query_sql));
  48. pstmt_query->setInt64(1, uid1);
  49. pstmt_query->setInt64(2, uid2);
  50. std::unique_ptr<sql::ResultSet> res(pstmt_query->executeQuery());
  51. if (res->next()) {
  52. thread_id = res->getInt("thread_id");
  53. return true;
  54. }
  55. }
  56. catch (sql::SQLException& e2) {
  57. std::cerr << "Query error: " << e2.what() << std::endl;
  58. }
  59. }
  60. std::cerr << "SQLException: " << e.what() << std::endl;
  61. return false;
  62. }
  63. }

优点:

  • ✅ 避免死锁
  • ✅ 大部分情况下性能好(只有一次数据库操作)

缺点:

  • ⚠️ 需要唯一索引
  • ⚠️ 依赖异常处理

记录锁死锁分析

image-20251204140931698

  1. bool MysqlDao::AddFriend(const int& from, const int& to, std::string back_name,
  2. std::vector<std::shared_ptr<AddFriendMsg>>& chat_datas) {
  3. auto con = pool_->getConnection();
  4. if (con == nullptr) {
  5. return false;
  6. }
  7. Defer defer([this, &con]() {
  8. pool_->returnConnection(std::move(con));
  9. });
  10. try {
  11. //开始事务
  12. con->_con->setAutoCommit(false);
  13. std::string reverse_back;
  14. std::string apply_desc;
  15. {
  16. // 1. 锁定并读取
  17. std::unique_ptr<sql::PreparedStatement> selStmt(con->_con->prepareStatement(
  18. "SELECT back_name, descs "
  19. "FROM friend_apply "
  20. "WHERE from_uid = ? AND to_uid = ? "
  21. "FOR UPDATE"
  22. ));
  23. selStmt->setInt(1, to);
  24. selStmt->setInt(2, from);
  25. std::unique_ptr<sql::ResultSet> rsSel(selStmt->executeQuery());
  26. if (rsSel->next()) {
  27. reverse_back = rsSel->getString("back_name");
  28. apply_desc = rsSel->getString("descs");
  29. }
  30. else {
  31. // 没有对应的申请记录,直接 rollback 并返回失败
  32. con->_con->rollback();
  33. return false;
  34. }
  35. }
  36. {
  37. // 2. 执行真正的更新
  38. std::unique_ptr<sql::PreparedStatement> updStmt(con->_con->prepareStatement(
  39. "UPDATE friend_apply "
  40. "SET status = 1 "
  41. "WHERE from_uid = ? AND to_uid = ?"
  42. ));
  43. updStmt->setInt(1, to);
  44. updStmt->setInt(2, from);
  45. if (updStmt->executeUpdate() != 1) {
  46. // 更新行数不对,回滚
  47. con->_con->rollback();
  48. return false;
  49. }
  50. }
  51. {
  52. // 3. 准备第一个SQL语句, 插入认证方好友数据
  53. std::unique_ptr<sql::PreparedStatement> pstmt(con->_con->prepareStatement("INSERT IGNORE INTO friend(self_id, friend_id, back) "
  54. "VALUES (?, ?, ?) "
  55. ));
  56. //反过来的申请时from,验证时to
  57. pstmt->setInt(1, from); // from id
  58. pstmt->setInt(2, to);
  59. pstmt->setString(3, back_name);
  60. // 执行更新
  61. int rowAffected = pstmt->executeUpdate();
  62. if (rowAffected < 0) {
  63. con->_con->rollback();
  64. return false;
  65. }
  66. //准备第二个SQL语句,插入申请方好友数据
  67. std::unique_ptr<sql::PreparedStatement> pstmt2(con->_con->prepareStatement("INSERT IGNORE INTO friend(self_id, friend_id, back) "
  68. "VALUES (?, ?, ?) "
  69. ));
  70. //反过来的申请时from,验证时to
  71. pstmt2->setInt(1, to); // from id
  72. pstmt2->setInt(2, from);
  73. pstmt2->setString(3, reverse_back);
  74. // 执行更新
  75. int rowAffected2 = pstmt2->executeUpdate();
  76. if (rowAffected2 < 0) {
  77. con->_con->rollback();
  78. return false;
  79. }
  80. }
  81. // 4. 创建 chat_thread
  82. long long threadId = 0;
  83. {
  84. std::unique_ptr<sql::PreparedStatement> threadStmt(con->_con->prepareStatement(
  85. "INSERT INTO chat_thread (type, created_at) VALUES ('private', NOW());"
  86. ));
  87. threadStmt->executeUpdate();
  88. std::unique_ptr<sql::Statement> stmt(con->_con->createStatement());
  89. std::unique_ptr<sql::ResultSet> rs(
  90. stmt->executeQuery("SELECT LAST_INSERT_ID()")
  91. );
  92. if (rs->next()) {
  93. threadId = rs->getInt64(1);
  94. }
  95. else {
  96. return false;
  97. }
  98. }
  99. // 5. 插入 private_chat
  100. {
  101. std::unique_ptr<sql::PreparedStatement> pcStmt(con->_con->prepareStatement(
  102. "INSERT INTO private_chat(thread_id, user1_id, user2_id) VALUES (?, ?, ?)"
  103. ));
  104. pcStmt->setInt64(1, threadId);
  105. pcStmt->setInt(2, from);
  106. pcStmt->setInt(3, to);
  107. if (pcStmt->executeUpdate() < 0) return false;
  108. }
  109. // 6. 可选:插入初始消息到 chat_message
  110. if (apply_desc.empty() == false)
  111. {
  112. std::unique_ptr<sql::PreparedStatement> msgStmt(con->_con->prepareStatement(
  113. "INSERT INTO chat_message(thread_id, sender_id, recv_id, content,created_at, updated_at, status) VALUES (?, ?, ?, ?,NOW(),NOW(),?)"
  114. ));
  115. msgStmt->setInt64(1, threadId);
  116. msgStmt->setInt(2, to);
  117. msgStmt->setInt(3, from);
  118. msgStmt->setString(4, apply_desc);
  119. msgStmt->setInt(5, 2);
  120. if (msgStmt->executeUpdate() < 0) { return false; }
  121. std::unique_ptr<sql::Statement> stmt(con->_con->createStatement());
  122. std::unique_ptr<sql::ResultSet> rs(
  123. stmt->executeQuery("SELECT LAST_INSERT_ID()")
  124. );
  125. if (rs->next()) {
  126. auto messageId = rs->getInt64(1);
  127. auto tx_data = std::make_shared<AddFriendMsg>();
  128. tx_data->set_sender_id(to);
  129. tx_data->set_msg_id(messageId);
  130. tx_data->set_msgcontent(apply_desc);
  131. tx_data->set_thread_id(threadId);
  132. tx_data->set_unique_id("");
  133. tx_data->set_status(2);
  134. std::cout << "addfriend insert message success" << std::endl;
  135. chat_datas.push_back(tx_data);
  136. }
  137. else {
  138. return false;
  139. }
  140. }
  141. {
  142. std::unique_ptr<sql::PreparedStatement> msgStmt(con->_con->prepareStatement(
  143. "INSERT INTO chat_message(thread_id, sender_id, recv_id, content, created_at, updated_at, status) VALUES (?, ?, ?, ?,NOW(),NOW(),?)"
  144. ));
  145. msgStmt->setInt64(1, threadId);
  146. msgStmt->setInt(2, from);
  147. msgStmt->setInt(3, to);
  148. msgStmt->setString(4, "We are friends now!");
  149. msgStmt->setInt(5, 2);
  150. if (msgStmt->executeUpdate() < 0) { return false; }
  151. std::unique_ptr<sql::Statement> stmt(con->_con->createStatement());
  152. std::unique_ptr<sql::ResultSet> rs(
  153. stmt->executeQuery("SELECT LAST_INSERT_ID()")
  154. );
  155. if (rs->next()) {
  156. auto messageId = rs->getInt64(1);
  157. auto tx_data = std::make_shared<AddFriendMsg>();
  158. tx_data->set_sender_id(from);
  159. tx_data->set_msg_id(messageId);
  160. tx_data->set_msgcontent("We are friends now!");
  161. tx_data->set_thread_id(threadId);
  162. tx_data->set_unique_id("");
  163. tx_data->set_status(2);
  164. chat_datas.push_back(tx_data);
  165. }
  166. else {
  167. return false;
  168. }
  169. }
  170. // 提交事务
  171. con->_con->commit();
  172. std::cout << "addfriend insert friends success" << std::endl;
  173. return true;
  174. }
  175. catch (sql::SQLException& e) {
  176. // 如果发生错误,回滚事务
  177. if (con) {
  178. con->_con->rollback();
  179. }
  180. std::cerr << "SQLException: " << e.what();
  181. std::cerr << " (MySQL error code: " << e.getErrorCode();
  182. std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;
  183. return false;
  184. }
  185. return true;
  186. }

分析

这段代码死锁的关键点在于同意好友好插入用户顺序不同,导致死锁。

  1. {
  2. // 3. 准备第一个SQL语句, 插入认证方好友数据
  3. std::unique_ptr<sql::PreparedStatement> pstmt(con->_con->prepareStatement("INSERT IGNORE INTO friend(self_id, friend_id, back) "
  4. "VALUES (?, ?, ?) "
  5. ));
  6. //反过来的申请时from,验证时to
  7. pstmt->setInt(1, from); // from id
  8. pstmt->setInt(2, to);
  9. pstmt->setString(3, back_name);
  10. // 执行更新
  11. int rowAffected = pstmt->executeUpdate();
  12. if (rowAffected < 0) {
  13. con->_con->rollback();
  14. return false;
  15. }
  16. //准备第二个SQL语句,插入申请方好友数据
  17. std::unique_ptr<sql::PreparedStatement> pstmt2(con->_con->prepareStatement("INSERT IGNORE INTO friend(self_id, friend_id, back) "
  18. "VALUES (?, ?, ?) "
  19. ));
  20. //反过来的申请时from,验证时to
  21. pstmt2->setInt(1, to); // from id
  22. pstmt2->setInt(2, from);
  23. pstmt2->setString(3, reverse_back);
  24. // 执行更新
  25. int rowAffected2 = pstmt2->executeUpdate();
  26. if (rowAffected2 < 0) {
  27. con->_con->rollback();
  28. return false;
  29. }
  30. }

解决方案

保证插入顺序一致即可

  1. bool MysqlDao::AddFriend(const int& from, const int& to, std::string back_name,
  2. std::vector<std::shared_ptr<AddFriendMsg>>& chat_datas) {
  3. auto con = pool_->getConnection();
  4. if (con == nullptr) {
  5. return false;
  6. }
  7. Defer defer([this, &con]() {
  8. pool_->returnConnection(std::move(con));
  9. });
  10. try {
  11. // 开始事务
  12. con->_con->setAutoCommit(false);
  13. std::string reverse_back;
  14. std::string apply_desc;
  15. {
  16. // 1. 锁定并读取
  17. std::unique_ptr<sql::PreparedStatement> selStmt(con->_con->prepareStatement(
  18. "SELECT back_name, descs "
  19. "FROM friend_apply "
  20. "WHERE from_uid = ? AND to_uid = ? "
  21. "FOR UPDATE"
  22. ));
  23. selStmt->setInt(1, to);
  24. selStmt->setInt(2, from);
  25. std::unique_ptr<sql::ResultSet> rsSel(selStmt->executeQuery());
  26. if (rsSel->next()) {
  27. reverse_back = rsSel->getString("back_name");
  28. apply_desc = rsSel->getString("descs");
  29. }
  30. else {
  31. // 没有对应的申请记录,直接 rollback 并返回失败
  32. con->_con->rollback();
  33. return false;
  34. }
  35. }
  36. {
  37. // 2. 执行真正的更新
  38. std::unique_ptr<sql::PreparedStatement> updStmt(con->_con->prepareStatement(
  39. "UPDATE friend_apply "
  40. "SET status = 1 "
  41. "WHERE from_uid = ? AND to_uid = ?"
  42. ));
  43. updStmt->setInt(1, to);
  44. updStmt->setInt(2, from);
  45. if (updStmt->executeUpdate() != 1) {
  46. // 更新行数不对,回滚
  47. con->_con->rollback();
  48. return false;
  49. }
  50. }
  51. {
  52. // 3. 插入好友关系 - 关键改进:按照固定顺序插入避免死锁
  53. // 确定插入顺序:始终按照 uid 大小顺序
  54. int smaller_uid = std::min(from, to);
  55. int larger_uid = std::max(from, to);
  56. // 第一次插入:较小的 uid 作为 self_id
  57. std::unique_ptr<sql::PreparedStatement> pstmt(con->_con->prepareStatement(
  58. "INSERT IGNORE INTO friend(self_id, friend_id, back) "
  59. "VALUES (?, ?, ?)"
  60. ));
  61. if (from == smaller_uid) {
  62. pstmt->setInt(1, from);
  63. pstmt->setInt(2, to);
  64. pstmt->setString(3, back_name);
  65. }
  66. else {
  67. pstmt->setInt(1, to);
  68. pstmt->setInt(2, from);
  69. pstmt->setString(3, reverse_back);
  70. }
  71. int rowAffected = pstmt->executeUpdate();
  72. if (rowAffected < 0) {
  73. con->_con->rollback();
  74. return false;
  75. }
  76. // 第二次插入:较大的 uid 作为 self_id
  77. std::unique_ptr<sql::PreparedStatement> pstmt2(con->_con->prepareStatement(
  78. "INSERT IGNORE INTO friend(self_id, friend_id, back) "
  79. "VALUES (?, ?, ?)"
  80. ));
  81. if (from == larger_uid) {
  82. pstmt2->setInt(1, from);
  83. pstmt2->setInt(2, to);
  84. pstmt2->setString(3, back_name);
  85. }
  86. else {
  87. pstmt2->setInt(1, to);
  88. pstmt2->setInt(2, from);
  89. pstmt2->setString(3, reverse_back);
  90. }
  91. int rowAffected2 = pstmt2->executeUpdate();
  92. if (rowAffected2 < 0) {
  93. con->_con->rollback();
  94. return false;
  95. }
  96. }
  97. // 4. 创建 chat_thread
  98. long long threadId = 0;
  99. {
  100. std::unique_ptr<sql::PreparedStatement> threadStmt(con->_con->prepareStatement(
  101. "INSERT INTO chat_thread (type, created_at) VALUES ('private', NOW())"
  102. ));
  103. threadStmt->executeUpdate();
  104. std::unique_ptr<sql::Statement> stmt(con->_con->createStatement());
  105. std::unique_ptr<sql::ResultSet> rs(
  106. stmt->executeQuery("SELECT LAST_INSERT_ID()")
  107. );
  108. if (rs->next()) {
  109. threadId = rs->getInt64(1);
  110. }
  111. else {
  112. con->_con->rollback();
  113. return false;
  114. }
  115. }
  116. // 5. 插入 private_chat
  117. {
  118. std::unique_ptr<sql::PreparedStatement> pcStmt(con->_con->prepareStatement(
  119. "INSERT INTO private_chat(thread_id, user1_id, user2_id) VALUES (?, ?, ?)"
  120. ));
  121. pcStmt->setInt64(1, threadId);
  122. pcStmt->setInt(2, from);
  123. pcStmt->setInt(3, to);
  124. if (pcStmt->executeUpdate() < 0) {
  125. con->_con->rollback();
  126. return false;
  127. }
  128. }
  129. // 6. 插入初始消息(申请描述)
  130. if (!apply_desc.empty())
  131. {
  132. std::unique_ptr<sql::PreparedStatement> msgStmt(con->_con->prepareStatement(
  133. "INSERT INTO chat_message(thread_id, sender_id, recv_id, content, created_at, updated_at, status) "
  134. "VALUES (?, ?, ?, ?, NOW(), NOW(), ?)"
  135. ));
  136. msgStmt->setInt64(1, threadId);
  137. msgStmt->setInt(2, to);
  138. msgStmt->setInt(3, from);
  139. msgStmt->setString(4, apply_desc);
  140. msgStmt->setInt(5, 2);
  141. if (msgStmt->executeUpdate() < 0) {
  142. con->_con->rollback();
  143. return false;
  144. }
  145. std::unique_ptr<sql::Statement> stmt(con->_con->createStatement());
  146. std::unique_ptr<sql::ResultSet> rs(
  147. stmt->executeQuery("SELECT LAST_INSERT_ID()")
  148. );
  149. if (rs->next()) {
  150. auto messageId = rs->getInt64(1);
  151. auto tx_data = std::make_shared<AddFriendMsg>();
  152. tx_data->set_sender_id(to);
  153. tx_data->set_msg_id(messageId);
  154. tx_data->set_msgcontent(apply_desc);
  155. tx_data->set_thread_id(threadId);
  156. tx_data->set_unique_id("");
  157. tx_data->set_status(2);
  158. std::cout << "addfriend insert message success" << std::endl;
  159. chat_datas.push_back(tx_data);
  160. }
  161. else {
  162. con->_con->rollback();
  163. return false;
  164. }
  165. }
  166. // 7. 插入成为好友的消息
  167. {
  168. std::unique_ptr<sql::PreparedStatement> msgStmt(con->_con->prepareStatement(
  169. "INSERT INTO chat_message(thread_id, sender_id, recv_id, content, created_at, updated_at, status) "
  170. "VALUES (?, ?, ?, ?, NOW(), NOW(), ?)"
  171. ));
  172. msgStmt->setInt64(1, threadId);
  173. msgStmt->setInt(2, from);
  174. msgStmt->setInt(3, to);
  175. msgStmt->setString(4, "We are friends now!");
  176. msgStmt->setInt(5, 2);
  177. if (msgStmt->executeUpdate() < 0) {
  178. con->_con->rollback();
  179. return false;
  180. }
  181. std::unique_ptr<sql::Statement> stmt(con->_con->createStatement());
  182. std::unique_ptr<sql::ResultSet> rs(
  183. stmt->executeQuery("SELECT LAST_INSERT_ID()")
  184. );
  185. if (rs->next()) {
  186. auto messageId = rs->getInt64(1);
  187. auto tx_data = std::make_shared<AddFriendMsg>();
  188. tx_data->set_sender_id(from);
  189. tx_data->set_msg_id(messageId);
  190. tx_data->set_msgcontent("We are friends now!");
  191. tx_data->set_thread_id(threadId);
  192. tx_data->set_unique_id("");
  193. tx_data->set_status(2);
  194. chat_datas.push_back(tx_data);
  195. }
  196. else {
  197. con->_con->rollback();
  198. return false;
  199. }
  200. }
  201. // 提交事务
  202. con->_con->commit();
  203. std::cout << "addfriend insert friends success" << std::endl;
  204. return true;
  205. }
  206. catch (sql::SQLException& e) {
  207. // 如果发生错误,回滚事务
  208. if (con) {
  209. con->_con->rollback();
  210. }
  211. std::cerr << "SQLException: " << e.what();
  212. std::cerr << " (MySQL error code: " << e.getErrorCode();
  213. std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;
  214. // 如果是死锁错误(1213),可以考虑重试
  215. if (e.getErrorCode() == 1213) {
  216. std::cerr << "Deadlock detected, consider retry" << std::endl;
  217. }
  218. return false;
  219. }
  220. return true;
  221. }
热门评论

热门文章

  1. vscode搭建windows C++开发环境

    喜欢(596) 浏览(100577)
  2. 使用hexo搭建个人博客

    喜欢(533) 浏览(14372)
  3. Linux环境搭建和编码

    喜欢(594) 浏览(15945)
  4. MarkDown在线编辑器

    喜欢(514) 浏览(16295)
  5. 聊天项目(28) 分布式服务通知好友申请

    喜欢(507) 浏览(7367)

最新评论

  1. 解决博客回复区被脚本注入的问题 secondtonone1:走到现在我忽然明白一个道理,无论工作也好生活也罢,最重要的是开心,即使一份安稳的工作不能给我带来事业上的积累也要合理的舍弃,所以我还是想去做喜欢的方向。
  2. 处理网络粘包问题 zyouth: //消息的长度小于头部规定的长度,说明数据未收全,则先将部分消息放到接收节点里 if (bytes_transferred < data_len) { memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred); _recv_msg_node->_cur_len += bytes_transferred; ::memset(_data, 0, MAX_LENGTH); _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self)); //头部处理完成 _b_head_parse = true; return; } 把_b_head_parse = true;放在_socket.async_read_some前面是不是更好
  3. C++ 线程池原理和实现 mzx2023:两种方法解决,一种是改排序算法,就是当线程耗尽的时候,使用普通递归,另一种是当在线程池commit的时候,判断线程是否耗尽,耗尽的话就直接当前线程执行task
  4. 利用指针和容器实现文本查询 越今朝:应该添加一个过滤功能以解决部分单词无法被查询的问题: eg: "I am a teacher."中的teacher无法被查询,因为在示例代码中teacher.被解释为一个单词从而忽略了teacher本身。
  5. 无锁并发队列 TenThousandOne:_head  和 _tail  替换为原子变量。那里pop的逻辑,val = _data[h] 可以移到循环外面吗

个人公众号

个人微信