今天用黑马点评加rocketmq,想测试一下模拟大量用户下单

项目代码见CuSO41108/hmdp_init

遇到了经典的分布式事务数据不一致的问题:

我用ai写的测试用例,因为上次生成5000个token用jmeter压测失败了(暂未解决)

第一次测试:redis中stock为1000,数据库也为1000,THREAD_COUNT = 5000。

结果: 数据库最后剩下9,redis:stock剩下0 (这个时候忘记看voucher_order和seckill_voucher了)

起初是问了ai:

MQ重复消费 的可能性存在。必须保证handleVoucherOrder 和 createVoucherOrder 是 幂等的
已经有幂等校验:

1
2
3
4
5
6
7
8
9
Long count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();

if (count > 0) {

log.error("用户已经下过单了");

return;

}

// 在 handleVoucherOrder 中:

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
boolean isLock = lock.tryLock();

if(!isLock){

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

return; // <-- 致命 BUG #1

}

// 在 createVoucherOrder 中:

if (count > 0) {

log.error("用户已经下过单了");

return; // <-- 致命 BUG #2

}

if (!success) {

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

return; // <-- 致命 BUG #3

}

对于 onMessage 方法的 try 块来说,这个调用没有抛出任何异常,它“成功”执行完了。

catch 块没有捕获到任何东西。

onMessage 方法正常结束。

RocketMQ 收到一个“成功”的 ACK,于是心满意足地将这条消息标记为“已消费”并删除

往以下2个方面修了一下:

  1. 修复静默失败(启用 RocketMQ 重试机制)

  2. 配置死信队列 + 补偿 Redis

之后,进行第二次测试:

测试结果如下,数据库方面,voucher_order新增1073条,seckill_voucher库存从1000->19,redis方

面:seckill:stock:10从1000—>0,seckill:order:10新增1000.

92 个订单 = 1073 (总数) - 981 (正常订单)。这意味着有 92 次重复消费(Bug #1)导致了订单创建。这 92 次重复消费,由于事务隔离性的巧合,它们执行时 count()=0 通过了,但 update stock 失败了(因为库存早就是 0 了),但 save(order) 却成功了。

原因:

@Transactional 没能回滚 save(order) !

于是加了:

ALTER TABLE tb_voucher_order
ADD UNIQUE KEY UK_USER_VOUCHER (user_id, voucher_id); //了解到联合唯一索引

加上这个索引后的效果:

当 RocketMQ 重试消息时,即使你的 count() 检查因为事务隔离级别而失效(读到了旧快照),当 save(order) 尝试插入一个重复的(user_id, voucher_id)组合时,数据库会立即抛出 DuplicateKeyException。这个异常会导致整个事务(包括 update stock****)回滚,然后这个异常会被 RocketMQ 捕获,消息会继续重试,直到最后进入死信队列 (DLQ)。

加了联合唯一索引后,解决了!