实战:RocketMQ 幂等 + Redis 分布式锁 + 异常重试 保姆级教程

前言

本文基于真实代码 ,讲解如何使用 RocketMQ + Redis + 多线程批量更新 实现一个高可靠、防重复、防并发、支持自动重试的固薪分摊系统。

你将学到:

  1. RocketMQ 消费幂等设计(防止重复计算)
  2. Redis 分布式锁实战(防止并发冲突)
  3. 异常自动重试 + 钉钉告警
  4. 海量数据分页批量更新(无锁高性能)
  5. 生产级健壮消费模型

一、整体架构设计

1.1 业务流程

  1. 账单完成 → 发送 MQ 消息
  2. MQ 消费者接收消息
  3. 幂等去重(同一账期只允许一个任务进入)
  4. 检查所有货主是否完成账单
  5. 加分布式锁 → 执行固薪计算
  6. 按业绩比例分摊固薪
  7. 多线程批量更新千万级大表
  8. 异常自动重试 + 钉钉告警

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;
}

幂等保障机制

  1. trySet 原子操作,只有一个请求能成功
  2. TTL 自动释放,避免死锁
  3. 重复消息不阻塞,不报错,不重复执行
  4. 支持重触发机制,保证最终一致性

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 幂等三板斧

  1. Redis Pending 占位
  2. 分布式锁防止并发
  3. 重触发机制保证最终一致

7.2 异常保障三板斧

  1. try-catch 捕获所有异常
  2. 自动指数退避重试
  3. 钉钉实时告警

7.3 性能保障三板斧

  1. 异步多线程并行更新
  2. Keyset 分页无锁更新
  3. 批量更新减少IO

八、总结(最核心的3句话)

  1. MQ 幂等 = Redis 占位 + TTL 自动释放
  2. 分布式锁 = Redisson 可重入锁 + 防死锁
  3. 异常重试 = 指数退避 + 重触发标记 + 最终一致性

这套模型可以直接用于:

  • 结算汇总
  • 业绩分摊
  • 成本计算
  • 全量统计
  • 定时任务
相关推荐
草莓熊Lotso7 小时前
【Redis 进阶】主从复制深度解析:从配置落地到 PSYNC 同步原理
linux·开发语言·网络·数据库·redis·缓存·php
灯澜忆梦9 小时前
【Redis中间件】#2 | 基础数据类型语法
redis·后端
天天喝旺仔10 小时前
深入理解 JWT:从 Token 结构、签名机制到前后端鉴权实战
redis·安全·spring·微服务·https
一只小李郁vickie10 小时前
Redis 集群部署手册(3主3从)
运维·redis
懂软件的胡子个哥10 小时前
微信工单系统如何基于 WechatApi 做消息分流和状态流转
运维·分布式·微信·架构·企业微信
Gl�ria1 天前
Redis 高可用架构对比
数据库·redis
Zenova EdgeOS1 天前
Python 工业边缘 redis-py 异步实战
开发语言·redis·python
hanchenxing1 天前
前端异步任务三方案:Celery vs Redis Stream vs BullMQ 实战对比消息队列
前端·数据库·redis·异步任务·方案对比
九皇叔叔1 天前
从本地事务到分布式事务
java·分布式·分布式事务·cap·base
hey you~1 天前
云客服多渠道统一接入,消息队列技术实现方案
kafka·消息队列·rocketmq·系统集成·云客服·多渠道接入·接口对接