项目 / 城觅
城觅 · 生活服务平台
高并发交易系统之上,长出一个能动手的 AI 客服
1849 Agent 模块代码行
6 Agent 工具
0 新增数据库表
4 秒杀链路迭代
这个项目做了什么
城觅是一个本地生活服务平台:商户、优惠券、秒杀、订单。业务本身不新鲜, 我想练的是秒杀那条链路上的并发问题,以及后来加上去的智能客服模块。
这条链路我改了四轮,每一轮都因为上一轮压不住才存在:
- 阻塞队列 —— 单机内存队列削峰,服务重启就丢,多实例也不共享。
- Lua 原子扣减 —— 把「查库存 → 判断 → 扣减」压成一段 Lua 脚本,在 Redis 里原子执行,超卖没了。
- Redisson 分布式锁 —— 多实例下同一用户重复下单互相踩,用锁把用户维度串起来。
- Kafka 异步削峰 —— 同步落库扛不住瞬时流量,改成本地消息表 + Kafka 异步消费,并处理消费幂等。
架构怎么长出来的
STEP 1 削峰
阻塞队列扛第一波
下单请求先进内存队列,后台线程慢慢消费。问题很直接:重启即丢,多实例各排各的队。
BlockingQueue单机可丢弃
STEP 2 原子性
把判断和扣减压成一段 Lua
库存查询与扣减分成两步就会超卖。改成一段 Lua 脚本在 Redis 侧原子执行,超卖消失。
LuaRedis原子扣减
STEP 3 一致性
锁住「同一个人」
多实例部署后,同一用户并发下单会重复扣减。用 Redisson 分布式锁按用户 id 串行化,一人一单成立。
Redisson分布式锁一人一单
STEP 4 异步
落库走消息,不走同步
同步写 MySQL 是整条链路的瓶颈。改为本地消息表 + Kafka 异步消费,并对消费端做幂等。
Kafka本地消息表幂等
STEP 5 AI
让客服能动手
在既有交易系统上加 LangChain4j 智能客服:AI 只负责提议,用户二次确认后才执行,写操作复用既有消息链路。
LangChain4jFunction Calling二次确认
我解决过的工程问题
AI 直接调写操作:模型说「帮你退了」,其实没退
CRITICAL- 问题
- 客服模块最初让模型直接调工具,包括取消订单、申请退款这类写操作。
- 后果
- 模型会在用户只是抱怨时提前发起写操作,也会在参数缺失时编一个订单号。对话里说「已为你取消」,数据库里订单还在。这比答不出来严重得多。
- 方案
- 定下「AI 提议,用户确认,系统执行」三段式:模型只能生成一个待确认的意图对象,前端渲染成可点的确认卡,用户点了才走既有服务方法。
- 取舍
- 为什么不让模型自己判断该不该确认?因为「该不该动数据」是权限问题,不是语义问题,不能交给概率模型决定。
客服模块一行表都没加,却要读写交易库
IMPORTANT- 问题
- 最省事的做法是给 Agent 建几张会话表、工具日志表。但这样它就成了一个需要独立运维的子系统。
- 方案
- 约束自己不新增任何数据库表:会话上下文放 Redis 带 TTL,工具调用复用已有的服务方法与消息链路,审计落到现有日志管线。
- 验证
- 最终交付时
git diff —stat里 0 张新增表的 DDL;Agent 模块 1849 行代码全在既有结构内。
消息消费重试,同一笔订单扣了两次库存
IMPORTANT- 问题
- Kafka 消费端异常会重试,但下单消息的重试不是「重放」,是再执行一遍业务。
- 后果
- 用户看到一次下单,库存掉了 2。这类问题只在流量起来时才暴露,本地单线程测永远测不到。
- 方案
- 消费端按业务幂等键(用户 + 优惠券 + 时间片)落库前查重,重复消息直接 ack 丢弃,而不是依赖 at-least-once 语义蒙混过关。
一段关键实现
秒杀扣减的 Lua 脚本:把「查库存、判断是否已售罄、扣减」压进一次 Redis 原子执行。
-- seckill.lua —— 单脚本内完成判断与扣减,杜绝「查完再扣」之间的窗口
local stockKey = KEYS[1]
local orderKey = KEYS[2]
local userId = ARGV[1]
-- 同一用户重复请求直接短路,不消耗库存
if redis.call('SISMEMBER', orderKey, userId) == 1 then
return 2
end
local stock = tonumber(redis.call('GET', stockKey) or '-1')
if stock <= 0 then
return 1 -- 已售罄
end
redis.call('DECR', stockKey)
redis.call('SADD', orderKey, userId)
return 0 -- 扣减成功
已知限制
- Agent 模块的工具是硬编码注册的,没有做成插件化,加第 7 个工具要改构建期代码。
- 会话上下文只存 Redis 且带 TTL,服务重启后超过 TTL 的历史对话不可恢复。
- 秒杀链路的压测是在单机 Docker 环境做的,没有真正验证多机房部署下的时钟与锁表现。
- LangChain4j 的模型调用没有做降级兜底:模型服务不可用时,客服模块整体不可用而不是退化成人工话术。
参考来源
- Redis 官方文档 · Lua 脚本与 EVALhttps://redis.io/docs/latest/develop/interact/programmability/eval-intro/官方
- Redisson 官方文档 · 分布式锁https://redisson.pro/docs/data-and-services/locks-and-synchronizers/官方
- Apache Kafka 官方文档 · 消费者与投递语义https://kafka.apache.org/documentation/#semantics官方
- LangChain4j 官方文档 · Tools (Function Calling)https://docs.langchain4j.dev/tutorials/tools/官方