2025.09.24

黑马点评开发日志Day05——秒杀优化、消息队列

一、秒杀优化

1.1 异步秒杀思路

  • 原本的下单流程:当用户发起请求,此时会请求nginx,nginx会访问到tomcat,而tomcat中的程序,会进行串行操作,分成如下几个步骤:
    1. 查询优惠卷
    2. 判断秒杀库存是否足够
    3. 查询订单
    4. 校验是否是一人一单
    5. 扣减库存
    6. 创建订单

原下单流程

  • 其中,查询优惠卷、查询订单、减库存以及创建订单这四个步骤都需要访问数据库,所以耗时比较高,而我们又是串行执行的这几个步骤,所以在高并发情况下效果会比较差

  • 优化方案:我们将耗时比较短的逻辑判断放入到redis中,比如是否库存足够,比如是否一人一单,这样的操作,只要这种逻辑可以完成,就意味着我们是一定可以下单完成的,我们只需要进行快速的逻辑判断,根本就不用等下单逻辑走完,我们直接给用户返回成功, 再在后台开一个线程,后台线程慢慢的去执行queue里边的消息,这样程序不就很快了

优化后下单流程

  • 优化后的思路:当用户下单之后,判断库存是否充足只需要到redis中去根据key找对应的value是否大于0即可,如果不充足,则直接结束,如果充足,继续在redis中判断用户是否可以下单,如果set集合中没有这条数据,说明他可以下单,如果set集合中没有这条记录,则将userId和优惠卷存入到redis中,并且返回0,整个过程需要保证是原子性的,我们可以使用lua来操作。当以上判断逻辑走完之后,我们可以判断当前redis中返回的结果是否是0 ,如果是0,则表示可以下单,则将之前说的信息存入到到queue中去,然后返回,然后再来个线程异步的下单,前端可以通过返回的订单id来判断是否下单成功。

异步下单流程

1.2 基于Redis完成秒杀资格判断

  • 需求:
    • 新增秒杀优惠券的同时,将优惠券信息保存到Redis中
    • 基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
    • 如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
    • 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能

1.2.1 seckill.lua

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
-- 1. 参数列表
-- 1.1 优惠券id
local voucherId = ARGV[1]
-- 1.2 用户id
local userId = ARGV[2]

-- 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.3 判断用户是否下单 SISMEMBER orderKey userId
if (redis.call('sismember', orderKey, userId) == 1) then
-- 3.4 存在,说明是重复下单,返回2
return 2
end
-- 3.5 扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.6 下单(保存用户)
redis.call('sadd', orderKey, userId)
return 0

1.2.2 VoucherOrderServiceImpl

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
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

@Resource
private ISeckillVoucherService seckillVoucherService;

@Resource
private RedisIdWorker redisIdWorker;

@Resource
private StringRedisTemplate stringRedisTemplate;

@Resource
private RedissonClient redissonClient;

private static final 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
public Result seckillVoucher(Long voucherId) {
// 获取用户
Long userId = UserHolder.getUser().getId();

// 1. 执行Lua脚本
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString()
);

// 2. 判断结果是否为0
int r = result.intValue();
if (r != 0) {
// 2.1 不为0,代表没有购买资格
return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
}
// 2.2 为0,有购买资格,把下单信息保存到阻塞队列
long orderId = redisIdWorker.nextId("order");
// TODO 保存阻塞队列

// 3. 返回订单id
return Result.ok(orderId);
}
}

1.3 基于阻塞队列实现秒杀异步下单

1.3.1 VoucherOrderServiceImpl

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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

@Resource
private ISeckillVoucherService seckillVoucherService;

@Resource
private RedisIdWorker redisIdWorker;

@Resource
private StringRedisTemplate stringRedisTemplate;

@Resource
private RedissonClient redissonClient;

private static final 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();});
}

// 用于线程池处理的任务
// 当初始化完毕后,就会去从对列中去拿信息
private class VoucherOrderHandler implements Runnable {
@Override
public void run() {
while (true) {
try {
// 1. 获取队列中的订单信息
VoucherOrder voucherOrder = orderTasks.take();
// 2. 创建订单
handleVocherOrder(voucherOrder);
} catch (Exception e) {
log.error("处理订单异常", e);
}
}
}
}

