关于redis做秒杀库存扣减的生产实践及思考

前言

        近期组员接手了一个领券的业务,涉及到了对券批次库存的扣减操作,在多次尝试优化后压测起来仍有一些性能问题,由于接近deadline,于是自己也尝试上手优化了一下。让我对日常在论坛看到的redis秒杀库存的实现有了进一步的认知。

领券逻辑

        首先,先简单介绍下领券的逻辑,当然中间有一些业务定制化的细节我做了简化。

         这时候,为了保证券库存不会超领,我们接手的第一版里,逻辑是直接对券库存加了一把redis锁,这样确实是保证库存不会超领,但是由于线上更多的场景是多用户并发对一种券的领取,在压测这种场景的时候,tps只达到20,90线达到2s以上,显然不满足实际需求。

(第一版中直接锁库存ID,导致并发领同一种券时变成串行请求) 

Redis锁优化思路

        我们在网上经常看到的利用redis去做秒杀业务时,是利用redis的单线程特性,将库存存储在redis中,然后用incr方法,保证库存在并发扣减情况下,不会超扣。

        但是这里涉及到几个问题:

  1. 什么时候将库存存入redis,存永久还是定义一个有效期?
  2. 什么时候让库存数据实际落库,扣减到0的时候,还是定时同步? 如果业务需要知道库存要不足了,去做补货操作怎么办。
  3. 库存如果永久存在redis中,然后如果是扣减到0的时候才进行删除,再同步数据库,那么如果一直没有扣减完怎么办,是不是会一直占用着redis,而且管理台看到的库存也一直没有被扣减?

        这些问题,相关博客里并没有明说,我们只知道我们要从redis扣,但不知道什么放redis,什么时候同步到数据库,这些都是不清楚的,而且相关文章都有点千篇一律,那么只能结合后想一套自己的方案了。

        在查阅相关资料后,我自己采用的方案是,在查询的时候将库存存放到redis,设定一定有效期,因为我们的redis平时也需要做各种缓存,所以如果不设置有效期的话,就会留下一些脏数据。

        这里我们先给出一个lua脚本。

 if (redis.call("exists", KEYS[1])) == 1 then
    redis.call("expire", KEYS[1], ARGV[2])
    return redis.call("incr", KEYS[1], ARGV[1])
 else return nil end

        解释一下这个脚本的意思就是,判断传入key是否存在,如果存在执行incr,不存在返回null。

        为什么要这么做呢?因为我们的redis库存是有有效期的,我们每次去扣减库存的时候,就需要去判断库存数据是否存在,如果存在的话才能够去做incr操作。

        那么为什么不放在Java里进行呢?因为涉及到原子性操作。Java中的执行代码大概如下

if (redisUtil.exist(key)) { //1. 执行时判断到key存在
    // 2.但此时去incr时可能已经不存在
    redisUtil.incr(key, 1); // 3.返回库存剩余值为 1
    redisUtil.expire(key, 300);
}

        那么就可能出现库存被重置的情况。

        所以需要利用lua脚本在redis中执行的原子性,来确保不会发生这种情况(其实lua脚本执行会不会发生这个情况也是需要讨论的,目前没有查阅到相关资料会出现上面java执行的情况,测试中也是没有问题的)。

        这里还有一个点就是需要重新 expire ,为什么需要expire呢,这个我们放后面再讨论。

        重新优化完后,我们的代码逻辑大致如下图:

        

         虽然代码逻辑比上面看起来要复杂得多,不过做到了无锁化的设计,实际上只需要争抢一次redis锁,后续只需要在redis中做扣减即可。

        说明:这里同步数据库库存是直接 update goods set goodsCount = goodsCount - 1 where goodsId = xxx,也就是实时保持了数据库库存做同步的扣减。(这个尽量需要单独开一个事务,并放在大事务中的最后做执行)

        此时压测单机TPS上升到300~400+,90线都在1s以下。

        那么做到这一步,其实就可以交差了。其他的,比如失败回退库存,更新库存时去除库存缓存,这些逻辑就不在本文讨论之列了,可以自行发挥。

       

