实战: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. 异常重试 = 指数退避 + 重触发标记 + 最终一致性

这套模型可以直接用于:

  • 结算汇总
  • 业绩分摊
  • 成本计算
  • 全量统计
  • 定时任务
相关推荐
basketball6161 小时前
Redis基础:3. Redis 持久化(重要)
redis·bootstrap·mybatis
电商API_180079052472 小时前
高可用采集架构:分布式定时抓取淘宝商品详情项目设计
大数据·分布式·架构·数据挖掘·网络爬虫
heimeiyingwang2 小时前
【架构实战】线程池设计:高并发系统的资源管理艺术
分布式·架构
一个骇客2 小时前
分布式批处理:当你的单机脚本跑了一天一夜还没出结果
分布式·架构
Juicedata2 小时前
JuiceFS 1.4|大规模元数据操作优化:批量删除、克隆与 Redis 缓存全解析
数据库·redis·缓存
墨痕无声2 小时前
Redis集群
redis
小蒋学算法3 小时前
redis分布式锁实现
数据库·redis·分布式
珠***格3 小时前
四可装置核心技术:高精度采集、边缘计算、协议自适应
大数据·人工智能·分布式·能源·边缘计算