项目 / 城觅

城觅 · 生活服务平台

高并发交易系统之上,长出一个能动手的 AI 客服

2026
个人项目 SpringBoot
1849 Agent 模块代码行
6 Agent 工具
0 新增数据库表
4 秒杀链路迭代

这个项目做了什么

城觅是一个本地生活服务平台:商户、优惠券、秒杀、订单。业务本身不新鲜, 我想练的是秒杀那条链路上的并发问题,以及后来加上去的智能客服模块。

这条链路我改了四轮,每一轮都因为上一轮压不住才存在:

  1. 阻塞队列 —— 单机内存队列削峰,服务重启就丢,多实例也不共享。
  2. Lua 原子扣减 —— 把「查库存 → 判断 → 扣减」压成一段 Lua 脚本,在 Redis 里原子执行,超卖没了。
  3. Redisson 分布式锁 —— 多实例下同一用户重复下单互相踩,用锁把用户维度串起来。
  4. 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 的模型调用没有做降级兜底:模型服务不可用时,客服模块整体不可用而不是退化成人工话术。