SpringBoot + 消息去重 + 全局唯一 ID:高并发下确保消息仅处理一次

前言

在高并发的互联网应用中,我们经常会遇到这样的场景:用户提交订单、发起支付、积分兑换等操作。然而,由于网络不稳定、系统重试机制或用户误操作等原因,可能会导致同一条消息被多次处理,从而引发一系列问题:订单重复创建、支付重复扣款、积分重复发放等等。

今天,我就来跟大家分享一个在高并发场景下确保消息仅处理一次的经典解决方案——基于SpringBoot + 消息去重 + 全局唯一ID的技术组合。

问题场景分析

让我们先来看几个典型的重复消息处理场景:

1. 订单重复创建

当用户在电商平台上下单时,由于网络波动,客户端可能连续发送了几次相同的下单请求。如果没有有效的去重机制,系统可能会创建多个相同的订单,给商家和用户都带来困扰。

2. 支付重复扣款

在支付场景中,如果支付网关因为网络超时而重复发送支付成功通知,后端系统若没有去重处理,就可能导致用户被重复扣款,造成严重的用户体验问题。

3. 积分重复发放

在营销活动中,用户完成某项任务后获得积分奖励。如果因为网络原因导致任务完成消息被重复发送,用户可能会获得多倍的积分,这对活动的公平性造成了冲击。

传统解决方案的局限

面对这些问题,很多同学可能会想到一些传统解决方案:

1. 数据库约束

最直接的想法是在数据库层面设置唯一索引,比如订单表的订单号、支付记录表的流水号等。这种方法确实能防止数据重复,但它只能在数据持久化阶段起作用,业务逻辑可能已经执行了一部分,造成不必要的资源消耗。

2. 业务逻辑判断

在业务层面对关键字段进行判断,如果已经存在就不执行相应操作。但这种方式在高并发场景下容易出现竞态条件,导致判断失效。

3. 分布式锁

使用分布式锁来确保同一时间只有一个请求能处理特定的业务。这种方法虽然能解决问题,但实现复杂,且会影响系统性能。

我们的解决方案

经过多年的实践和优化,我总结出一套高效的解决方案:全局唯一ID生成 + Redis原子操作去重 + 业务幂等性设计

1. 全局唯一ID的重要性

首先,我们需要一个可靠的全局唯一ID生成策略。在分布式系统中,UUID虽然简单易用,但长度较长且无序,不利于数据库索引优化。相比之下,雪花算法(Snowflake)是一个更好的选择:

  • 趋势递增:生成的ID大致按时间顺序递增,有利于数据库索引性能
  • 全局唯一:结合时间戳、机器ID和序列号,确保在分布式环境下不重复
  • 高效生成:本地生成,无需网络请求,性能极高

2. Redis实现高效去重

Redis的原子操作特性使其成为实现消息去重的理想选择。我们利用Redis的SET key value NX EX seconds命令,这个命令具有以下特点:

  • NX:只有key不存在时才设置
  • EX:设置过期时间,避免内存无限增长
  • 原子性:整个操作是原子的,天然支持并发安全

3. 业务幂等性设计

在实现技术去重的同时,业务逻辑本身也需要设计为幂等的,即多次执行同一操作产生的结果与执行一次的结果相同。

SpringBoot实战实现

下面,我通过一个完整的例子来展示如何在SpringBoot中实现这套方案。

1. 添加依赖

在pom.xml中添加必要的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.10</version>
</dependency>

2. 全局ID生成器

@Component
public class IdGenerator {
    
    private final Snowflake snowflake;
    
    public IdGenerator() {
        this.snowflake = IdUtil.getSnowflake();
    }
    
    public String generateMessageId(String businessPrefix) {
        return businessPrefix + "_" + String.valueOf(snowflake.nextId());
    }
}

3. 消息去重服务

@Service
public class MessageDeduplicationService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String DEDUPE_KEY_PREFIX = "msg_dedup:";
    
    public boolean isProcessed(String messageId) {
        String key = DEDUPE_KEY_PREFIX + messageId;
        Boolean hasKey = redisTemplate.hasKey(key);
        return hasKey != null && hasKey;
    }
    
    public boolean markAsProcessed(String messageId) {
        String key = DEDUPE_KEY_PREFIX + messageId;
        // 使用setIfAbsent实现原子性操作
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, 
            System.currentTimeMillis(), 7, TimeUnit.DAYS);
        return result != null && result;
    }
}

4. 消息处理服务

@Service
public class MessageProcessingService {
    
    @Autowired
    private MessageDeduplicationService deduplicationService;
    
    @Autowired
    private IdGenerator idGenerator;
    
    public boolean processMessageWithDedup(String businessKey, Object messageData) {
        String messageId = idGenerator.generateMessageId(businessKey);
        
        // 检查消息是否已处理
        if (deduplicationService.isProcessed(messageId)) {
            return true; // 消息已处理,返回成功
        }
        
        // 尝试标记消息为已处理
        if (!deduplicationService.markAsProcessed(messageId)) {
            return false; // 标记失败,可能是并发冲突
        }
        
        try {
            // 执行实际业务逻辑
            executeBusinessLogic(businessKey, messageData);
            return true;
        } catch (Exception e) {
            // 业务执行失败,可以根据需要决定是否移除去重标记
            return false;
        }
    }
    
    private void executeBusinessLogic(String businessKey, Object messageData) {
        // 实现具体的业务逻辑
    }
}

性能优化与最佳实践

1. 过期时间设置

合理设置Redis键的过期时间非常重要。时间太短可能导致正常的延迟消息被误认为重复,时间太长则会占用过多内存。一般建议根据业务特点设置3-7天。

2. 批量处理优化

对于批量消息处理场景,可以考虑批量去重检查,减少Redis交互次数:

public List<String> filterUnprocessed(List<String> messageIds) {
    List<String> unprocessed = new ArrayList<>();
    for (String messageId : messageIds) {
        if (!isProcessed(messageId)) {
            unprocessed.add(messageId);
        }
    }
    return unprocessed;
}

3. Redis连接池配置

合理配置Redis连接池参数,确保在高并发场景下的性能:

spring:
  redis:
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5

4. 异常处理策略

当Redis不可用时,需要有合适的降级策略,比如记录日志并允许消息继续处理,或者暂时停止服务等待Redis恢复。

总结

通过今天的分享,我们了解了在高并发场景下如何确保消息仅被处理一次的完整解决方案。这套方案的核心在于:

  1. 全局唯一ID:为每条消息生成唯一的标识符
  2. Redis原子操作:利用Redis的原子性确保去重的并发安全性
  3. 业务幂等设计:确保业务逻辑本身具有幂等性

这套方案已经在多个高并发项目中得到验证,能够有效防止重复消息处理带来的各种问题。在实际应用中,还需要根据具体的业务场景进行调整和优化。

如果你觉得这篇文章对你有帮助,欢迎关注我们的公众号"服务端技术精选",获取更多实用的技术干货!


标题:SpringBoot + 消息去重 + 全局唯一 ID:高并发下确保消息仅处理一次
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/02/07/1770270583048.html

    0 评论
avatar