问题:当前秒杀功能的执行逻辑是怎样的?

​ –解答:

image-20240314200959800

问题:这种逻辑存在什么问题?

​ –解答:所有处理均由一个线程完成,性能较低。

问题:如何进行优化?

​ –解答:可以使用Redis完成检查库存是否充足和校验一人一单的过程,对于满足条件的线程(请求),将其放入阻塞队列中,异步的处理这些请求(生成订单,扣减库存,将订单信息存入数据库中)

问题:为什么要用阻塞队列?

​ –解答:因为我们想要异步的处理生成订单的请求,而不是一旦某个用户满足条件,就立即为他生成订单(这里的生成订单指的是扣减数据库中的库存)。

问题:BlockingQueue有哪些特性?

​ –解答:本质上是一个队列,这也是一个接口,有多个实现类,这里选择的是ArrayBlockingQueue。阻塞队列最大的特点就是,当取不到数据时,该线程会一直等待,直到队列中有数据。

问题:那这样不会影响效率吗,因为要一直等待?

​ –解答:由于我们在处理订单,也就是从阻塞队列中取数据时,是交给另一个线程来做的,因此对我们的主线程不会有影响,其实主线程在redis判断完后就基本上可以将秒杀结果返回给用户了。

问题:Redis中是如何工作的?

​ –解答:这也是我们接下来要说到的。因为要保证Redis中操作的原子性,因此我们会将这些操作以Lua脚本的方式呈现。首先我们会判断对应优惠券的库存是否充足,这里我们可以使用String,Value结构来存储数据;至于校验该用户是否购买过该优惠券,则可以使用String,Set结构来存储数据。使用Set的好处有很多,比如key中对应的value是唯一的,不会存在多个相同的value,这时只要调用set 的ismember命令就能够判断该用户是否购买过此优惠券。

​ 因此我们现在的秒杀流程变成了这样:

image-20240315114547926

实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/**
* <p>
* 秒杀优惠券表,与优惠券是一对一关系 服务实现类
* </p>
*
* @author dch
* @since 2024-03-08
*/
@Service
@Slf4j
public class SeckillVoucherServiceImpl extends ServiceImpl<SeckillVoucherMapper, SeckillVoucher> implements ISeckillVoucherService {
@Resource
private UniqueIdGenerator uniqueIdGenerator;
@Resource
private VoucherOrderMapper voucherOrderMapper;
private static DefaultRedisScript redisScript;
static{
redisScript = new DefaultRedisScript();
redisScript.setLocation(new ClassPathResource("/RedisWork.lua"));
redisScript.setResultType(Long.class);
}
@Resource
private StringRedisTemplate stringRedisTemplate;
private BlockingQueue<VoucherOrder> blockingQueue = new ArrayBlockingQueue<>(1024 * 1024);
/**
* 创建一个线程
*/
private static ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
private ISeckillVoucherService currentProxy;

@PostConstruct
public void init(){
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}

private class VoucherOrderHandler implements Runnable{
//项目一启动就检查阻塞队列中是否有未处理的订单,有就进行处理
public void run(){
while(true){
try {
VoucherOrder take = blockingQueue.take();
//创建订单
handleVoucherOrder(take);
} catch (Exception e) {
log.error("处理订单异常",e);
}
}
}
}
public Result seckillVoucher(Long id){
//1.根据id从数据库中查询对应的优惠券
SeckillVoucher seckillVoucher = getById(id);
//2.判断该优惠券是否开始以及结束
if(seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())){
return Result.fail("活动未开始");
}
if(seckillVoucher.getEndTime().isBefore(LocalDateTime.now())){
return Result.fail("活动已结束");
}
List<String> keys = new ArrayList<>();
keys.add(RedisConstants.SECKILL_STOCK_KEY + id);
keys.add(RedisConstants.SECKILL_VOUCHER_KEY + id);
//3.调用lua脚本判断库存是否充足以及该用户是否已经购买过该秒杀券
Long userid = UserHolder.getUser().getId();
Long flag = (Long)stringRedisTemplate.execute(redisScript, keys, userid + "");

if(flag == 1){
return Result.fail("库存不足");
}
if(flag == 2){
return Result.fail("不能重复购买");
}

//4.生成订单
long seckillVoucherOrderId = uniqueIdGenerator.generatorId(RedisConstants.SECKILL_VOUCHER_ID_PREFIX);
VoucherOrder voucherOrder = new VoucherOrder();
//4.1 填入订单id,使用全局唯一id生成器生成
voucherOrder.setId(seckillVoucherOrderId);
//4.2 填入用户id
voucherOrder.setUserId(userid);
//4.3 填入优惠券id
voucherOrder.setVoucherId(id);
//4.4 将订单放入阻塞队列,等待处理
blockingQueue.add(voucherOrder);
currentProxy = (ISeckillVoucherService) AopContext.currentProxy();

return Result.ok(voucherOrder);
}

/**
* 处理订单
* @param voucherOrder
*/
public void handleVoucherOrder(VoucherOrder voucherOrder){
currentProxy.createVoucherOrder(voucherOrder);
}

/**
* 更新数据库
* @param voucherOrder
*/
@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder){
//5.更新库存
boolean success = update().setSql("stock = stock - 1").eq("voucher_id", voucherOrder.getVoucherId()).gt("stock",0).update();
voucherOrderMapper.insert(voucherOrder);
}
}