1 . 异步秒杀优化
秒杀下单完整代码 :
- 同步(Synchronous)是指程序按照顺序依次执行,每一步操作完成后再进行下一步。在同步模式下,当一个任务开始执行时,程序会一直等待该任务完成后才会继续执行下一个任务。
- 异步(Asynchronous)是指程序在执行任务时,不需要等待当前任务完成,而是在任务执行的同时继续执行其他任务。在异步模式下,任务的执行顺序是不确定的,程序通过回调、事件通知等方式来获取任务执行的结果
关于测试jmeter参考 : 参考
之前的下单流程 :
当用户发起请求,此时会请求nginx,nginx会访问到tomcat,而tomcat中的程序,会进行串行操作,分成如下几个步骤 :
1、查询优惠卷
2、判断秒杀库存是否足够
3、查询订单
4、校验是否是一人一单
5、扣减库存
6、创建订单
在这六步操作中,又有很多操作是要去操作数据库的,而且还是一个线程串行执行, 这样就会导致我们的程序执行的很慢,所以我们需要异步程序执行,那么如何加速呢?
在这里笔者想给大家分享一下课程内没有的思路,看看有没有小伙伴这么想,比如,我们可以不可以使用异步编排来做,或者说我开启N多线程,N多个线程,一个线程执行查询优惠卷,一个执行判断扣减库存,一个去创建订单等等,然后再统一做返回,这种做法和课程中有哪种好呢?答案是课程中的好,因为如果你采用我刚说的方式,如果访问的人很多,那么线程池中的线程可能一下子就被消耗完了,而且你使用上述方案,最大的特点在于,你觉得时效性会非常重要,但是你想想是吗?并不是,比如我只要确定他能做这件事,然后我后边慢慢做就可以了,我并不需要他一口气做完这件事,所以我们应当采用的是课程中,类似消息队列的方式来完成我们的需求,而不是使用线程池或者是异步编排的方式来完成这个需求。
优化方案:
我们将耗时比较短的逻辑判断放入到redis中,比如是否库存足够,比如是否一人一单,这样的操作,只要这种逻辑可以完成,就意味着我们是一定可以下单完成的,我们只需要进行快速的逻辑判断,根本就不用等下单逻辑走完,我们直接给用户返回成功, 再在后台开一个线程,后台线程慢慢的去执行queue里边的消息,这样程序不就超级快了吗?而且也不用担心线程池消耗殆尽的问题,因为这里我们的程序中并没有手动使用任何线程池,当然这里边有两个难点
第一个难点是我们怎么在redis中去快速校验一人一单,还有库存判断
第二个难点是由于我们校验和tomct下单是两个线程,那么我们如何知道到底哪个单他最后是否成功,或者是下单完成,为了完成这件事我们在redis操作完之后,我们会将一些信息返回给前端,同时也会把这些信息丢到异步queue中去,后续操作中,可以通过这个id来查询我们tomcat中的下单逻辑是否完成了。
优化后流程图 :
实现细节 :
-
首先将减库存和创建订单这种耗时多的分离出来,单独安排一个线程去执行;
-
然后对于判断秒杀库存和校验一人一单这种需要查询数据库的操作用redis来进行优化 : 将优惠卷和订单信息存在redis当中,秒杀资格的判断,放到redis中去做;
-
当主线程进来之后,先去找redis,在redis中完成资格校验,这里先保存优惠卷id,用户id,订单id,到阻塞队列,然后返回用户一个订单id,然后一步读取队列中的信息 , 完成下单 ;
整体思路:
当用户下单之后,判断库存是否充足只需要导redis中去根据key找对应的value是否大于0即可,如果不充足,则直接结束,如果充足,继续在redis中判断用户是否可以下单,如果set集合中没有这条数据,说明他可以下单,如果set集合中没有这条记录,则将userId和优惠卷存入到redis中,并且返回0,整个过程需要保证是原子性的,我们可以使用lua来操作
当以上判断逻辑走完之后,我们可以判断当前redis中返回的结果是否是0 ,如果是0,则表示可以下单,则将之前说的信息存入到到queue中去,然后返回,然后再来个线程异步的下单,前端可以通过返回的订单id来判断是否下单成功。
总结 : 将同步操作变成异步操作,大大得缩短秒杀业务的流程,从而提高了秒杀业务的并发 ;
2 . Redis完成秒杀资格判断
需求:
-
新增秒杀优惠券的同时,将优惠券信息保存到Redis中
-
基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
-
如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
-
开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
2.1 添加优惠卷库存到Redis中 :
// 保存秒杀库存到redis
stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY+voucher.getId(), voucher.getStock().toString()) ;
在postman中用添加秒杀卷的方式测试 :
2.2 基于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)
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0
VoucherOrderServiceImpl :
// 执行Lua脚本
private static DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>() ;
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);//设置返回类型
}
/**
* 优惠卷秒杀下单
* @param voucherId
* @return
*/
@Override
// @Transactional
public Result seckillVoucher(Long voucherId) {
// 获取用户
Long userId = UserHolder.getUser().getId();
Long orderId = redisIdWorker.nextId("order");
// 1 . 执行Lua脚本
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(),
userId.toString(),
String.valueOf(orderId)
) ;
// 2 . 判断结果是0
int r = result.intValue();
if(r != 0) {
// 2 . 1 不为0 , 代表没有购买资格
return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
}
// 2 . 2 为0 , 由购买资格 , 把下单信息保存到阻塞队列
// TODO 保存阻塞队列
// 3 . 返回订单id
return Result.ok() ;
}
测试 :
当同一用户访问第二次的时候,返回了"不能重复下单" ;
3 . 基于阻塞队列实现秒杀优化
3 . 1 抢单
阻塞队列 (BlockingQueue):
-
当一个线程尝试从一个阻塞队列中获取元素的时候,如果没有元素 , 就会被阻塞 ,直到队列中有元素才会被唤醒,然后获取元素;
-
然而当前订单也不是一直有,只有当有人下单的时候,才会有,所以这里适合采用阻塞队列;
创建阻塞队列 :
// 创建阻塞队列
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024) ;
抢单代码 (获取资格之后,封装VoucherOrder到阻塞队列当中去):
// 2 . 2 为0 , 由购买资格 , 把下单信息保存到阻塞队列
VoucherOrder voucherOrder = new VoucherOrder();
// 2 . 2 订单id
voucherOrder.setId(orderId);
// 2 . 3 用户id
voucherOrder.setUserId(userId);
// 2 . 4 代金卷id
voucherOrder.setVoucherId(voucherId);
// 2 . 5
save(voucherOrder);
// TODO 保存阻塞队列
orderTasks.add(voucherOrder);
3 . 2 异步下单 :
实现步骤 :
-
要做的是开启异步独立线程, 不断实现下单任务 ;
-
需要实现线程池和线程任务 ;
-
线程任务需要在任务类初始化完成之后,就开始执行(任何时候都可能有任务进来) , 这里利用Spring注解@PostConstruct来实现 ;
-
任务只有两步 : 获取订单信息 , 和创建订单;
完整代码 :
package com.hmdp.service.impl;
import com.hmdp.config.RedissonConfig;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.SimpleRedisLock;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private RedisIdWorker redisIdWorker ; // 注入id生成器
@Resource
private ISeckillVoucherService seckillVoucherService ;
@Resource
private StringRedisTemplate stringRedisTemplate ;
@Resource
private RedissonClient redissonClient;
// 执行Lua脚本
private static DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>() ;
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);//设置返回类型
}
// 创建阻塞队列
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024) ;
// 线程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
@PostConstruct // 在当前类初始化完毕之后就执行
private void init(){
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler()) ; // 提交任务
}
/**
* 创建订单
* @param voucherOrder
*/
private void handleVoucherOrder(VoucherOrder voucherOrder) {
// 1 . 获取用户id
Long userId = voucherOrder.getUserId();
// 2 . 创建锁对象
RLock lock = redissonClient.getLock("lock:order:"+userId);
// 3 . 获取锁
boolean isLock = lock.tryLock();//无参 , 错误不等待
// 4 . 判断是否获取锁成功
if(!isLock) {
// 获取锁失败,返回错误或重试
log.error("不允许重复下单");
return ;
}
try{
// 获取代理对象(事务)
// IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy() ;
proxy.createVoucherOrder(voucherOrder) ;
}finally {
// 释放锁
lock.unlock();
}
}
// 线程任务 :
private class VoucherOrderHandler implements Runnable{
@Override
public void run(){
while(true){
try {
// 1 . 获取队列中的订单信息
VoucherOrder voucherOrder = orderTasks.take() ;
// 2 . 创建队列
handleVoucherOrder(voucherOrder) ;
} catch (Exception e) {
log.error("处理订单异常!",e);
}
}
}
}
private IVoucherOrderService proxy ;
/**
* 优惠卷秒杀下单
* @param voucherId
* @return
*/
@Override
// @Transactional
public Result seckillVoucher(Long voucherId) {
// 获取用户
Long userId = UserHolder.getUser().getId();
Long orderId = redisIdWorker.nextId("order");
// 1 . 执行Lua脚本
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(),
userId.toString(),
String.valueOf(orderId)
) ;
// 2 . 判断结果是0
int r = result.intValue();
if(r != 0) {
// 2 . 1 不为0 , 代表没有购买资格
return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
}
// 2 . 2 为0 , 由购买资格 , 把下单信息保存到阻塞队列
VoucherOrder voucherOrder = new VoucherOrder();
// 2 . 2 订单id
voucherOrder.setId(orderId);
// 2 . 3 用户id
voucherOrder.setUserId(userId);
// 2 . 4 代金卷id
voucherOrder.setVoucherId(voucherId);
// 2 . 5
save(voucherOrder);
// 2 . 6 保存阻塞队列
orderTasks.add(voucherOrder);
// 3 . 1 获取代理对象
proxy = (IVoucherOrderService) AopContext.currentProxy() ;
// 3 . 返回订单id
return Result.ok(orderId) ;
}
/**
* 加锁
* @param voucherOrder
*/
@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
// 5 . 一人一单
Long userId = voucherOrder.getUserId();
// 5 . 1 查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder).count();
// 5 . 2 判断用户师是否存在
if (count > 0) {
// 用户已经购买过了
log.error("用户已经购买了一次!");
return ;
}
// 6 . 扣减库存
boolean tag = seckillVoucherService.update()
.setSql("stock = stock - 1") // set stock = stock - 1
.eq("voucher_id", voucherOrder)
.gt("stock", 0)
.update();
if (!tag) {
log.error("用户已经购买了一次!");
return ;
}
save(voucherOrder);
}
}
注意 :
- 在异步实现某些事情时,不能通过ThreadLocal的方式来获取信息,比如proxy和获取UserId,这些在多线程下都是不一样的 ;
测试 :