Django3搭配MySQL时如何防范医生接诊场景的并发操作冲突?
会。如果不做特殊的并发控制,3名医生同时点击认领按钮时完全可能出现数据异常,核心原因是常规的「先查询患者状态→判断是否为待接诊→执行更新认领」逻辑是典型的非原子"读-改-写"流程:
- 3个并发请求会同时查询到Jennifer的状态为「待接诊」,全部通过业务校验
- 后续的更新操作会依次执行,最终Jennifer的接诊医生字段会被最后一个提交的请求覆盖,甚至可能在接诊关联表写入多条重复的认领记录,完全违反"一个待接诊患者只能被一名医生认领"的业务规则。
所有方案的核心思路都是把状态校验和更新操作合并为原子操作,从数据库层面拦截重复认领,不要只靠前端或者应用层的非原子判断做控制。
优先推荐:基于条件更新的乐观锁方案
这个方案代码最简洁、性能最好,完全适配接诊认领的业务场景,不需要显式加数据库锁,依靠MySQL更新操作本身的原子性实现并发控制。
核心逻辑是直接把「患者为待接诊状态」作为update语句的查询条件,通过数据库返回的影响行数判断是否认领成功,示例代码:
from django.db import transaction from your_app.models import Patient, ConsultRecord def claim_patient(request, patient_id): current_doctor = request.user.doctor with transaction.atomic(): # 直接执行更新,where条件同时校验患者ID和待接诊状态 updated_count = Patient.objects.filter( id=patient_id, status=Patient.Status.WAITING # 待接诊状态枚举值 ).update( status=Patient.Status.CLAIMED, attending_doctor=current_doctor ) if updated_count == 0: return JsonResponse({"code": 1, "msg": "该患者已被其他医生认领,请刷新列表"}) # 认领成功后写入接诊记录等后续关联操作 ConsultRecord.objects.create( patient_id=patient_id, doctor=current_doctor ) return JsonResponse({"code": 0, "msg": "认领成功"})
MySQL执行单条update语句时会自动给匹配的行加写锁,同一时间永远只有一个请求能匹配到待接诊状态的目标患者,更新成功后其他并发请求的update语句匹配不到符合条件的行,影响行数为0,直接返回认领失败,从根本上避免重复认领。
备选方案:基于select_for_update的悲观锁方案
如果后续认领逻辑需要执行更多关联查询、多步数据校验,逻辑比较复杂,可以用Django提供的select_for_update()方法实现数据库行级排他锁,把整个认领流程包在事务里,同一时间只有一个事务能拿到目标患者行的锁,其他请求会阻塞等待锁释放,示例代码:
from django.db import transaction from your_app.models import Patient def claim_patient(request, patient_id): current_doctor = request.user.doctor try: with transaction.atomic(): # 查询时加排他行锁,匹配不到符合条件的患者直接抛DoesNotExist patient = Patient.objects.select_for_update().get( id=patient_id, status=Patient.Status.WAITING ) # 拿到锁之后再执行多步校验、更新操作 patient.status = Patient.Status.CLAIMED patient.attending_doctor = current_doctor patient.save() # 其他关联操作... return JsonResponse({"code":0, "msg":"认领成功"}) except Patient.DoesNotExist: return JsonResponse({"code":1, "msg":"该患者已被其他医生认领,请刷新列表"})
注意这个方案需要确保患者表使用InnoDB存储引擎,否则会锁全表;如果不想让请求阻塞等待锁,可以给select_for_update()传入skip_locked=True参数,拿不到锁的请求会直接返回空,不会排队,适合并发量更高的场景。
前端辅助优化(仅做体验提升,不能替代后端控制)
- 医生点击认领按钮后立刻将按钮置灰,禁止重复提交
- 待接诊列表设置合理的自动刷新间隔,及时移除已经被认领的患者
- 认领接口返回失败时给用户明确提示,引导刷新列表
注意:所有前端拦截都只是体验优化,数据正确性的保障100%依赖后端的原子化逻辑,绝对不能只靠前端状态判断做拦截。
下面这种无任何并发控制的写法是出现数据冲突的核心原因,绝对不要用:
# ❌ 错误示例,并发场景必出问题 def bad_claim_logic(request, patient_id): patient = Patient.objects.get(id=patient_id) # 多个并发请求会同时通过这个判断 if patient.status == Patient.Status.WAITING: patient.attending_doctor = request.user.doctor patient.status = Patient.Status.CLAIMED # 最后执行save的请求会覆盖之前的所有更新 patient.save() return JsonResponse({"msg":"操作完成"})
内容的提问来源于stack exchange,提问作者ivbtar

