引入 Redis 分布式锁解决并发脏写 (Dirty Write)-AI模拟面试的构建rag部分

问题:

在原有的无状态化架构下,如果候选人回答完问题,系统会同时触发两条线:

  1. 前台线程:调用大模型生成回复,并将聊天记录(Memory)追加后写入 Redis。

  2. 后台协程 :执行耗时约 2 秒的向量库预检索(Prefetch),将检索到的参考资料追加后写入 Redis。 由于两条线几乎同时拉取了同一份旧的考卷(session_data),跑得慢的后台任务在写入时,会把前台刚刚保存好的最新聊天记录强行覆盖(抹除),导致严重的数据丢失

解决办法:

  • 打造锁工厂(基础设施):

    • redis_utils.py 中,基于 @asynccontextmanager 和 Redis 的原生单线程互斥能力,手写了分布式锁生成器 acquire_session_lock

    • 防死锁设计(Lease Time): 设置了 timeout=10 的租约时间,即使拥有锁的进程意外宕机断电,10秒后锁也会自动强制释放,防止考场永久卡死。

    • 快速失败设计(Fail-fast): 设置了 blocking_timeout=5.0,如果排队等锁超过 5 秒直接抛出异常,拒绝让服务器资源陷入无意义的死等。

      python 复制代码
      @asynccontextmanager
      async def acquire_session_lock(session_id: str):
          """
          获取 Redis 分布式锁,防止并发读写导致数据覆盖(脏写)
          """
          lock_key = f"lock:session:{session_id}"
      
          # timeout=10: 锁的绝对过期时间(万一服务器在这个瞬间断电,10秒后锁也会自动解开,防止死锁)
          lock = redis_client.lock(lock_key, timeout=10)
      
          # blocking_timeout=5.0: 如果别人正在写,我最多在门口等 5 秒
          acquired = await lock.acquire(blocking_timeout=5.0)
      
          if not acquired:
              raise Exception("获取会话锁超时,系统极其繁忙")
      
          try:
              # 拿到锁了,放行执行内部业务逻辑
              yield lock
          finally:
              try:
                  # 业务执行完,或者代码报错了,无论如何都要把锁解开,让下一个人进来
                  if await lock.owned():  # 确保当前还是锁的主人
                      await lock.release()
              except Exception as e:
                  print(f"⚠️ 释放 Redis 锁异常: {e}")
  • 重构预检索签名(根除脏数据):

    • 严禁在后台任务中传递可能已经过期的 session_data 字典拷贝。

    • pre_retrieve_knowledge 的入参改为只接收 session_id,强制函数在内部自行拉取最新状态。

  • 实现 CAS 思想的加锁闭环(业务应用):

    • 在后台完成耗时的向量库检索后,不直接写入,而是进入严格的**"加锁临界区"**流程: 👉 抢锁 (async with acquire_session_lock) 👉 强制重读 (await get_session 拉取这 2 秒内可能被前台修改过的最新卷子) 👉 安全合入 (将预检索资料拼接到最新卷子上) 👉 覆盖保存 (await save_session) 👉 自动释放锁 (finally 机制保证离开时交还控制权)

      python 复制代码
              try:
                  # 请求这把特定的锁
                  async with acquire_session_lock(session_id):
                      # 重新去 Redis 拿最新的会话状态(避开了这2秒内前端可能的修改)
                      latest_session = await get_session(session_id)
                      if latest_session:
                          # 只修改预检索参考字段
                          latest_session["prefetch_reference"] = combined_docs
                          # 安全覆写回 Redis
                          await save_session(session_id, latest_session)
                          print("✅ 预检索结果已安全上锁并写入 Redis!")
                      else:
                          print("⚠️ 写入预检索结果失败:未在 Redis 中找到该会话(可能已交卷)")
              except Exception as e:
                  print(f"⚠️ 保存预检索数据时发生锁异常: {e}")
相关推荐
沸点小助手3 小时前
「国产龙虾谁能打过OpenClaw & 你敢让微信龙虾碰代码吗」沸点获奖名单公示|本周互动话题上新🎊
前端·后端·面试
studyForMokey3 小时前
【Android面试】RecylerView专题
android·spring·面试
wunaiqiezixin3 小时前
MyString类的常见面试问题
c++·面试
lizhongxuan3 小时前
LLM Wiki:让大模型替你打理知识库的完整指南
前端·后端·面试
刘~浪地球4 小时前
Redis 从入门到精通(九):事务详解
数据库·redis·缓存
__土块__4 小时前
一次电商秒杀系统架构评审:从本地锁到分布式锁的演进与取舍
java·redis·高并发·分布式锁·redisson·架构设计·秒杀系统
白眼黑刺猬4 小时前
实时库存预警: 如何实现秒级更新且保证在高并发下不出现“超卖”显示错误?
大数据·面试·职场和发展
蒸蒸yyyyzwd4 小时前
检索系统学习笔记
分布式·学习
明灯伴古佛5 小时前
面试:为什么synchronized是一个悲观锁
java·面试·职场和发展