引入 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}")
相关推荐
M ? A11 分钟前
Vue 转 React:toRaw(),VuReact 怎么处理?
前端·javascript·vue.js·经验分享·react.js·面试·vureact
喜欢流萤吖~1 小时前
分布式事务:微服务的数据一致性之困
分布式·微服务·架构
AI人工智能+电脑小能手10 小时前
【大白话说Java面试题】【Java基础篇】第15题:JDK1.7中HashMap扩容为什么会发生死循环?如何解决
java·开发语言·数据结构·后端·面试·哈希算法
Moment12 小时前
2026 年,AI 全栈时代到了,前端简历别再只写前端技术了 🫠🫠🫠
前端·后端·面试
白晨并不是很能熬夜13 小时前
【PRC】第 2 篇:Netty 通信层 — NIO 模型 + 自定义协议 + 心跳
java·开发语言·后端·面试·rpc·php·nio
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.14 小时前
《redis-cluster 集群部署完全手册(含扩容+缩容)》
数据库·redis·缓存
0xDevNull15 小时前
Java项目中Redis热点Key自动检测方案详细教程
java·spring boot·redis
M ? A15 小时前
Vue 的 scoped 样式穿透 React 不支持?用 VuReact 编译就行
前端·javascript·vue.js·react.js·面试·开源·vureact
极客沐森15 小时前
如何取消大批量的超时订单,关于超时架构的探讨
面试·架构
豹哥学前端16 小时前
10分钟彻底搞懂 window 对象、全局环境与 JS 引擎
前端·面试