目录
一、优化思路
二、缓存库存与订单
1、库存缓存的redis数据结构
2、订单信息缓存的redis数据结构
三、整体流程
四、lua脚本确保权限校验操作的原子性
一、优化思路
【Redis】电商项目秒杀问题之超卖问题与一人一单问题_1373i的博客-CSDN博客https://blog.csdn.net/qq_61903414/article/details/130568972?spm=1001.2014.3001.5501在之前的文章里解决了电商项目项目超卖与一人一单的一些线程安全问题,之前的操作大体流程是:下单请求到达服务器,服务器会先查询库存是否足够,如果足够则继续判断用户是否已经下过单,如果没有下过单则去进行后续扣减库存生成订单这些操作我,完成后返回给客户端。如果在高并发情况下该接口的性能是相对较低的,因为上述操作有许多数据库的读写操作,只有等这些操作完成后我们才能返回响应,那么怎么去优化响应速度呢?
【RabbitMQ】初识消息中间件MQ_1373i的博客-CSDN博客https://blog.csdn.net/qq_61903414/article/details/130138361?spm=1001.2014.3001.5501在之前RabbitMQ的文章中我们有提到MQ的优点,有一个就是异步提速,这里我们可以借鉴这一思路,异步的去处理一些操作。上述的操作其实主要分为两类,一类是对用户是否有购买权限的校验,还有一类是执行后续下单操作。这两类的关系是首先你必须得有购买权限才能去执行后续操作,那么现在我们可以将权限校验这一部分操作提取处理,请求到达服务器后先去进行用户是否有购买权限的校验,也就是先判断库存是否足够与用户是否已下单,如果用户满足购买要求则直接将订单号返回给前端,然后将该订单存入一个阻塞队列中,开启一个扫描线程,扫描该队列异步的去处理后续下单操作,但是此处值得注意的是我们如果使用Java中的阻塞队列的化,其实性能也不一定会提高,因为他是占用jvm资源的,所以我们可以使用RabbitMQ等这些消息队列中间件,我们可以使用redis来实现消息队列的效果,在后续的文章会讲到。现在我们对下单操作进行了优化,此时我们还可以对用户购买权限的校验进行优化,这两个操作都是去查询数据库的,在前面的文章里我们学习了redis缓存的使用,所以我们可以使用redis做缓存将商品的库存与订单信息缓存到redis中,此时请求到服务器后会去redis中获取到库存如果足够,在从redis中查询订单信息是否已存,如果存在则将redis中的库存进行扣减,然后加入订单缓存,将该订单号返回客户端,将订单信息交给MQ异步的去处理扣减数据库库存。
二、缓存库存与订单
1、库存缓存的redis数据结构
库存的缓存使用string就可以了,不过在缓存的时候key需要注意他的命名,在我们使用缓存时也要注意缓存时key的命名,要有区分度比如:业务名+特定信息
2、订单信息缓存的redis数据结构
这里的订单信息缓存我们主要使用它来判断用户是否已经下单所以此处缓存没有必要将所有的订单信息都缓存进来,此处我们只需要缓存用户id即可,在查询时查询该用户id是否存在即可判断,此时我们可以使用set来存储订单信息,key为业务order+商品id,value为购买过该商品的用户id,由于set数据结构的特点,当用户已经下过单时,在进行用户id存入该缓存时如果已经下过单了就会存入失败,以此来判断用户是否下过单
三、整体流程
上述流程还存在线程安全问题,即上述查询库存以及查询缓存订单信息与扣减缓存库存这一系列操作不是原子性的,那么我们可以通过lua脚本来保证上述操作的原子性以此保证线程安全。
所有整体的秒杀流程如下图:请求到服务器时先执行用户购买权限校验的lua脚本,拿到脚本执行的结果后判断是否满足下单操作,如果满足则将订单信息存入MQ并返回订单id
四、lua脚本确保权限校验操作的原子性
首先我们在使用Java操作redis执行该lua脚本时先要考虑该脚本需要哪些参数呢,我们要判断库存是否足够,就需要传入该商品的信息,然后我们需要判断用户是否已经下过单就需要知道该用户的id,所以我们需要知道商品的id与用户的id
-- 获取参数-- 1.商品idlocal productId = ARGV[1];-- 2.用户idlocal userId = ARGV[2];-- 构造缓存的key-- 1.订单keylocal orderKey = "secKill:order" .. productId;-- 2.库存idlocal stockKey = "secKill:stock" .. productId;-- 判断库存是否足够if (tonumber(redis.call('get',stockKey)) <= 0) then-- 库存不足 返回1return 1;end-- 判断是否下过单if (redis.call('sismember',orderKey,userId) == 1) then-- 存在 返回2return 2;end-- 满足扣减库存redis.call('incrby',stockKey,-1);-- 下单:缓存订单中加入该用户redis.call("sadd",orderKey,userId);return 0;
然后我们可以在Java中调用API执行这段lua脚本