MySQL 并发请求常见问题

6 阅读4分钟

当多个请求同时访问数据库时,主要会遇到以下几类并发问题:

1. 读写冲突问题

(1) 脏读(Dirty Read)

现象:事务A读取了事务B未提交的修改,事务B后来回滚了
示例

  事务B: UPDATE accounts SET balance = 500 WHERE id = 1; (未提交) 
  事务A: SELECT balance FROM accounts WHERE id = 1; (读到500)  
  事务B: ROLLBACK; (实际余额还是原来的1000)  

(2) 不可重复读(Non-repeatable Read)

现象:同一事务内两次读取同一数据结果不同
示例

  事务A: SELECT balance FROM accounts WHERE id = 1; (返回1000)  
  事务B: UPDATE accounts SET balance = 800 WHERE id = 1; (提交)  
  事务A: SELECT balance FROM accounts WHERE id = 1; (返回800)  

(3) 幻读(Phantom Read)

现象:同一事务内同样的查询条件返回不同行数的结果
示例

  事务A: SELECT * FROM accounts WHERE balance > 500; (返回2条)  
  事务B: INSERT INTO accounts VALUES(3'C'600); (提交)  
  事务A: SELECT * FROM accounts WHERE balance > 500; (返回3条)  

2. 写写冲突问题

(1) 更新丢失(Lost Update)

现象:两个事务同时读取并修改同一数据,后者覆盖前者
示例

  事务A: 读取count=5 → 计算count+1=6 → 更新count=6  
  事务B: 读取count=5 → 计算count+1=6 → 更新count=6  
  (最终结果应为7但得到6)  

当两个进程同时读取同一个数据然后尝试修改时,会导致更新丢失(Lost Update)。

在例子中,两个进程都读取到count=5,然后各自加1设置为6,最终结果应该是7但却得到了6。

1. 使用原子更新操作

最直接和高效的方法是使用MySQL的原子更新:

UPDATE page_views SET count = count + 1 WHERE page_id = 123;

这样MySQL会在内部处理增量操作,不需要先读取再更新。

2. 使用事务和SELECT FOR UPDATE

如果必须先在应用逻辑中处理值再更新:

START TRANSACTION;
SELECT count FROM page_views WHERE page_id = 123 FOR UPDATE;
-- 在应用逻辑中处理count值
UPDATE page_views SET count = ? WHERE page_id = 123;
COMMIT;

FOR UPDATE会对选中的行加排他锁,防止其他事务同时修改。

3. 使用乐观锁

通过版本号或时间戳实现:

-- 先读取数据和版本号
SELECT count, version FROM page_views WHERE page_id = 123;

-- 更新时检查版本号是否变化
UPDATE page_views 
SET count = count + 1, version = version + 1 
WHERE page_id = 123 AND version = ?;

如果受影响行数为0,说明并发冲突,需要重试。

4. 使用应用层锁

在应用层使用分布式锁(如Redis锁)控制对特定资源的访问。

最佳实践

  1. 优先使用原子操作:像计数器这种简单场景,直接用count = count + 1最可靠高效。
  2. 减少事务持有时间:如果必须使用事务,尽量缩短事务持续时间。
  3. 合理设计重试机制:对于乐观锁方案,需要实现重试逻辑。

在你的浏览量统计场景中,第一种原子更新方案是最简单有效的解决方案。

(2) 写偏斜(Write Skew)

现象:两个事务基于相同的前提条件做出决策,但合并后结果不正确
示例

  规则:医生值班表必须至少有一人  
  事务A: 检查有2人值班 → 允许医生A请假  
  事务B: 检查有2人值班 → 允许医生B请假  
  结果:无人值班  

3. 锁相关问题

(1) 死锁(Deadlock)

现象:两个事务互相等待对方释放锁
示例

  事务A: 锁定行1 → 请求行2  
  事务B: 锁定行2 → 请求行1  

(2) 锁等待超时

现象:事务等待锁时间超过设定阈值
常见原因:长事务持有锁时间过长

(3) 锁升级

现象:行锁升级为表锁,导致并发性能下降
常见原因:无索引或索引失效导致全表扫描

4. 性能相关问题

(1) 热点行竞争

现象:大量并发请求集中访问少数数据行
典型场景:秒杀系统中的商品库存

(2) 连接数耗尽

现象:数据库连接池被占满,新请求无法获取连接

(3) 高CPU/IO负载

现象:并发请求过多导致数据库服务器资源耗尽

解决方案选择指南

 问题类型        适用解决方案                     
 脏读            提高隔离级别到READ_COMMITTED     
 不可重复读      提高隔离级别到REPEATABLE_READ    
 幻读            使用SERIALIZABLE或间隙锁         
 更新丢失        SELECT FOR UPDATE或原子操作      
 死锁            重试机制、锁超时设置             
 热点行竞争      排队机制、应用层缓存、批量处理   

理解这些并发问题有助于设计更健壮的数据库应用,实际开发中需要根据具体业务场景选择合适的解决方案。