private void handleVocherOrder(VoucherOrder voucherOrder) {
// 1. 获取用户
Long userId = voucherOrder.getUserId();
// 2. 创建锁对象
RLock lock = redissonClient.getLock("lock:order:" + userId);
// 3. 获取锁
boolean isLock = lock.tryLock();
// 4. 判断是否成功
if (!isLock) {
// 获取失败,返回错误或重试
log.error("不允许重复下单");
return;
}
try {
// 5. 获取代理对象
proxy.createVoucherOrder(voucherOrder);
} finally {
// 6. 释放锁
lock.unlock();
}
}

private IVoucherOrderService proxy;

/**
* 优惠券秒杀下单
* @param voucherId
* @return
*/
@Override
public Result seckillVoucher(Long voucherId) {
// 获取用户
Long userId = UserHolder.getUser().getId();

// 1. 执行Lua脚本
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString()
);

// 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.3 订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 2.4 用户id
voucherOrder.setUserId(userId);
// 2.5 代金券id
voucherOrder.setVoucherId(voucherId);
// 2.6 放入阻塞队列
orderTasks.add(voucherOrder);

// 3. 获取代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();

// 4. 返回订单id
return Result.ok(orderId);
}

@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
// 5. 一人一单
// 5.1 查询订单
Long userId = UserHolder.getUser().getId();

int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
// 5.2 判断是否存在
if (count > 0) {
log.error("用户已经购买过了");
return;
}
// 6. 扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // set stock = stock - 1
.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0) // where id = ? and stock > 0
.update();
if (!success) {
// 扣减失败
log.error("库存不足");
return;
}
// 7. 创建订单
save(voucherOrder);
}
}

二、消息队列

2.1 消息队列概述

  • 消息队列(Message Queue),字面意思就是存放消息的队列。最简单的消息队列模型包括3个角色:
    • 消息队列:存储和管理消息,也被称为消息代理(Message Broker)
    • 生产者:发送消息到消息队列
    • 消费者:从消息队列获取消息并处理消息

消息队列模型

  • Redis提供了三种不同的方式来实现消息队列:
    • list结构:基于List结构模拟消息队列
    • PubSub:基本的点对点消息模型
    • Stream:比较完善的消息队列模型

2.2 基于List结构模拟消息队列

  • 队列的入口和出口不在一边,因此我们可以利用:LPUSH结合RPOP、或者RPUSH结合LPOP来实现
  • 不过要注意的是,当队列中没有消息时RPOPLPOP操作会返回null,并不像JVM的阻塞队列那样会阻塞并等待消息。因此这里应该使用BRPOP或者BLPOP来实现阻塞效果。

基于List结构模拟消息队列

  • 优点:
    • 利用Redis存储,不受限于JVM内存上限
    • 基于Redis的持久化机制,数据安全性有保证
    • 可以满足消息有序性
  • 缺点:
    • 无法避免消息丢失
    • 只支持单消费者

2.2 基于PubSub的消息队列

  • PubSub(发布订阅)是Redis2.0版本引入的消息传递模型。顾名思义,消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息。
    • SUBSCRIBE channel [channel] :订阅一个或多个频道
    • PUBLISH channel msg :向一个频道发送消息
    • PSUBSCRIBE pattern[pattern] :订阅与pattern格式匹配的所有频道

基于PubSub的消息队列

  • 优点:
    • 采用发布订阅模型,支持多生产、多消费
  • 缺点:
    • 不支持数据持久化
    • 无法避免消息丢失
    • 消息堆积有上限,超出时数据丢失

2.3 Stream的单消费模式

  • Stream 是 Redis 5.0 引入的一种新数据类型,可以实现一个功能非常完善的消息队列。

  • 发送消息的命令:XADD

    • 例如:
  • 读取消息的方式之一:XREAD

    • 例如:
    • XREAD阻塞方式读取最新消息:
  • 在业务开发中,我们可以循环的调用XREAD阻塞方式来查询最新消息,从而实现持续监听队列的效果,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
