Skip to content

以下是整理好的 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 的设计模式优势在于:

  1. 原子性:Redis 执行 Lua 脚本是单线程原子的,中途不会被其他命令插入,彻底解决了并发下的超卖和座位状态不一致问题。
  2. 高性能:将库存检查、库存扣减、座位状态移动、幂等记录记录合并为 一次网络 IO,极大地提升了抢票场景下的并发吞吐量。
  3. 灵活性:通过 JSON 传递复杂对象,避免了 Redis Lua 参数数量过多的限制,也便于 Java 端封装逻辑。