如何锁定ChecklistItem关联对象以避免竞态条件?
解决ChecklistItem并发更新引发的竞态条件问题
问题拆解
你判断的没错,这就是竞态条件导致的问题:当多个ChecklistItem被快速保存时,每个实例的update_checklist_completion方法会各自查询数据库。最后一个执行的方法可能读取到还未被其他请求更新的旧数据,误判仍有未完成项,导致Checklist没能被标记为已完成。
实用解决方案
1. 用数据库行锁(悲观锁)彻底解决并发冲突
直接锁定对应的Checklist记录,确保同一时间只有一个请求能修改它的状态。修改update_checklist_completion方法:
def update_checklist_completion # 启动事务并锁定Checklist行,事务结束后才释放锁 checklist.with_lock do # 重新从数据库加载最新数据,避免依赖内存中的旧对象 checklist.reload # 基于最新数据更新完成状态 checklist.update(completed: checklist.checklist_items.unchecked.none?) end end
with_lock是Rails提供的便捷方法,会自动开启事务并给Checklist行加排他锁,其他请求必须等待当前事务完成才能操作这条记录,从根源上杜绝并发冲突。
2. 直接用数据库查询统计状态,不依赖内存对象
不要依赖siblings这类内存中的关联集合,直接通过数据库查询未完成项的数量,确保每次读取的都是最新数据:
def update_checklist_completion # 直接查询数据库中未完成的项数 unchecked_count = ChecklistItem.where(checklist_id: checklist.id, checked: false).count checklist.update(completed: unchecked_count.zero?) end
这种方法简单高效,普通场景下够用。如果是超高并发场景,可以套一层事务强化:
def update_checklist_completion Checklist.transaction do unchecked_count = ChecklistItem.where(checklist_id: checklist.id, checked: false).count checklist.update(completed: unchecked_count.zero?) end end
3. 把after_save替换为after_commit
after_save在事务提交前执行,此时当前请求的修改还未写入数据库,其他并发请求的修改也可能未落地。换成after_commit能确保当前修改持久化后,再去检查并更新状态:
# 指定仅在创建或更新操作提交后触发 after_commit :update_checklist_completion, on: [:create, :update] def update_checklist_completion unchecked_count = ChecklistItem.where(checklist_id: checklist.id, checked: false).count checklist.update(completed: unchecked_count.zero?) end
这个方法能减少大部分并发问题,但极端高并发场景下,建议配合数据库锁使用。
4. 批量操作时统一更新状态(从源头减少触发次数)
如果是批量完成多个ChecklistItem的场景,不要让每个Item都触发一次状态更新,直接在批量操作后统一更新Checklist:
# 批量更新示例 ChecklistItem.where(id: item_ids).update_all(checked: true) # 统一更新对应Checklist的完成状态 checklist = Checklist.find(checklist_id) checklist.update(completed: checklist.checklist_items.unchecked.none?)
这种方式减少了并发触发状态更新的次数,自然降低了竞态条件出现的概率。
内容的提问来源于stack exchange,提问作者DavidM
相关产品推荐
相关产品推荐

