当多个请求同时访问数据库时,主要会遇到以下几类并发问题:
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锁)控制对特定资源的访问。
最佳实践
- 优先使用原子操作:像计数器这种简单场景,直接用
count = count + 1
最可靠高效。 - 减少事务持有时间:如果必须使用事务,尽量缩短事务持续时间。
- 合理设计重试机制:对于乐观锁方案,需要实现重试逻辑。
在你的浏览量统计场景中,第一种原子更新方案是最简单有效的解决方案。
(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或原子操作 |
死锁 | 重试机制、锁超时设置 |
热点行竞争 | 排队机制、应用层缓存、批量处理 |
理解这些并发问题有助于设计更健壮的数据库应用,实际开发中需要根据具体业务场景选择合适的解决方案。