库存同步优化思路

         下面我们要讨论的就是数据库库存的同步扣减时机问题。注:以下操作没有在生产实践过,仅为思路。

        一般来说,我们翻阅的资料里,做完库存扣减的检查后,后续的比如商品秒杀的生成订单,这类操作,就会交由MQ去做处理,利用MQ的重试机制来保证事务的最终一致性,同时降低数据库压力,大致流程如下。

(当然实际像RocketMQ的事务消息是先发Half-Message,这里这样画是为了方便理解) 

        这里我们首先解答上面的一个问题,我们在incr后需要重新expire,这是为什么呢?因为我们需要考虑一种情况,我们的库存是有时效的,也就是中间可能会有一次请求需要到数据库取,如果当我们去数据库取的时候,而其他线程没有执行到数据库库存的同步扣减,那么就会导致缓存库存与数据库不一致,如下图:

        

         而我们如果在每次做redis incr扣减库存后,重新刷新过期时间,那么就可以保证我们在incr后,有expire的时间来保证我们去对数据库的库存做同步,这里就允许我们用MQ去做数据库的写入。

        而我们后面要做的,就是再把库存同步的时间再往后延迟。为什么呢,假设我们现在更新库存的语句为

        update goods set goodsCount = goodsCount - 1 where goodsId = xxx

        那么我们调1000次,实际上我们要的就是调一次

        update goods set goodsCount = goodsCount - 1000 where goodsId = xxx

        所以我们完全可以将库存更新同步数据库的请求进行累加,这里我们就可以利用MQ,每当有MQ消息扣减库存过来后,我们可以先在本地利用 LongAdder,进行累加,累加到一定值后,再执行对数据库的扣减,以及定期判断 LongAdder 是否 大于 0,如果 大于 0,则从 LongAdder 或 Reids 同步库存然后重置 LongAdder 。这里考虑用 Redis 里的库存是为了防止服务重启等状况的影响,当然如果能够做到优雅停机,那就可以定义destory方法,在destory方法里做。

        这里同时也需要考虑JVM内存问题,这些累加值实际上在Redis上都有,如果存JVM中,是否顶得住也是一个问题,所以也可以接收到MQ消息后,延迟几秒后去同步 Redis 中的库存。

        上述的思路可以大致如下图:

 

结语

        这次接触了这么一个业务,对redis秒杀库存这一套有了更进一步的认识,在此对过程中一些疑问做了总结,但是还是有一些点感觉云里雾里没有真正地去实践弄懂的话,可能还是无法完全掌握。另外就是对其他公司对于这类业务的实现有了更多的好奇心,因为后面大部分处理逻辑都只是自己的思路了,没有经历过生产的检验,所以也很想去了解下别人是怎么做的。

        

