该存储过程实现的自增操作是否存在竞态条件风险?
问题
我编写了如下存储过程:
CREATE PROCEDURE incrementSomeNum() BEGIN START TRANSACTION; # Not sure if this line is necessary. DECLARE a INT; SELECT someValue INTO a FROM someTable WHERE id = 1; UPDATE someTable SET someValue = a + 1 WHERE id = 1; COMMIT; # And this END;
客户端通过线程池获取连接,多线程执行调用该存储过程的操作:
connection = connectionPool.getConnection() connection.query("CALL incrementSomeNum();"); connection.close();
请问该操作是否能避免竞态条件?
回答
好问题!你的这个存储过程不能避免竞态条件,核心原因在于你把「读取值」和「更新值」拆成了两个独立的操作,哪怕包在事务里也没用,具体解释和修复方案如下:
为什么会出现竞态?
虽然你用START TRANSACTION和COMMIT把操作包裹成了一个事务,但事务只保证ACID特性(原子性、一致性、隔离性、持久性),没法直接解决并发下的操作原子性问题。举个具体的场景:
- 线程A执行
SELECT someValue INTO a,读取到someValue = 5 - 线程B在A还没执行
UPDATE的时候,也执行了同一个SELECT,同样读取到someValue = 5 - 线程A把值更新为
5+1=6并提交事务 - 线程B接着也把值更新为
5+1=6并提交事务
最终someValue的结果是6,但实际上两个线程执行后预期应该是7——这就是典型的竞态条件导致的更新丢失。
另外你疑惑的START TRANSACTION是否必要:在这个场景里,加不加它其实都解决不了竞态,但如果不加的话,SELECT和UPDATE会变成两个独立的自动提交事务,问题只会更严重。
两种修复方案
方案一:改用原子化的UPDATE语句(推荐)
完全不需要先读取值,直接让MySQL在一条语句里完成「读取+更新」的操作,这种方式是原子性的,MySQL会自动给目标行加排他锁,确保并发调用时排队执行:
CREATE PROCEDURE incrementSomeNum() BEGIN UPDATE someTable SET someValue = someValue + 1 WHERE id = 1; END;
这个方案最简单高效,没有多余的变量声明和事务操作,完全规避了竞态风险。
方案二:给SELECT加行锁(适用于复杂逻辑场景)
如果你的业务逻辑必须先读取值再做处理(比如要基于值做更多判断),那可以在SELECT时加上FOR UPDATE关键字,给目标行加排他锁,阻止其他线程读取该行直到当前事务提交:
CREATE PROCEDURE incrementSomeNum() BEGIN START TRANSACTION; DECLARE a INT; -- 加FOR UPDATE锁,其他线程必须等待当前事务提交才能读取该行 SELECT someValue INTO a FROM someTable WHERE id = 1 FOR UPDATE; UPDATE someTable SET someValue = a + 1 WHERE id = 1; COMMIT; END;
这样就能保证同一时间只有一个线程能读取到someValue的值,避免了并发读取相同值的问题。
最后补充:你的客户端连接池调用逻辑是没问题的,竞态问题的根源完全在存储过程的内部逻辑,和连接池的使用无关。
内容的提问来源于stack exchange,提问作者RnMss
相关产品推荐
相关产品推荐

