前言
本文基于真实代码 ,讲解如何使用 RocketMQ + Redis + 多线程批量更新 实现一个高可靠、防重复、防并发、支持自动重试的固薪分摊系统。
你将学到:
- RocketMQ 消费幂等设计(防止重复计算)
- Redis 分布式锁实战(防止并发冲突)
- 异常自动重试 + 钉钉告警
- 海量数据分页批量更新(无锁高性能)
- 生产级健壮消费模型
一、整体架构设计
1.1 业务流程
- 账单完成 → 发送 MQ 消息
- MQ 消费者接收消息
- 幂等去重(同一账期只允许一个任务进入)
- 检查所有货主是否完成账单
- 加分布式锁 → 执行固薪计算
- 按业绩比例分摊固薪
- 多线程批量更新千万级大表
- 异常自动重试 + 钉钉告警
1.2 技术栈
- RocketMQ 消息队列
- Redisson 分布式锁
- 多线程异步批量更新
- Redis 幂等控制
- 钉钉异常告警
二、RocketMQ 如何实现消费幂等(核心重点)
2.1 为什么需要幂等?
- MQ 重试机制会重复投递
- 网络抖动会导致重复消费
- 固薪计算是汇总计算,重复执行会导致数据翻倍错误
2.2 我们的幂等方案:Redis Pending 占位
核心思路
同一账期同一时间只允许一个任务进入计算 ,其他消息直接跳过,并设置 retrigger 标记,等主任务执行完再重试。
代码实现
java
// 1. 构建幂等Key
String pendingKey = PENDING_KEY_PREFIX + billDateStr;
RBucket<String> pendingBucket = redissonClient.getBucket(pendingKey);
// 2. 尝试占位(2分钟TTL)
boolean acquired = pendingBucket.trySet("1", PENDING_TTL_SECONDS, TimeUnit.SECONDS);
if (!acquired) {
// 3. 未获取到 → 设置重触发标记,直接返回
RBucket<String> retriggerBucket = redissonClient.getBucket(triggerKey);
retriggerBucket.trySet("1", RETRIGGER_TTL_SECONDS, TimeUnit.SECONDS);
return;
}
幂等保障机制
trySet原子操作,只有一个请求能成功- TTL 自动释放,避免死锁
- 重复消息不阻塞,不报错,不重复执行
- 支持重触发机制,保证最终一致性
2.3 幂等Key设计规则
java
private static final String PENDING_KEY_PREFIX = "bms:fixed:salary:pending:";
- 按 账期(yyyy-MM-dd) 维度隔离
- 天然支持多账期并行计算
- 不互相干扰
三、Redis 分布式锁如何加锁(生产级标准)
3.1 为什么需要分布式锁?
- 多实例部署,防止多节点同时执行
- 防止并发修改同批数据
- 防止重复汇总计算导致数据错误
3.2 Redisson 锁实现(可重入、防死锁)
java
RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + billDateStr);
boolean isLocked = lock.tryLock(30, TimeUnit.SECONDS);
if (!isLocked) {
throw new ServiceException("锁被占用,跳过本次计算");
}
try {
// 执行业务逻辑
} finally {
// 一定要释放锁!
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
3.3 锁关键特性
- tryLock(30s):30秒获取不到直接放弃,避免阻塞
- 自动续期:Redisson 看门狗机制
- 防死锁:任务异常也能释放
- 可重入:同一线程可重复加锁
四、异常如何重试?(全自动指数退避重试)
4.1 重试策略
- 最大重试次数:3次
- 重试间隔:2分钟 → 5分钟 → 10分钟
- 异常自动设置
retrigger重触发 - 超过最大次数放弃,等待MQ重新投递
4.2 代码实现
java
if (retryCount > MAX_RETRY_COUNT) {
redissonClient.getBucket(triggerKey).set("1", RETRIGGER_TTL_SECONDS, TimeUnit.SECONDS);
break;
}
if (retryCount > 0) {
long delayMinutes = RETRY_DELAY_MINUTES[retryCount - 1];
Thread.sleep(delayMinutes * 60 * 1000L);
}
4.3 异常兜底机制
java
catch (Exception e) {
// 1. 记录日志
log.error("固薪计算异常", e);
// 2. 设置重触发
redissonClient.getBucket(triggerKey).set("1", RETRIGGER_TTL_SECONDS, TimeUnit.SECONDS);
// 3. 钉钉发送告警
SendDingdingMessageUtils.sendTextMessageAtAll(DingdingWebHookUrlEnum.BMS_B2C, message);
}
五、完整消费模型(生产级标准代码)
5.1 MQ 消费者
java
@Override
public ConsumeResult consume(MessageView messageView) {
String messageBody = StandardCharsets.UTF_8.decode(messageView.getBody()).toString();
try {
TenantContextHolder.setTenantId(1);
B2cFixedSalaryCalculateDto context = JsonUtils.parseObject(messageBody, B2cFixedSalaryCalculateDto.class);
if (context != null) {
b2cFixedSalaryCalculateService.fixedSalaryCalculate(context);
}
} catch (Exception e) {
log.error("消息处理失败", e);
SendDingdingMessageUtils.sendTextMessageAtAll(DingdingWebHookUrlEnum.BMS_B2C, "异常");
return ConsumeResult.FAILURE;
} finally {
TenantContextHolder.clear();
}
return ConsumeResult.SUCCESS;
}
5.2 消息发送
java
public void sendFixedSalaryCalculateMessage(String customerId, LocalDate billDate) {
B2cFixedSalaryCalculateDto dto = new B2cFixedSalaryCalculateDto();
dto.setCustomerId(customerId);
dto.setBillDate(billDate);
bmsRocketMQSendMessageService.syncSendNormalMessage(
BmsMQConstants.BMS_FIXED_SALARY_TOPIC,
JSONObject.toJSONString(dto)
);
}
六、高并发大表更新(千万级无锁批量更新)
6.1 为什么用分页Keyset更新?
- 避免大事务
- 避免锁表
- 支持千万级数据
- 内存占用极低
6.2 代码实现
java
private void batchUpdateFixedSalary(LocalDate billDate, String customerId,
String warehouseId, BigDecimal fixedSalary) {
Long lastId = 0L;
List<B2CServiceFeeDo> feeDoList;
do {
// 1. Keyset分页查询
feeDoList = b2CServiceFeeDoRepository.selectIdsByDateAndCustomer(
billDate, customerId, warehouseId, lastId, BATCH_SIZE);
if (CollectionUtils.isEmpty(feeDoList)) break;
// 2. 计算利润
feeDoList.forEach(feeDo -> {
feeDo.setLaborCostFixedSalary(fixedSalary);
newB2cService.calculateLaborProfit(feeDo);
newB2cService.calculateOrderProfit(feeDo);
});
// 3. 批量更新
b2CServiceFeeDoRepository.batchUpdateFixedSalaryAndOrderProfit(feeDoList);
lastId = feeDoList.get(feeDoList.size() - 1).getId();
} while (feeDoList.size() >= BATCH_SIZE);
}
七、生产保障三板斧(必看)
7.1 幂等三板斧
- Redis Pending 占位
- 分布式锁防止并发
- 重触发机制保证最终一致
7.2 异常保障三板斧
- try-catch 捕获所有异常
- 自动指数退避重试
- 钉钉实时告警
7.3 性能保障三板斧
- 异步多线程并行更新
- Keyset 分页无锁更新
- 批量更新减少IO
八、总结(最核心的3句话)
- MQ 幂等 = Redis 占位 + TTL 自动释放
- 分布式锁 = Redisson 可重入锁 + 防死锁
- 异常重试 = 指数退避 + 重触发标记 + 最终一致性
这套模型可以直接用于:
- 结算汇总
- 业绩分摊
- 成本计算
- 全量统计
- 定时任务