返利app消息队列应用:基于RabbitMQ的异步佣金结算系统设计

返利app消息队列应用:基于RabbitMQ的异步佣金结算系统设计

大家好,我是省赚客APP研发者阿可!在省赚客APP(juwatech.cn)中,用户完成电商订单后,平台需根据联盟回调数据进行佣金结算。由于回调频率高、第三方接口不稳定、数据库写入耗时等因素,若采用同步处理极易造成线程阻塞与系统雪崩。为此,我们引入 RabbitMQ 构建异步佣金结算系统,实现削峰填谷、失败重试与最终一致性。本文将从消息模型、生产消费逻辑到可靠性保障,结合核心代码详解实现细节。

消息模型设计:事件驱动架构

我们将佣金结算拆解为两个核心事件:

  • CommissionPendingEvent:订单确认有效,待结算;
  • CommissionRetryEvent:结算失败,需延时重试。

对应 RabbitMQ 拓扑如下:

java 复制代码
@Configuration
public class RabbitMqConfig {

    public static final String COMMISSION_EXCHANGE = "juwatech.commission.exchange";
    public static final String PENDING_QUEUE = "juwatech.commission.pending.queue";
    public static final String RETRY_QUEUE = "juwatech.commission.retry.queue";
    public static final String DLX_EXCHANGE = "juwatech.commission.dlx.exchange";

    @Bean
    public DirectExchange commissionExchange() {
        return new DirectExchange(COMMISSION_EXCHANGE, true, false);
    }

    @Bean
    public Queue pendingQueue() {
        return QueueBuilder.durable(PENDING_QUEUE)
            .deadLetterExchange(DLX_EXCHANGE)
            .deadLetterRoutingKey("retry")
            .ttl(30000) // 30秒未消费则进入重试队列
            .build();
    }

    @Bean
    public Queue retryQueue() {
        return QueueBuilder.durable(RETRY_QUEUE)
            .deadLetterExchange(COMMISSION_EXCHANGE)
            .deadLetterRoutingKey("pending")
            .ttl(600000) // 10分钟延时重试
            .build();
    }

    @Bean
    public DirectExchange dlxExchange() {
        return new DirectExchange(DLX_EXCHANGE, true, false);
    }

    @Bean
    public Binding pendingBinding() {
        return BindingBuilder.bind(pendingQueue()).to(commissionExchange()).with("pending");
    }

    @Bean
    public Binding retryBinding() {
        return BindingBuilder.bind(retryQueue()).to(dlxExchange()).with("retry");
    }
}

消息生产:幂等投递保障

在回调网关中,仅当订单首次接收时投递消息:

java 复制代码
@Service
public class CommissionEventPublisher {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private CommissionRecordMapper commissionRecordMapper;

    public void publishIfAbsent(String tradeId, CommissionPendingEvent event) {
        // 幂等检查:防止重复投递
        if (commissionRecordMapper.existsByTradeId(tradeId)) {
            return;
        }
        // 插入"待处理"记录 + 发送消息(本地事务)
        transactionTemplate.execute(status -> {
            commissionRecordMapper.insertAsPending(tradeId, event.getUserId(), event.getAmount());
            rabbitTemplate.convertAndSend(
                RabbitMqConfig.COMMISSION_EXCHANGE,
                "pending",
                event,
                message -> {
                    message.getMessageProperties().setMessageId(tradeId); // 用于去重
                    return message;
                }
            );
            return null;
        });
    }
}

消息消费:可靠处理与异常重试

消费者启用手动 ACK,确保处理成功才确认:

java 复制代码
@Component
@RabbitListener(queues = RabbitMqConfig.PENDING_QUEUE)
public class CommissionConsumer {

    @Autowired
    private AccountService accountService;

    @Autowired
    private CommissionRecordMapper commissionRecordMapper;

    @RabbitHandler
    public void handle(CommissionPendingEvent event, Channel channel, Message message) throws IOException {
        try {
            // 再次幂等校验(防重复消费)
            if (commissionRecordMapper.isProcessed(event.getTradeId())) {
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
                return;
            }

            // 执行佣金发放
            accountService.credit(event.getUserId(), event.getAmount(), "返利入账");

            // 更新状态为成功
            commissionRecordMapper.markAsSuccess(event.getTradeId());

            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (InsufficientBalanceException | UserNotFoundException e) {
            // 业务错误:直接拒绝,不重试
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
            log.error("Commission failed with unrecoverable error", e);
        } catch (Exception e) {
            // 系统异常:拒绝并重回队列(最多3次后进入DLQ)
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            log.warn("Commission processing failed, will retry", e);
        }
    }
}

死信队列监控与人工干预

当消息重试超过阈值(如3次),进入死信队列,由运维后台处理:

java 复制代码
@RabbitListener(queues = "juwatech.commission.dlq")
public void handleDeadLetter(CommissionPendingEvent event) {
    // 记录至DB,供人工审核
    deadLetterLogMapper.insert(
        DeadLetterLog.builder()
            .tradeId(event.getTradeId())
            .payload(JsonUtil.toJson(event))
            .reason("Max retries exceeded")
            .build()
    );
}

消息追踪与监控

通过 Micrometer 上报消费延迟与失败率:

java 复制代码
private final Counter consumeFailureCounter;
private final Timer consumeTimer;

public void handle(...) {
    Timer.Sample sample = consumeTimer.start();
    try {
        // ...处理逻辑
    } catch (Exception e) {
        consumeFailureCounter.increment();
        throw e;
    } finally {
        sample.stop();
    }
}

Prometheus 抓取指标后,在 Grafana 中配置告警:
rate(juwatech_commission_consume_failure_total[5m]) > 0.05

连接与资源管理

使用 Spring Boot 自动配置连接池,避免资源泄漏:

yaml 复制代码
spring:
  rabbitmq:
    host: rabbitmq.juwatech.cn
    port: 5672
    username: juwatech
    password: ${RABBITMQ_PASSWORD}
    listener:
      simple:
        acknowledge-mode: manual
        concurrency: 5
        max-concurrency: 20
        prefetch: 10

本文著作权归聚娃科技省赚客app开发者团队,转载请注明出处!

相关推荐
用户83071968408210 小时前
RabbitMQ vs RocketMQ 事务大对决:一个在“裸奔”,一个在“开挂”?
后端·rabbitmq·rocketmq
初次攀爬者2 天前
RabbitMQ的消息模式和高级特性
后端·消息队列·rabbitmq
初次攀爬者4 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
让我上个超影吧5 天前
消息队列——RabbitMQ(高级)
java·rabbitmq
塔中妖5 天前
Windows 安装 RabbitMQ 详细教程(含 Erlang 环境配置)
windows·rabbitmq·erlang
断手当码农5 天前
Redis 实现分布式锁的三种方式
数据库·redis·分布式
初次攀爬者5 天前
Redis分布式锁实现的三种方式-基于setnx,lua脚本和Redisson
redis·分布式·后端
业精于勤_荒于稀5 天前
物流订单系统99.99%可用性全链路容灾体系落地操作手册
分布式
Ronin3055 天前
信道管理模块和异步线程模块
开发语言·c++·rabbitmq·异步线程·信道管理
Asher05095 天前
Hadoop核心技术与实战指南
大数据·hadoop·分布式