问题:
在原有的无状态化架构下,如果候选人回答完问题,系统会同时触发两条线:
-
前台线程:调用大模型生成回复,并将聊天记录(Memory)追加后写入 Redis。
-
后台协程 :执行耗时约 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机制保证离开时交还控制权)pythontry: # 请求这把特定的锁 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}")
-