Q: 当前实现会对业务异常(如库存不足)抛出RuntimeException,导致消息重复投递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
createVoucherOrder  中

boolean success = seckillVoucherService.update()

.setSql("stock = stock - 1")

.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock",0)

.update();

if (!success) {

*log*.error("库存不足!");

//return;

throw new RuntimeException("数据库库存不足");

}

//创建订单

save(voucherOrder);

A:

  • 不可恢复错误(如重复订单):记录日志并正常返回
    可恢复错误(如数据库超时):抛出异常触发重试

Q:

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
public void handleVoucherOrder(VoucherOrder voucherOrder) {

//1.获取用户

Long userId = voucherOrder.getUserId();

//创建锁对象

RLock lock = redissonClient.getLock("lock:order:" + userId);

//获取锁

boolean isLock = lock.tryLock();

if(!isLock){

*log*.error("不允许重复下单");

//return;

throw new RuntimeException("获取锁失败,不允许重复下单");

}

try {

//获取代理对象

IVoucherOrderService proxy = (IVoucherOrderService) AopContext.*currentProxy*();

proxy.createVoucherOrder(voucherOrder);

} finally {

lock.unlock();

}

}

handleVoucherOrder中的

redisson锁与redis+lua重复了吗?有必要删除吗?

A:有,否则相当于做了三重校验:Redis Lua → Redisson 锁 → 数据库唯一性,性能会打折

Q:幂等性保障

  • 目前在 createVoucherOrder 里只是查 count 判断是否下过单。

  • 更推荐在 数据库表层面加唯一索引:

    1
    ALTER TABLE voucher_order ADD UNIQUE KEY uk_user_voucher (user_id,     voucher_id);

    这样即使 RocketMQ 消息重复投递也能防止重复下单。

Q:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
boolean success = seckillVoucherService.update()

.setSql("stock = stock - 1")

.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock",0)

.update();

if (!success) {

log.error("库存不足!");

//return;

throw new RuntimeException("数据库库存不足");

}

这段以后并发高了怎么解决?

Q:

  • RocketMQ 在极端情况下可能会“重复投递”,所以消费端必须幂等。

    已经在 DB 里有 query().eq(“user_id”, userId).eq(“voucher_id”, …) 判断,但更好的办法是

    建唯一索引,直接用 DB 保证幂等。

Q:

  • 现在 throw new RuntimeException,RocketMQ 会重试。

    但要注意:如果是逻辑性错误(比如用户已下过单),重试是没意义的,会死循环。

    建议只对系统性异常(DB 连接超时、Redis 挂了)抛异常,让 MQ 重试;如果是业务性错误,直

    接 log.warn 并吞掉

Q.RocketMQ监听器抛出异常.消息被送回消费者,消费者再次尝试处理。由于相同原因(库存仍为0)失败,抛出异常,再次被重新安排。这就形成了一个无限循环

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void onMessage(VoucherOrder voucherOrder) {

log.info("接收到秒杀订单信息:{}", voucherOrder);

try {
((VoucherOrderServiceImpl) voucherOrderService).handleVoucherOrder(voucherOrder);
}catch (Exception e){
log.error("处理秒杀订单失败!订单详情: {}", voucherOrder, e);
// 抛出异常,触发 RocketMQ 的自动重试机制
//删掉这句 throw new RuntimeException("消费失败,触发重试", e);
}
}