<think>好的,用户是Java岗位的,面试中可能会被问到Redis在项目中的使用情况。我需要帮他整理一个结构清晰、内容具体的回答。首先,得想想面试官想了解什么,他们可能关注候选人是否有实际经验,是否理解Redis的常见应用场景,以及如何解决实际问题。 首先,用户需要明确项目背景。比如,项目是电商、社交还是其他类型,这样回答更有针对性。比如电商的话,常见的场景包括缓存、分布式锁、秒杀系统、会话管理等。然后,要具体说明Redis在项目中的用途,比如缓存热点数据,减轻数据库压力,提高响应速度。这里需要提到具体的数据结构,比如String、Hash、List、Set、Zset,以及各自的应用场景,比如商品信息用String缓存,购物车用Hash存储。 接下来,可能涉及缓存穿透、雪崩、击穿的问题及解决方案。比如用布隆过滤器防穿透,随机过期时间防雪崩,互斥锁或永不过期防击穿。这部分展示问题解决能力。 分布式锁的实现也很重要,用setnx命令,设置超时时间,结合lua脚本保证原子性。还可以提到RedLock算法,但可能根据项目复杂度选择简单方案。此外,异步队列用List消息队列,或者Pub/Sub实现通知功能,比如订单状态更新。 会话管理方面,用Redis存储用户登录信息,共享Session,适用于集群环境。数据统计可能用HyperLogLog统计UV,或者Bitmap记录签到,展示对Redis高级功能的了解。 性能优化方面,连接池配置,合理设置超时时间,大Key拆分,避免慢查询。Pipeline批量操作提升效率。这些细节能体现实际经验。 最后,结合项目结果,比如QPS提升多少,响应时间降低多少,用数据量化成果。整个回答需要结构清晰,分点说明,结合实例,展示技术深度和实际应用能力。</think>作为Java岗位候选人,回答Redis使用时可按照以下结构分层阐述,重点突出技术选型逻辑和实战细节: --- ### 一、明确项目背景(铺垫场景) "在XX电商项目中,我们面临高并发读场景(如商品详情页访问量峰值达5W QPS),为缓解MySQL压力,我们引入Redis作为分布式缓存层,主要解决以下问题:" --- ### 二、核心使用场景(技术细节分层) #### 1. 热点数据缓存 - **数据结构选择**:商品基础信息使用`String`结构(序列化JSON),购物车用`Hash`存储用户维度数据 - **缓存策略**:采用`旁路缓存模式`,代码示例: ```java public Product getProduct(Long id) { String key = "product:" + id; String productJson = redisTemplate.opsForValue().get(key); if(productJson != null) return deserialize(productJson); Product product = productMapper.selectById(id); // DB查询 redisTemplate.opsForValue().set(key, serialize(product), 30, TimeUnit.MINUTES); return product; } ``` - **淘汰策略**:设置差异化的TTL(基础数据30分钟,库存数据5秒) #### 2. 分布式锁实现 - **技术方案**:采用`SET key uuid NX EX`指令实现,Lua脚本保证原子性 - **防死锁机制**:强制设置过期时间 + watchdog线程续期(Redisson实现) - **典型场景**:库存扣减、订单超时关闭 #### 3. 秒杀系统优化 - **前置库存校验**:使用Redis原子操作预减库存 ```java Long stock = redisTemplate.execute( "if redis.call('exists', KEYS[1]) == 1 then " + " local stock = tonumber(redis.call('get', KEYS[1]));" + " if stock > 0 then return redis.call('decr', KEYS[1]) end;" + "end return -1;", Collections.singletonList("seckill:stock:" + itemId)); ``` - **请求削峰**:库存扣减成功后,将订单信息写入Redis List队列异步处理 #### 4. 会话管理 - **Session存储**:采用Spring Session Data Redis实现分布式会话 - **数据结构**:Hash存储用户ID、权限、最后访问时间等字段 --- ### 三、进阶技术实践(展示技术深度) #### 1. 缓存异常处理 - **缓存击穿**:使用Redisson分布式锁实现互斥重建 - **缓存雪崩**:基础数据设置基础过期时间+随机偏移量(30min±300s) - **热点Key**:采用本地二级缓存+Redis分片多副本 #### 2. 性能优化 - **Pipeline批处理**:聚合200个库存查询请求,降低网络开销 - **大Key治理**:将10W成员的大Set拆分为100个Hash分片 --- ### 四、成果量化(数据佐证) "通过上述方案,核心接口响应时间从800ms降至120ms,MySQL负载降低72%,在双11大促期间平稳支撑了12W QPS的流量峰值" --- ### 五、技术反思(体现成长) "后续发现部分冷数据缓存命中率不足30%,我们增加了缓存预热机制和动态TTL调整策略,使整体命中率提升到68%" --- 通过这种"场景→技术方案→代码级细节→数据验证→反思优化"的结构化表达,既展示了技术应用的深度,又体现了工程实践中的系统性思考,比单纯罗列功能点更具说服力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值