关于Rails文档中悲观锁示例的竞态条件疑问
关于Rails悲观锁与竞态条件的问题
你的观察完全正确——如果在with_lock块外执行account = Account.first,确实会存在你描述的竞态条件,导致数据更新丢失。
为什么会出现问题?
当两个并发请求都在块外执行Account.first时,它们都会从数据库读取到初始的balance = 200并保存在内存中。第一个请求进入with_lock块后,会锁定这条记录并将余额修改为100,完成事务后释放锁。但第二个请求此时内存里的余额还是旧的200,它进入块后会基于这个旧值计算,最终将余额改成100而非正确的0,相当于丢失了一次扣减操作。
正确的解决方式
要彻底避免这种竞态,必须把查询操作和修改操作放在同一个锁定的事务中,确保查询时就锁定记录,不让其他事务读取到未提交的旧数据。有两种常见的正确写法:
写法1:使用带锁定的查询
直接在查询时就加上悲观锁,这样其他事务无法读取这条记录,直到当前事务完成:
account = Account.lock.first account.with_lock do account.balance -= 100 account.save! end
或者更简洁的事务内写法:
Account.transaction do account = Account.lock.first account.balance -= 100 account.save! end
写法2:将查询放入with_lock块内
如果偏好with_lock的块语法,也可以把查询逻辑放进块中(注:此写法实际是表级锁,行级锁更推荐上面的方式):
Account.with_lock do account = Account.first account.balance -= 100 account.save! end
关键总结
悲观锁的核心是在读取数据时就锁定,避免其他事务读取到脏数据。如果先读取再锁定,中间的时间窗口会给并发请求留下读取旧数据的机会,从而引发竞态条件。
内容的提问来源于stack exchange,提问作者53c
相关产品推荐
相关产品推荐