while (true) {
// 尝试读取队列中的消息,最多阻塞2秒
Object msg = redis.execute("XREAD COUNT 1 BLOCK 2000 STREAMS users $");
if (msg == null) {
// 2秒内没有消息,继续下一次循环
continue;
}

// 处理消息
handleMessage(msg);
}
  • 但是:当我们指定起始ID为$时,代表读取最新的消息,如果我们处理一条消息的过程中,又有超过1条以上的消息到达队列,则下次获取时也只能获取到最新的一条,会出现漏读消息的问题。

  • STREAM类型消息队列的XREAD命令特点:

    • 消息可回溯
    • 一个消息可以被多个消费者读取
    • 可以阻塞读取
    • 有消息漏读的风险

2.4 基于Stream的消息队列-消费者组

  • 消费者组(Consumer Group):将多个消费者划分到一个组中,监听同一个队列。具备下列特点:

    • 消息分流:队列中的消息会分流给组内的不同消费者,而不是重复消费,从而加快消息处理的速度
    • 消息标识:消费者组会维护一个标示,记录最后一个被处理的消息,哪怕消费者宕机重启,还会从标示之后读取消息。确保每一个消息都会被消费
    • 消息确认:消费者获取消息后,消息处于pending状态,并存入一个pending-list。当处理完成后需要通过XACK来确认消息,标记消息为已处理,才会从pending-list移除
  • 创建消费者组

    1
    XGROUP CREATE key groupName ID [MKSTREAM]>
    • key: 队列名称
    • groupName: 消费者组名称
    • ID: 起始ID标示,$代表队列中最后一个消息,0则代表队列中的第一个消息
    • MKSTREAM: 队列不存在时自动创建队列
  • 其他常见命令:

    1
    2
    3
    4
    5
    6
    // 删除指定的消费者组
    XGROUP DESTORY key groupName
    // 给指定的消费者组添加消费者
    XGROUP CREATECONSUMER key groupname consumername
    // 删除消费者组中的指定消费者
    XGROUP DELCONSUMER key groupname consumername
  • 从消费者组读取消息:

    1
    XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
    • group:消费组名称
    • consumer:消费者名称,如果消费者不存在,会自动创建一个消费者
    • count:本次查询的最大数量
    • BLOCK milliseconds:当没有消息时最长等待时间
    • NOACK:无需手动ACK,获取到消息后自动确认
    • STREAMS key:指定队列名称
    • ID:获取消息的起始ID:
      • “>”:从下一个未消费的消息开始
      • 其它:根据指定id从pending-list中获取已消费但未确认的消息,例如0,是从pending-list中的第一个消息开始
  • 消费者监听消息的基本思路:

    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
    while (true) {
    // 尝试监听队列,使用阻塞模式,最长等待2000毫秒
    Object msg = redis.execute("XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS >");
    if (msg == null) {
    // 2秒内没有消息,继续下一次循环
    continue;
    }
    try {
    // 处理消息,完成后一定要ACK
    handleMessage(msg);
    } catch (Exception e) {
    // 处理异常
    while (true) {
    Object msg = redis.execute("XREADGROUP GROUP g1 c1 COUNT 1 STREAMS s1 0");
    if (msg == null) {
    // 没有异常消息,跳出循环
    break;
    }
    try {
    // 说明有异常,再次处理
    handleMessage(msg);
    } catch (Exception e) {
    // 再次出现异常,记录日志,继续循环
    continue;
    }
    }

    }
    }
  • STREAM类型消息队列的XREADGROUP命令特点:

    • 消息可回溯
    • 可以多消费者争抢消息,加快消费速度
    • 可以阻塞读取
    • 没有消息漏读的风险
    • 有消息确认机制,保证消息至少被消费一次

2.5 三种消息队列方式比较

List PubSub Stream
消息持久化 支持 不支持 支持
阻塞队列 支持 支持 支持
消息堆积处理 受限于内存空间,可以利用多消费者加快处理 受限于消费者缓冲区 受限于队列长度,可以利用消费者组提高消费速度,减少堆积
消息确认机制 不支持 不支持 支持
消息回溯 不支持 不支持 支持

