利用RDBMS事务避免竞态条件:分布式系统计数一致性咨询
问题分析与解决方案
一、当前场景是否会产生数据不一致?
会产生数据不一致,最终大概率得到id=2的val为1,而非预期的2。
原因在于当前的「读-改-写」逻辑,即使包裹在事务中,默认的数据库事务隔离级别(如Read Committed)无法阻止并发更新丢失:
- 实例1处理Event-1,10:00:00启动事务,读取id=2无记录,count设为0,加1后变为1,未提交;
- 实例2处理Event-2,10:00:00:50启动事务,同样读取id=2无记录,count设为0,加1后变为1;
- 实例2耗时2秒,先在10:00:02:50提交事务,将id=2的val更新为1;
- 实例1在10:00:03提交事务,再次将id=2的val更新为1,覆盖实例2的结果,最终val为1而非2。
事务仅保证单个操作的原子性,但无法解决跨事务的「读-改-写」竞态问题。
二、除事务外的竞态条件处理方案
1. 数据库悲观锁(行级锁)
读取数据时显式加行级锁,确保同一时间只有一个事务能修改该行:
START TRANSACTION; SELECT val FROM table WHERE id = ? FOR UPDATE; -- 加排他锁,其他事务需等待锁释放 -- 无记录则count=0,count+1 INSERT INTO table (id, val) VALUES (?, 1) ON DUPLICATE KEY UPDATE val = val + 1; COMMIT;
SELECT ... FOR UPDATE会锁定目标行,直到当前事务提交,避免并发读取旧值。
2. 数据库乐观锁
通过版本号校验实现,不主动加锁,更新时验证数据是否被修改:
- 给表新增
version字段,初始值为0; - 更新逻辑:
-- 先读取当前值和版本号 SELECT val, version FROM table WHERE id = ?; -- 仅当版本号未变化时执行更新 UPDATE table SET val = ?, version = version + 1 WHERE id = ? AND version = ?;
若更新影响行数为0,说明存在并发修改,需重试操作。
3. 原子化SQL操作
将「读-改-写」合并为单个原子SQL语句,利用数据库原子性保证操作唯一:
- MySQL:
INSERT INTO table (id, val) VALUES (?, 1) ON DUPLICATE KEY UPDATE val = val + 1;
- PostgreSQL:
INSERT INTO table (id, val) VALUES (?, 1) ON CONFLICT (id) DO UPDATE SET val = table.val + 1;
无需额外事务包裹,数据库直接保证操作原子性,从根源避免竞态。
4. 分布式锁
跨节点部署场景下,用分布式锁(如Redis锁)控制同一id的并发处理:
- 处理前尝试获取锁:
SET lock:id:2 "locked" NX EX 5(Redis命令,NX表示仅锁不存在时设置,EX设超时时间); - 成功获取锁后执行更新,完成后释放锁;
- 未获取锁则等待或重试,确保同一时间仅一个实例处理该id事件。
需注意处理锁超时、续约问题,避免死锁或锁失效。
内容的提问来源于stack exchange,提问作者Siddhartha Sadhukhan
相关产品推荐
相关产品推荐

