LUA+Reids实现库存秒杀预扣减 记录流水 以及自己的思考

目录

lua脚本

记录流水

记录流水的作用

流水什么时候删除


我们在做库存扣减的时候,显示基于Lua脚本和Redis实现的预扣减

这样可以在秒杀扣减的时候保证操作的原子性和高效性

lua脚本

// ... 已有代码 ...

    @Override
    public InventoryResponse decrease(InventoryRequest request) {
        // 创建库存响应对象
        InventoryResponse inventoryResponse = new InventoryResponse();
        // 定义用于减少库存的Lua脚本
        String luaScript = """
                -- 检查哈希表 KEYS[2] 中是否已存在 ARGV[2] 对应的字段
                -- 如果存在,说明该操作已执行过,返回错误信息
                if redis.call('hexists', KEYS[2], ARGV[2]) == 1 then
                    return redis.error_reply('OPERATION_ALREADY_EXECUTED')
                end
                                
                -- 从 Redis 中获取 KEYS[1] 对应的值,即当前库存
                local current = redis.call('get', KEYS[1])
                -- 如果返回值为 false,说明键不存在,返回错误信息
                if current == false then
                    return redis.error_reply('KEY_NOT_FOUND')
                end
                -- 尝试将当前库存值转换为数字,如果转换失败,返回错误信息
                if tonumber(current) == nil then
                    return redis.error_reply('current value is not a number')
                end
                -- 如果当前库存为 0,返回库存为零的错误信息
                if tonumber(current) == 0 then
                    return redis.error_reply('INVENTORY_IS_ZERO')
                end
                -- 如果当前库存小于要减少的数量 ARGV[1],返回库存不足的错误信息
                if tonumber(current) < tonumber(ARGV[1]) then
                    return redis.error_reply('INVENTORY_NOT_ENOUGH')
                end
                                
                -- 计算减少库存后的新库存值
                local new = tonumber(current) - tonumber(ARGV[1])
                -- 将新的库存值存储回 Redis 中
                redis.call('set', KEYS[1], tostring(new))
                                
                -- 获取 Redis 服务器的当前时间(秒和微秒)
                local time = redis.call("time")
                -- 将获取到的时间转换为毫秒级时间戳
                local currentTimeMillis = (time[1] * 1000) + math.floor(time[2] / 1000)
                                
                -- 使用哈希结构存储库存减少操作的日志
                -- 在 KEYS[2] 对应的哈希表中,以 ARGV[2] 为字段名,存储操作日志的 JSON 字符串
                redis.call('hset', KEYS[2], ARGV[2], cjson.encode({
                    action = "decrease",  -- 操作类型为减少库存
                    from = current,       -- 操作前的库存值
                    to = new,             -- 操作后的库存值
                    change = ARGV[1],     -- 减少的库存数量
                    by = ARGV[2],         -- 操作标识
                    timestamp = currentTimeMillis  -- 操作的时间戳
                }))
                                
                -- 返回更新后的库存值
                return new
                """;

        try {
            // ... 已有代码 ...

首先是合法性校验

然后是原子性扣减

最后记录一条库存扣减流水


记录流水

在 lua 脚本中 我们不仅是做了库存的扣减

还在 redis 中用 hash 存储了一条流水

key:买家id + token + 扣减数量

value:本次扣减的变化的库存数,变化前的库存数,变化后的库存数 变化操作的ID 变化的时间戳

记录流水的作用

  1. 幂等

执行lua脚本的时候 查看是否有当前流水

如果有 说明是一个重复请求 直接幂等掉

  1. 对账

很重要

redis库存扣减后 需要持久化到数据库 这边使用的是mq异步处理

如何保证一致性呢

我们用redis里的hash流水和数据库流水进行对账

如果不一致的话

很有可能是mq丢消息了

就要会进行排查了

流水什么时候删除

数据库 与 缓存对账后删除

商品下架后24小时后删除

<think>嗯,用户现在想设计一个高并发场景下的拼团库存扣减方案,特别关注分布式锁和数据一致性。之前我已经给过一个技术实现方案,里面提到了使用Redis分布式锁和数据库事务。现在用户可能希望更深入地了解如何在高并发下确保库存扣减的正确性,同时保持系统的高可用性。 首先,我需要回顾之前的回答,确保这次的内容不会重复,但又能补充更多细节。之前提到过Redis分布式锁和库存扣减的逻辑,但用户现在可能想知道更具体的实现步骤,或者有没有更好的替代方案,比如乐观锁或者缓存扣减。 然后,用户提到的引用[1]和引用[2]需要仔细分析。引用[1]讲的是CAP定理,说明在分布式系统中需要在一致性和可用性之间权衡。而引用[2]提到了苏宁的案例,指出在高并发下缓存和数据库的数据不一致可能导致超卖。这提示我在设计库存扣减时,必须处理好缓存和数据库的同步问题,避免数据不一致。 用户的问题集中在高并发、拼团、库存扣减、分布式锁和数据一致性这几个关键词上。我需要考虑以下几个点: 1. **库存扣减的原子性**:如何确保在高并发下,多个请求同时扣减库存时不会出现超卖。可能需要使用Redis的原子操作,比如DECR或者Lua脚本,或者结合数据库的乐观锁。 2. **分布式锁的应用**:虽然Redis分布式锁可以防止并发问题,但可能会有性能瓶颈。是否有更好的方式,比如分段锁或者无锁设计? 3. **缓存与数据库的一致性**:引用[2]提到缓存数据可能在同步前失效,导致不一致。如何确保缓存中的库存和数据库保持一致?是否需要异步同步,或者使用延迟双删策略? 4. **系统的可用性**:根据CAP定理,选择AP还是CP?拼团场景可能需要更高的可用性,允许短暂的不一致,但最终必须一致。比如,先扣缓存库存,再异步更新数据库,但要处理可能的失败情况。 5. **降级和容错机制**:在高并发下,系统压力大,如何设计降级策略?比如,当Redis不可用时,是否直接走数据库,虽然可能性能下降,但保证可用性。 接下来,我需要将这些点组织成一个合理的方案。可能的结构是: - 分层库存设计:缓存层做扣减,数据库层做最终扣减。 - 使用Redis的原子操作确保缓存层的原子性。 - 通过消息队列异步同步到数据库,处理可能的失败重试。 - 引入熔断降级机制,保证系统可用性。 - 最终一致性检查,比如定时任务核对缓存和数据库的数据。 还要考虑是否使用分段锁来减少争用,比如将库存分成多个键,分散并发压力。此外,是否需要使用乐观锁,比如在数据库更新时检查版本号,避免覆盖旧数据。 最后,需要验证方案的可行性,比如在高并发测试中是否能有效防止超卖,同时保持系统的响应速度。可能还需要考虑监控和报警机制,及时发现和处理异常情况。</think>### 高并发拼团库存扣减方案设计 针对高并发场景下的库存扣减问题,需结合**分布式锁**、**缓存扣减**、**异步队列**和**最终一致性**策略,在性能与数据一致性之间取得平衡。以下是分阶段技术实现方案: --- #### 一、分层库存设计(核心思路) 1. **缓存层扣减** - 使用Redis存储可售库存(如`stock:product_123`),通过原子操作实现快速扣减: ```bash # 原子扣减库存(返回扣减后的值) DECRBY stock:product_123 1 # 若需防止超扣(扣减后值>=0) EVAL "local current = redis.call('GET', KEYS[1]); if current >= ARGV[1] then return redis.call('DECRBY', KEYS[1], ARGV[1]) else return -1 end" 1 stock:product_123 1 ``` - **优势**:单节点可达10万+ QPS,避免直接访问数据库[^2]。 2. **数据库层最终扣减** - 使用MySQL记录实际库存,通过**异步队列**同步缓存扣结果: ```sql UPDATE product SET stock = stock - 1 WHERE id = 123 AND stock >= 1; ``` --- #### 二、分布式锁优化方案 1. **分段锁降低竞争** - 将库存拆分为N个分片(如`stock:product_123_segment_1`~`stock:product_123_segment_10`),分散锁粒度: ```java int segment = userId.hashCode() % 10; // 用户ID哈希取模选择分片 String lockKey = "stock_lock:product_123_segment_" + segment; ``` - **效果**:并发冲突降低90%(假设分片数为10)[^1]。 2. **锁超时与自动续期** - 结合Redisson实现锁自动续期,防止业务未完成时锁过期: ```java RLock lock = redisson.getLock("stock_lock:product_123"); try { lock.lock(30, TimeUnit.SECONDS); // 看门狗自动续期 // 执行业务逻辑 } finally { lock.unlock(); } ``` --- #### 三、数据一致性保障 1. **补偿事务设计** - 若缓存扣减成功但数据库更新失败,通过定时任务回补数据: ```java @Scheduled(fixedDelay = 60000) public void syncStockToDB() { List<ProductStockDiff> diffs = redisService.getStockDiffs(); diffs.forEach(diff -> { if (dbStockService.adjustStock(diff)) { redisService.clearStockDiff(diff); // 清理已同步记录 } }); } ``` 2. **最终一致性校验** - 引入**版本号**或**乐观锁**防止覆盖更新: ```sql UPDATE product SET stock = stock - 1, version = version + 1 WHERE id = 123 AND version = #{currentVersion}; ``` --- #### 四、降级与熔断策略 1. **限流保护** - 使用Sentinel对库存接口实施QPS限流: ```java FlowRule rule = new FlowRule(); rule.setResource("deductStock"); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setCount(1000); // 每秒最大请求数 FlowRuleManager.loadRules(Collections.singletonList(rule)); ``` 2. **熔断降级** - 当Redis响应时间超过阈值时,自动切换至本地缓存: ```java @HystrixCommand(fallbackMethod = "fallbackDeductStock") public boolean deductStock(Long productId) { // 正常业务逻辑 } ``` --- ### 典型时序图 ```plaintext 用户请求 -> 网关限流 -> Redis库存 -> 写入扣减流水 -> 返回成功 ↓ 异步队列消费 -> 数据库最终扣减 -> 删除流水记录 ↓ 定时任务补偿 <- 异常检测 ``` --- ### 关键问题解决方案对比 | 方案 | 一致性强度 | 并发能力 | 实现复杂度 | 适用场景 | |---------------------|------------|-----------|------------|------------------------| | 纯数据库行锁 | 强一致性 | 低(<1k) | 低 | 低频次普通业务 | | Redis原子操作+异步 | 最终一致 | 高(>5w) | 中 | 秒杀、拼团等高并发场景 | | 分布式锁+分段 | 强一致性 | 中(1w+) | 高 | 需强一致的中高并发场景 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员多多_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值