2.6 基于Stream的消息队列实现异步秒杀下单

  • 需求:
    • 创建一个Stream类型的消息队列,名为stream.orders
    • 修改之前的秒杀下单Lua脚本,在认定有抢购资格后,直接向stream.orders队列中添加消息,内容包含voucherId、userId、orderId
    • 项目启动时,开启一个线程任务,尝试获取stream.orders队列中的消息,完成下单

2.6.1 seckill.lua

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
-- 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.3 判断用户是否下单 SISMEMBER orderKey userId
if (redis.call('sismember', orderKey, userId) == 1) then
-- 3.4 存在,说明是重复下单,返回2
return 2
end
-- 3.5 扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.6 下单(保存用户)
redis.call('sadd', orderKey, userId)
-- 3.7 发送消息到队列中,XADD stream.orders * k1 v1 k2 v2
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)

return 0

2.6.2 VoucherOrderServiceImpl

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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

@Resource
private ISeckillVoucherService seckillVoucherService;

@Resource
private RedisIdWorker redisIdWorker;

@Resource
private StringRedisTemplate stringRedisTemplate;

@Resource
private RedissonClient redissonClient;

private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}

private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

@PostConstruct
private void init() {
SECKILL_ORDER_EXECUTOR.submit(() -> {new VoucherOrderHandler();});
}

private class VoucherOrderHandler implements Runnable {
String queueName = "stream.orders";
@Override
public void run() {
while (true) {
try {
// 1. 获取消息队列中的订单信息 XREADDROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create(queueName, ReadOffset.lastConsumed())
);
// 2. 判断消息是否成功
if (list.isEmpty() || list == null) {
// 2.1 如果获取失败,说明没有消息,继续下一次循环
continue;
}
// 3. 解析消息中的订单信息
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> values = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, new VoucherOrder(), true);
// 4. 如果获取成功,可以下单
handleVocherOrder(voucherOrder);
// 5. ACK确认 SACK stream.orders g1 id
stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());
} catch (Exception e) {
log.error("处理订单异常", e);
handlePendingList();
}
}
}

private void handlePendingList() {
while (true) {
try {
// 1. 获取pending-list中的订单信息 XREADDROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1),
StreamOffset.create(queueName, ReadOffset.from("0"))
);
// 2. 判断消息是否成功
if (list.isEmpty() || list == null) {
// 2.1 如果获取失败,说明pending-list没有消息,结束循环
break;
}
// 3. 解析消息中的订单信息
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> values = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, new VoucherOrder(), true);
// 4. 如果获取成功,可以下单
handleVocherOrder(voucherOrder);
// 5. ACK确认 SACK stream.orders g1 id
stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());
} catch (Exception e) {
log.error("处理订单异常", e);
}
}
}
}

private void handleVocherOrder(VoucherOrder voucherOrder) {
// 1. 获取用户
Long userId = voucherOrder.getUserId();
// 2. 创建锁对象
RLock lock = redissonClient.getLock("lock:order:" + userId);
// 3. 获取锁
boolean isLock = lock.tryLock();
// 4. 判断是否成功
if (!isLock) {
// 获取失败,返回错误或重试
log.error("不允许重复下单");
return;
}
try {
// 5. 获取代理对象
proxy.createVoucherOrder(voucherOrder);
} finally {
// 6. 释放锁
lock.unlock();
}
}

private IVoucherOrderService proxy;

/**
* 优惠券秒杀下单
* @param voucherId
* @return
*/
@Override
public Result seckillVoucher(Long voucherId) {
// 获取用户
Long userId = UserHolder.getUser().getId();
// 获取订单id
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 ? "库存不足" : "不能重复下单");
}

// 3. 获取代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();

// 4. 返回订单id
return Result.ok(orderId);
}

@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
// 5. 一人一单
// 5.1 查询订单
Long userId = UserHolder.getUser().getId();

int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
// 5.2 判断是否存在
if (count > 0) {
log.error("用户已经购买过了");
return;
}
// 6. 扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // set stock = stock - 1
.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0) // where id = ? and stock > 0
.update();
if (!success) {
// 扣减失败
log.error("库存不足");
return;
}
// 7. 创建订单
save(voucherOrder);
}
}