【Redis实战篇】秒杀优化

1. 秒杀优化-异步秒杀思路

我们来回顾一下下单流程

当用户发起请求,此时会请求nginxnginx会访问到tomcat,而tomcat中的程序,会进行串行操作,分成如下几个步骤

  • 1、查询优惠卷

  • 2、判断秒杀库存是否足够

  • 3、查询订单

  • 4、校验是否是一人一单

  • 5、扣减库存

  • 6、创建订单

在这六步操作中,由于有很多操作是要去操作数据库的,而且还是一个线程串行执行,添加了分布式锁, 这样就会导致我们这段程序执行耗时比较长,并发能力变弱了,所以我们需要异步程序执行,那么如何优化呢?

在这里笔者想给大家分享一下课程内没有的思路,看看有没有小伙伴这么想,比如,我们可以不可以使用异步编排来做,或者说我开启N多个线程,一个线程执行查询优惠卷,一个执行判断扣减库存,一个去创建订单等等,然后再统一做返回,这种做法和课程中有哪种好呢?答案是课程中的好,因为如果你采用我刚说的方式,如果访问的人很多,那么线程池中的线程可能一下子就被消耗完了,而且你使用上述方案,最大的特点在于,你觉得时效性会非常重要,但是你想想是吗?并不是,比如我只要确定他能做这件事,然后我后边慢慢做就可以了,我并不需要他一口气做完这件事,所以我们应当采用的是课程中,类似消息队列的方式来完成我们的需求,而不是使用线程池或者是异步编排的方式来完成这个需求。

优化方案 :我们将耗时比较短的逻辑判断放入到redis中,比如是否库存足够,比如是否一人一单,这样的操作,只要这种逻辑可以完成,就意味着我们是一定可以下单完成的,我们只需要进行快速的逻辑判断,根本就不用等下单逻辑走完,我们直接给用户返回成功, 再在后台开一个线程,后台线程慢慢的去执行阻塞queue里边的消息,这样程序不就超级快了吗?而且也不用担心线程池消耗殆尽的问题,因为这里我们的程序中并没有手动使用任何线程池,当然这里边有两个难点:

  • 第一个难点是我们怎么在redis中去快速校验一人一单,还有库存判断

  • 第二个难点是由于我们校验和tomcat下单是两个线程,那么我们如何知道到底哪个单他最后是否成功,或者是下单完成,为了完成这件事我们在redis操作完之后,我们会将一些信息返回给前端,同时也会把这些信息丢到异步queue中去,后续操作中,可以通过这个id来查询我们tomcat中的下单逻辑是否完成了。

我们现在来看看整体思路:

  1. 选择Redis存储结构 : 用户下单我们只需要存储库存变量即可,可以直接使用string类型结构,而一人一单问题,我们是需要存储很多用户的购买记录,并且用户不能重复下单,所以这里我们选择set存储结构再适合不过。
  2. 逻辑 :当用户下单之后,判断库存是否充足只需要到redis中去根据key找对应的value是否大于0即可,如果不充足,则直接结束,如果充足,继续在redis中判断用户是否可以下单,如果set集合中没有这条数据,说明他可以下单,并扣减库存,再将userId存入当前优惠券的set中,并且返回0,整个过程需要保证是原子性的,我们可以使用lua来操作。当以上判断逻辑走完之后,我们可以判断当前redis中返回的结果是否是0 ,如果是0,则表示可以下单,则将之前说的信息存入到阻塞queue中去,此时开启单独的线程异步写入数据库中,最后返回订单id,前端可以通过返回的订单id来判断是否下单成功。

2. 秒杀优化-Redis完成秒杀资格判断

需求:

  • 新增秒杀优惠券的同时,将优惠券信息保存到Redis中

  • 基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功

  • 如果抢购成功,将优惠券id和用户id封装后存入阻塞队列

  • 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能


VoucherServiceImpl👇 将用户资格判断放入redis中,用户响应信息只有和redis操作,性能大幅提高,新增秒杀优惠券的同时,将优惠券库存同步到redis中

java 复制代码
@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {
    // 保存优惠券
    save(voucher);
    // 保存秒杀信息
    SeckillVoucher seckillVoucher = new SeckillVoucher();
    seckillVoucher.setVoucherId(voucher.getId());
    seckillVoucher.setStock(voucher.getStock());
    seckillVoucher.setBeginTime(voucher.getBeginTime());
    seckillVoucher.setEndTime(voucher.getEndTime());
    seckillVoucherService.save(seckillVoucher);
    // 保存秒杀库存到Redis中
    //SECKILL_STOCK_KEY 这个变量定义在RedisConstans中
    //private static final String SECKILL_STOCK_KEY ="seckill:stock:"
    stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}

seckill.lua脚本👇 对redis的查询、判断、数据操作写入一个lua脚本,防止线程并发安全问题

lua 复制代码
-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 1.3.订单id
local orderId = ARGV[3]

-- 2.数据key
-- 2.1.库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId

-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.2.库存不足,返回1
    return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3.存在,说明是重复下单,返回2
    return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
return 0

写完lua脚本,剩下的就是执行lua脚本,根据脚本拿到的结果,为非0,根据情况返回"库存不足" 或者 "不能重复下单;为0,返回全局id生成器生成的订单id。至于订单保存到阻塞队列中的逻辑,咱们稍后实现。

VoucherOrderServiceImpl

3. 秒杀优化-基于阻塞队列实现秒杀优化

创建阻塞队列,保存数据类型为VoucherOrder,并设置大小为 1024 * 1024

处理订单操作的逻辑:👇

4. 总结

秒杀业务的优化思路是什么?

  • 先利用Redis完成库存余量、一人一单判断,完成抢单业务
  • 再将下单业务放入阻塞队列,利用独立线程异步下单

基于阻塞队列的异步秒杀存在哪些问题?

  • 内存限制问题(使用jdk中的阻塞队列,以后如果有大量的订单需要创建,很容易出现OOM问题)
  • 数据安全问题(数据是存储在内存里的,若出现服务宕机了,任务还没执行完毕,导致用户的订单数据丢失)

针对此问题,我们将会再下一篇解决。

相关推荐
编程、小哥哥4 分钟前
互联网大厂Java面试:从Spring Boot到微服务架构的技术深挖
java·spring boot·redis·微服务·prometheus·面试技巧
Elastic 中国社区官方博客8 分钟前
Elasticsearch 索引副本数
大数据·数据库·elasticsearch·搜索引擎·全文检索
冬瓜的编程笔记31 分钟前
【八股战神篇】MySQL高频面试题
数据库·mysql·面试
hello1114-39 分钟前
Redis学习打卡-Day3-分布式ID生成策略、分布式锁
redis·分布式·学习
赵渝强老师1 小时前
【赵渝强老师】Memcached的路由算法
数据库·redis·nosql·memcached
belldeep1 小时前
groovy 如何遍历 postgresql 所有的用户表 ?
数据库·postgresql
2401_896008191 小时前
PostgreSQL
数据库·postgresql
搞不懂语言的程序员1 小时前
Redis Sentinel如何实现高可用?
数据库·redis·sentinel
wangzhongyudie1 小时前
SQL实战:06交叉日期打折问题求解
数据库·sql
掘金-我是哪吒2 小时前
分布式微服务系统架构第129集:redis安装部署文档
redis·分布式·微服务·架构·系统架构