以下是整理好的 Markdown 格式文档。根据内容分析,这段逻辑属于交易执行阶段而非查询详情阶段,因此标题定为 “高并发抢票核心:Redis Lua 原子化库存扣减方案解析” 更为贴切。
高并发抢票核心:Redis Lua 原子化库存扣减方案解析
这是一个非常典型的 “Java 构造复杂参数 -> Lua 脚本原子执行” 的高并发防超卖场景。 该函数的目的是在 Java 层准备所有必要的数据,一次性传给 Redis (Lua 脚本),在脚本内部进行原子的 “库存扣减” 和 “座位锁定”,从而避免分布式锁带来的性能损耗和网络 IO 开销。
以下是参数构造、Redis 数据结构及 Lua 脚本处理的详细解析:
1. KEYS 参数映射解析
Java 代码中的 List<String> keys 会被传递给 Redis eval 命令的 KEYS 数组。
| Lua 索引 | Java 来源值 (逻辑推断) | 含义与作用 |
|---|---|---|
| KEYS[1] | "1" 或 "2" | 策略标识。"1" = 选座购买"2" = 缺省选座 (自动配座/购买无座票) |
| KEYS[2] | ...PROGRAM_SEAT_NO_SOLD... | 未售座位 Hash 前缀。用于定位存放未卖出座位的 Redis Hash Key。 |
| KEYS[3] | ...PROGRAM_SEAT_LOCK... | 锁定座位 Hash 前缀。用于定位存放已锁定座位的 Redis Hash Key。 |
| KEYS[4] | programId | 节目 ID。 |
| KEYS[5] | ...PROGRAM_RECORD | 交易记录 Hash Key。用于记录本次操作,实现幂等性,防止重复提交。 |
| KEYS[6] | REDUCE-uid-userId | 本次交易唯一标识 (幂等 Key)。作为 Field 存入 KEYS[5] 对应的 Hash 中。 |
| KEYS[7] | REDUCE | 操作类型。作为 Value 存入交易记录中。 |
2. ARGV 参数数据解析
Java 代码中的 String[] data 会被传递给 Redis eval 命令的 ARGV 数组。这里采用了 “通过 JSON 传递复杂结构体” 的设计模式。
| Lua 索引 | Java 数据结构 | JSON 内容与用途 |
|---|---|---|
| ARGV[1] | jsonArray | 库存扣减信息。包含: 1. programTicketRemainNumberHashKey: 票档库存的 Redis Hash Key。2. ticketCount: 要扣减的数量。3. seatNoSoldHashKey: (仅策略2时使用) 未售座位 Hash Key。 |
| ARGV[2] | addSeatDatajsonArray | 座位处理信息 (仅策略1选座购买时有值)。 包含 seatNoSoldHashKey 和用户选的具体 seatDataList。用于脚本精确查找这些座位并移动到锁定池。 |
| ARGV[3] | ticketUserIdList | 购票人 ID 列表。 可能用于记录日志、校验购买数量限制或绑定实名信息。 |
3. Redis 核心数据结构设计
根据代码中的 RedisKeyManage 和操作逻辑,核心数据结构为 Hash (哈希表)。
3.1 票档库存数据
- Redis Key:
program_ticket_remain_number_hash:{programId}:{ticketCategoryId}(推测) - 结构:
Hash - Field: 具体票档 ID (或 TicketCategoryId)
- Value: 剩余票数 (Integer)
- 作用: 控制总库存,防止超卖。
3.2 座位数据 (未售/锁定)
- Redis Key:
- 未售:
program_seat_no_sold:{programId}:{ticketCategoryId} - 锁定:
program_seat_lock:{programId}:{ticketCategoryId}
- 未售:
- 结构:
Hash - Field: 座位 ID (String)
- Value: 座位详细信息的 JSON 字符串 (包含排、列、价格、区域等)
- 操作逻辑: 抢票成功 = 原子性地执行
HDEL(从未售移除) +HSET(加入锁定)。
4. Lua 脚本逻辑解析 (伪代码)
Lua 脚本利用 cjson 库解析参数,并保证整个流程的原子性。
lua
-- 1. 获取输入参数
local strategy = KEYS[1]
local recordHashKey = KEYS[5]
local distinctKey = KEYS[6]
-- 2. 幂等性校验 (防止重复提交)
-- 检查本次交易ID是否已存在
if redis.call('HEXISTS', recordHashKey, distinctKey) == 1 then
return cjson.encode({code = 500, msg = "重复请求"})
end
-- 3. 解析 JSON 参数 (ARGV[1] 包含库存信息)
local ticketStockInfo = cjson.decode(ARGV[1])
---------------------------------------------------
-- 分支 A: 选座购买 (strategy == "1")
---------------------------------------------------
if strategy == "1" then
-- 解析具体的座位列表 (ARGV[2])
local seatTargetInfo = cjson.decode(ARGV[2])
for i, item in ipairs(seatTargetInfo) do
local noSoldKey = item.seatNoSoldHashKey
local seatList = cjson.decode(item.seatDataList)
-- 检查用户选的这些座位,是否真的还在"未售"池子里
for j, seat in ipairs(seatList) do
local seatId = tostring(seat.id)
local seatJson = redis.call('HGET', noSoldKey, seatId)
-- 如果获取不到,说明座位刚才被别人抢了
if not seatJson then
return cjson.encode({code = 1001, msg = "座位已被抢占"})
end
-- 执行移动:从未售删除,添加到锁定
redis.call('HDEL', noSoldKey, seatId)
redis.call('HSET', KEYS[3] .. ":" .. item.ticketCategoryId, seatId, seatJson)
end
end
end
---------------------------------------------------
-- 分支 B: 自动配座/缺省选座 (strategy == "2")
---------------------------------------------------
if strategy == "2" then
-- 简易逻辑:遍历库存信息,去 noSoldHashKey 里随机找 N 个座位
for i, item in ipairs(ticketStockInfo) do
local count = tonumber(item.ticketCount)
local noSoldKey = item.seatNoSoldHashKey
-- 获取 Hash 中所有的 key (实际生产中,大Key直接 HKEYS 可能会卡顿,通常维护一个 List 做空闲队列)
local availableSeatIds = redis.call('HKEYS', noSoldKey)
if #availableSeatIds < count then
return cjson.encode({code = 1002, msg = "库存不足"})
end
-- 循环前 count 个座进行锁定操作 (逻辑同上:HDEL -> HSET)
-- ...
end
end
-- 4. 扣减库存 (通用逻辑)
for i, item in ipairs(ticketStockInfo) do
local stockHashKey = item.programTicketRemainNumberHashKey
local count = tonumber(item.ticketCount)
-- 对应 Hash 里的字段减去数量
-- HINCRBY 支持负数,即扣减
local currentStock = redis.call('HINCRBY', stockHashKey, tostring(item.ticketCategoryId), -count)
-- 扣减后检查是否小于0(超卖校验)
if currentStock < 0 then
-- 回滚逻辑(可选,或者直接报错利用Redis回滚特性)
return cjson.encode({code = 1003, msg = "余票不足"})
end
end
-- 5. 记录本次操作并返回成功
-- 标记该请求已处理
redis.call('HSET', recordHashKey, distinctKey, "1")
return cjson.encode({code = 0, msg = "成功", purchaseSeatList = ...})5. 总结
这种 Key List + JSON Array in ARGV 的设计模式优势在于:
- 原子性:Redis 执行 Lua 脚本是单线程原子的,中途不会被其他命令插入,彻底解决了并发下的超卖和座位状态不一致问题。
- 高性能:将库存检查、库存扣减、座位状态移动、幂等记录记录合并为 一次网络 IO,极大地提升了抢票场景下的并发吞吐量。
- 灵活性:通过 JSON 传递复杂对象,避免了 Redis Lua 参数数量过多的限制,也便于 Java 端封装逻辑。