SpringBoot + RabbitMQ + MongoDB + Redis:高性能高并发高可用的半导体生产工厂数据处理落地实践

438 阅读26分钟

目录

引言:半导体生产的数字化挑战

一、 业务场景与核心挑战深度剖析

二、 总体架构设计

1.1 架构示意图

1.2 核心组件职责

三、 核心难题与设计:双写一致性保障

3.1 最终一致性方案:通过消费端幂等 + 消息重试

3.2 代码片段示例 (SpringBoot)

四、 核心难题与设计:百万级数据处理能力

3.1 高性能写入设计

3.2 高性能查询设计

五、 高可用性设计

六、核心组件选型与配置深度解析

七、双写一致性保障:终极解决方案与实践

幂等消费:

processData方法:先DB,后Cache (Pipeline优化)

最终补偿机制 

八、MongoDB与Redis数据结构设计示例

MongoDB Document设计:

Redis数据结构设计:

八(附)、 性能优化策略

九、详细代码放出

1. SpringBoot-Ingestion-Service (接入服务)

2. SpringBoot-Process-Service (处理服务)

3. MongoDB 数据模型与索引

4. 补偿Job代码 (CompensationJob.java)

九(附)、核心总结

十、SpringBoot + RabbitMQ + MongoDB + Redis 半导体生产数据处理平台核心总结

一、 核心架构思想:分层解耦,各司其职

二、 核心难题解决方案:双写一致性保障

三、 百万级性能优化关键点


引言:半导体生产的数字化挑战

在现代半导体制造中,晶圆经过数百道工序,每台设备每秒都可能产生数以千计的数据点(温度、压力、良率等)。这些数据不仅是生产监控的依据,更是工艺优化、良率分析和故障预测的宝贵资产。构建一个能实时处理海量数据、保证数据一致性、并支持高并发查询的系统,是数字化工厂的核心基础。

本实践将深入探讨如何利用SpringBoot 作为快速开发框架,RabbitMQ 进行异步解耦与流量削峰,MongoDB 存储海量非结构化时序数据,并结合Redis 提供高速缓存与实时状态查询,最终打造一个稳定、高效的数据处理平台。

一、 业务场景与核心挑战深度剖析

1.1 半导体生产数据特性:

  • 海量性 (Volume) :一台蚀刻机每秒可产生数KB到数MB的数据,全厂日数据量可达TB级。
  • 高速性 (Velocity) :数据上报频率极高,要求接入层有百万级TPS的处理能力。
  • 多样性 (Variety) :数据格式复杂,包括时序数据(传感器读数)、事件数据(报警、状态变更)、图片(AOI检测)等。
  • 价值性 (Value) :数据是良率分析和工艺优化的核心,要求数据不丢失、不错乱,查询高效、准确

1.2 核心挑战:

  1. 高并发写入:如何承接数据洪流,避免数据丢失或服务雪崩?
  2. 双写一致性:如何保证MongoDB(权威数据源)和Redis(缓存)之间的数据最终一致?这是系统设计的重中之重
  3. 海量数据存储与查询:如何设计MongoDB分片以支持水平扩展?如何设计Redis数据结构以支持复杂实时查询?
  4. 系统高可用:任何中间件或服务都不能成为单点故障。

二、 总体架构设计

我们的核心目标是:可靠地接收、处理和存储每秒可能产生的数十万条数据,并保证应用层在查询最新状态和历史数据时的绝对一致性。

1.1 架构示意图

​编辑

1.2 核心组件职责

  • SpringBoot接入服务 (Data-Ingestion-Service) :

    • 提供高性能HTTP接口或MQTT客户端,接收生产设备上报的数据。
    • 完成数据清洗、格式校验、基础格式化。
    • 将合法消息异步发送至RabbitMQ,立即响应设备,确保高吞吐量。
  • RabbitMQ集群:

    • 作为系统的异步缓冲与削峰填谷的核心组件。设置持久化、镜像队列,确保消息不丢失。
    • 将数据生产与消费解耦,允许消费者动态扩展。
  • 数据消费服务 (Data-Process-Service) :

    • 核心中的核心,负责从RabbitMQ消费消息。
    • 肩负双写重任:将数据持久化至MongoDB,同时更新Redis中的最新状态或聚合数据。
    • 实现消费者组模式,多实例并行消费,提升处理能力。
  • MongoDB分片集群:

    • 存储全量历史生产数据。利用其灵活的文档模型,轻松存储不同设备类型的异构数据。
    • 通过分片(Sharding)机制,按设备ID或时间戳进行分片,实现容量的水平扩展和高并发写入。
  • Redis集群:

    • 存储设备最新状态常用统计结果生产计数器等。
    • 提供极低的查询延迟,支撑实时监控大屏、设备实时状态查询等场景。
    • 使用Redis的原子操作和复杂数据结构,实现高效计算。

三、 核心难题与设计:双写一致性保障

双写(MongoDB和Redis)最关键的挑战是如何保证原子性:即两者要么都成功,要么都失败。在网络抖动或服务重启时,避免数据不一致。

3.1 最终一致性方案:通过消费端幂等 + 消息重试

我们不采用强一致性分布式事务(如2PC,性能低下),而是依靠消息队列的可靠性投递和消费者的幂等设计来实现最终一致性。

步骤详解:

  1. 可靠消息生产:接入服务发送消息到RabbitMQ时,开启publisher-confirm机制,确保消息已持久化到Broker。

  2. 消费端幂等处理

    • 每条消息携带一个唯一业务ID(如deviceId_timestamp或全局唯一ID)。
    • 在处理消息前,消费者先检查Redis中是否存在该ID的处理记录(例如:SETNX id_12345 "processing")。
    • 如果已存在,说明是重复消息,直接确认消息并跳过处理。
  3. 双写执行顺序

    • 先写Redis?先写MongoDB? 这是一个关键选择。

    • 我们的选择:先写MongoDB,再更新Redis。

      • 原因:MongoDB是数据的权威来源(Source of Truth)。如果先写Redis成功但写MongoDB失败,会导致脏数据永久存在于缓存中,难以清理。反之,如果先写MongoDB成功但更新Redis失败,我们可以通过消息重试机制再次更新Redis,或者通过其他补偿机制(如从MongoDB回填Redis)来最终达到一致。
  4. 异常处理与重试

    • 将双写操作放在一个本地事务中(例如使用@Transactional),但这只能保证数据库操作,无法保证Redis。
    • 更佳实践:将“写MongoDB”和“更新Redis”视为一个整体业务逻辑。任何一步失败,则抛出异常。
    • RabbitMQ消费者默认采用自动确认模式。我们应改为手动确认。只有业务处理成功,才basicAck确认消息。如果抛出异常,则basicNack并让消息重新入队(或进入死信队列),等待重试。
  5. 补偿机制

    • 对于重试多次仍失败的消息,会进入死信队列。
    • 启动一个后台任务监控死信队列,进行告警和人工干预。或者尝试解析失败原因,进行特定逻辑的补偿。

3.2 代码片段示例 (SpringBoot)

@Service
@Slf4j
public class DataProcessService {

    @Autowired
    private MongoTemplate mongoTemplate;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @RabbitListener(queues = "fab.data.queue")
    @Transactional(rollbackFor = Exception.class) // 保证Mongo操作的事务性(需MongoDB 4.0+)
    public void handleMessage(DeviceDataMessage message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
        String messageId = message.getMessageId();
        // 1. 幂等检查
        Boolean isAbsent = redisTemplate.opsForValue().setIfAbsent("MSG_ID:" + messageId, "PROCESSED", Duration.ofMinutes(10));
        if (Boolean.FALSE.equals(isAbsent)) {
            log.warn("Duplicate message received, id: {}", messageId);
            channel.basicAck(tag, false); // 确认消息
            return;
        }

        try {
            // 2. 先写MongoDB
            DeviceDataDocument doc = convertToDocument(message);
            mongoTemplate.insert(doc);

            // 3. 再更新Redis状态 (例如:更新设备最新读数)
            String redisKey = "device:status:" + message.getDeviceId();
            redisTemplate.opsForHash().put(redisKey, "latest_temperature", message.getTemperature());
            redisTemplate.opsForHash().put(redisKey, "latest_timestamp", message.getTimestamp());
            // ... 其他状态更新

            // 4. 一切成功,确认消息
            channel.basicAck(tag, false);
        } catch (Exception e) {
            log.error("Error processing message: " + messageId, e);
            // 删除幂等键,允许重试(可选,取决于业务)
            redisTemplate.delete("MSG_ID:" + messageId);
            // 否认消息,并要求重新入队
            channel.basicNack(tag, false, true);
            // 抛出异常,让@Transactional回滚MongoDB操作
            throw new RuntimeException(e);
        }
    }
}

四、 核心难题与设计:百万级数据处理能力

3.1 高性能写入设计

  • RabbitMQ优化

    • 使用镜像队列,保证高可用。
    • 发送端开启批量确认异步发送,提升吞吐量。
    • 根据业务规划多个Exchange和Queue,实现业务隔离,避免相互影响。
  • 消费者并行性

    • @RabbitListener注解中配置concurrency参数,启动多个消费者线程。@RabbitListener(queues = "fab.data.queue", concurrency = "10-20")
    • 部署多个Data-Process-Service实例,形成消费者组,水平扩展消费能力。
  • MongoDB批量写入

    • 消费者在处理时,可以积累一批消息(如1000条或攒够5ms)后,使用insertMany进行批量写入,大幅减少网络IO和数据库开销。
  • Redis管道(Pipeline)

    • 在更新Redis时,同样可以采用Pipeline技术,将多个命令一次性发送,减少RTT次数。

3.2 高性能查询设计

  • MongoDB查询优化

    • 为查询条件建立复合索引(如{deviceId: 1, timestamp: -1})。
    • 使用分片键作为常用查询条件,避免 scatter-gather 查询。
    • 使用 Projection 只返回必要的字段。
    • 对历史数据集合使用TTL索引,自动过期旧数据(如果需要)。
  • Redis物化视图

    • 不要只是简单缓存原始数据。利用Redis强大的数据结构预计算常用查询。
    • 示例:使用Sorted Set (ZSET) 维护设备产量日排行。每次处理完一条良品数据后,执行ZINCRBY day:20231030:output 1 deviceId
    • 示例:使用HyperLogLog (PFADD) 统计每日活跃设备数,占用极小空间。
    • 示例:使用Hash (HASH) 存储设备完整实时状态,一次查询即可获取所有属性。

五、 高可用性设计

  1. 无单点故障

    • 所有中间件均采用集群模式部署:RabbitMQ镜像集群、MongoDB分片副本集、Redis Cluster模式。任何单个节点宕机都不会影响整体服务。
  2. 服务本身无状态化

    • Data-Ingestion-ServiceData-Process-Service本身不持有状态,可以轻松水平扩展和重启。通过Nginx等负载均衡器对外提供服务。
  3. 优雅降级与熔断

    • 使用 Resilience4j 或 Sentinel 实现熔断。如果MongoDB或Redis响应缓慢或不可用,消费者服务可以快速失败,并让消息重新排队,避免线程池被拖垮。同时,可以降级为只写MongoDB,后续再通过日志补偿Redis。
  4. 监控与告警

    • 全面监控:SpringBoot Actuator + Micrometer + Prometheus + Grafana
    • 监控关键指标:RabbitMQ队列堆积情况、MongoDB操作延迟、Redis内存使用率和命中率、JVM状态、服务实例健康度。一旦异常,立即告警。

六、核心组件选型与配置深度解析

1. SpringBoot-Ingestion-Service (接入服务)

  • 职责:高可用、高性能的数据接收端。

  • 关键配置与代码

    • 使用WebFlux替代传统MVC:基于Netty,应对高并发连接。

      // @PostMapping("/api/v1/data") 传统注解
      @RestController
      @RequiredArgsConstructor
      public class DataIngestionController {
          private final RabbitTemplate rabbitTemplate;
      
          @PostMapping(value = "/api/v2/data", consumes = MediaType.APPLICATION_JSON_VALUE)
          public Mono<ResponseEntity<String>> ingestData(@RequestBody @Valid DeviceData data) {
              // 1. 基础校验 (使用JSR-303)
              // 2. 生成唯一消息ID,用于幂等
              String messageId = generateMessageId(data.getDeviceId(), data.getTimestamp());
              data.setMessageId(messageId);
      
              // 3. 异步发送至RabbitMQ,并配置publisher-confirm
              CorrelationData correlationData = new CorrelationData(messageId);
              return Mono.fromCallable(() -> {
                  rabbitTemplate.convertAndSend("data.exchange", "data.routingkey", data, correlationData);
                  return ResponseEntity.accepted().body("Accepted");
              }).subscribeOn(Schedulers.boundedElastic()); // 防止阻塞Netty工作线程
          }
      }
      

    • RabbitTemplate配置

      spring:
        rabbitmq:
          publisher-confirm-type: correlated # 开启publisher confirm
          publisher-returns: true
          template:
            mandatory: true
      

2. RabbitMQ集群

  • 设计:采用镜像队列,保证队列高可用。

  • 关键配置

    • 交换机:data.exchange (Topic类型)
    • 队列:data.persistence.queue (持久化)
    • 绑定:data.exchange -> data.persistence.queue with data.routingkey
    • 队列策略:ha-mode: all, ha-sync-mode: automatic

3. SpringBoot-Process-Service (处理服务) - 核心中的核心

  • 职责:可靠消费,双写一致性保障。

  • 关键设计

    • 多实例并行消费:通过concurrency配置。

      spring:
        rabbitmq:
          listener:
            simple:
              acknowledge-mode: manual # 手动ACK
              concurrency: 10 # 最小消费者数
              max-concurrency: 20 # 最大消费者数
              prefetch: 50 # 每个消费者预取数量,平衡吞吐和公平
      

    • 批量消费:进一步提升吞吐量(需生产者支持批量发送或消费者攒批)。

      @Bean
      public BatchRabbitListenerContainerFactory batchFactory() {
          BatchRabbitListenerContainerFactory factory = new BatchRabbitListenerContainerFactory();
          factory.setBatchListener(true);
          factory.setBatchSize(1000); // 一批的数量
          factory.setReceiveTimeout(3000L); // 等待超时时间
          return factory;
      }
      
      @RabbitListener(queues = "data.persistence.queue", containerFactory = "batchFactory")
      public void handleBatch(List<Message> messages, Channel channel) {
          // 批量处理逻辑
      }
      

七、双写一致性保障:终极解决方案与实践

我们采用 “先写库,再删缓存” 结合 “消息队列重试” 和 “最终一致性补偿” 的综合方案。

步骤详解与代码:

死信队列监控将重试多次失败的消息转入DLQ,并触发告警。定时补偿Job:针对Redis可能存在的脏数据,定时从MongoDB中拉取最新数据刷新Redis。

@Scheduled(cron = "0 0/5 * * * ?") // 每5分钟执行一次
public void compensativeRefreshRedis() {
    // 1. 扫描MongoDB中最近更新的设备
    // 2. 与Redis中的数据进行对比
    // 3. 如果不一致,则以MongoDB为准,覆盖Redis
}

  1. 幂等消费

    public void handleMessage(DeviceData message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
        String messageId = message.getMessageId();
        // 使用Redis原子操作SETNX+NX+EX做幂等
        String redisKey = "msg:id:" + messageId;
        Boolean isNew = redisTemplate.opsForValue().setIfAbsent(redisKey, "processing", Duration.ofMinutes(10));
        if (Boolean.FALSE.equals(isNew)) {
            log.info("Duplicate message, skipped: {}", messageId);
            channel.basicAck(tag, false);
            return;
        }
    
        try {
            // 2. 执行业务逻辑(写MongoDB,更新Redis)
            processData(message);
    
            // 3. 成功,确认消息
            channel.basicAck(tag, false);
            // 可选:成功后可以删除或保留幂等键一段时间
            // redisTemplate.delete(redisKey);
        } catch (Exception e) {
            log.error("Process failed, message will be retried: {}", messageId, e);
            // 4. 失败,删除幂等键,允许下次重试
            redisTemplate.delete(redisKey);
            channel.basicNack(tag, false, true); // 重试
        }
    }
    

    processData方法:先DB,后Cache (Pipeline优化)

    @Transactional(rollbackFor = Exception.class) // 开启MongoDB事务(4.0+)
    public void processData(DeviceData message) {
        // 1. 转换并插入MongoDB
        DeviceDataDocument doc = convertToDocument(message);
        mongoTemplate.insert(doc);
    
        // 2. 更新Redis状态 (使用Pipeline减少RTT)
        String statusKey = "device:status:" + message.getDeviceId();
        String statsKey = "device:stats:" + message.getDeviceId();
        redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            // 更新Hash结构的状态
            connection.hashCommands().hSet(statusKey.getBytes(), "temp".getBytes(), String.valueOf(message.getTemp()).getBytes());
            connection.hashCommands().hSet(statusKey.getBytes(), "timestamp".getBytes(), String.valueOf(message.getTimestamp()).getBytes());
    
            // 更新Sorted Set用于排行榜查询
            connection.zSetCommands().zAdd("device:output:rank".getBytes(), message.getOutput(), message.getDeviceId().getBytes());
            return null;
        });
    }
    

    最终补偿机制 

八、MongoDB与Redis数据结构设计示例

MongoDB Document设计:

// collection: device_data_${deviceId_shard}
{
  "_id": ObjectId("..."),
  "messageId": "EQP001_1621234567890", // 唯一标识,可建唯一索引
  "deviceId": "EQP001",
  "timestamp": ISODate("2021-05-17T08:16:07.890Z"),
  "metrics": {
    "temperature": 23.4,
    "pressure": 101.3,
    "voltage": 12.5
  },
  "status": "RUNNING",
  "recipe": "BKM-001",
  "lotId": "LOT123456"
}
// 索引:{deviceId: 1, timestamp: -1}

Redis数据结构设计:

  • 实时状态 (String/Hash) :
    HSET device:status:EQP001 temperature 23.4 pressure 101.3 timestamp 1621234567890 status "RUNNING"
  • 当日产量排行 (Sorted Set) :
    ZINCRBY rank:output:20240517 1 EQP001
  • 设备最新报警 (List) :
    LPUSH device:alarm:EQP001 "Over Temperature at 1621234567890" & LTRIM device:alarm:EQP001 0 9 // 只保留10条

八(附)、 性能优化策略

  1. MongoDB

    • 分片策略:以deviceId作为分片键,保证同一设备的数据落在同一分片上,利于查询。
    • 写入优化:启用writeConcern: majorityreadConcern: majority保证读写一致性,但可根据业务调整(如writeConcern: w1)以提升写入速度。
    • 读优化:使用覆盖索引,减少内存交换。
  2. Redis

    • 内存优化:使用Hash等紧凑数据结构,避免使用大量Key。
    • Pipeline:在所有需要连续执行多个命令的场景(如初始化缓存)中大量使用。
    • 集群模式:必须使用Redis Cluster,数据分片存储,实现容量和性能的线性扩展。
  3. JVM与GC调优

    • 针对高并发服务,建议使用G1GC或ZGC。
    • 关键参数:-Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200

九、详细代码放出

1. SpringBoot-Ingestion-Service (接入服务)

application.yml 关键配置

server:
  port: 8080

spring:
  rabbitmq:
    host: rabbitmq-cluster
    port: 5672
    username: admin
    password: strongpassword
    publisher-confirm-type: correlated # 开启publisher confirm
    publisher-returns: true
    template:
      mandatory: true

  data:
    mongodb:
      uri: mongodb://user:pass@mongos-router:27017/fab-database?replicaSet=rs0

  redis:
    cluster:
      nodes:
        - redis-node-1:6379
        - redis-node-2:6379
        - redis-node-3:6379
      max-redirects: 3
    lettuce:
      pool:
        max-active: 16
        max-wait: -1

logging:
  level:
    org.springframework.web: DEBUG
    com.example.ingestion: DEBUG

核心Java代码 (DataIngestionController.java)

@RestController
@RequiredArgsConstructor
public class DataIngestionController {

    private final RabbitTemplate rabbitTemplate;

    @PostMapping(value = "/api/v1/data", consumes = MediaType.APPLICATION_JSON_VALUE)
    public Mono<ResponseEntity<String>> ingestData(@RequestBody @Valid DeviceData data, ServerWebExchange exchange) {
        // 1. 生成唯一消息ID (设备ID+时间戳+随机数)
        String messageId = generateMessageId(data.getDeviceId(), data.getTimestamp());
        data.setMessageId(messageId);

        // 2. 异步发送到RabbitMQ,避免阻塞Netty工作线程
        return Mono.fromCallable(() -> {
            // 设置消息的correlationData,用于confirm回调
            CorrelationData correlationData = new CorrelationData(messageId);
            rabbitTemplate.convertAndSend("data.exchange", "data.routing.key", data, correlationData);
            return ResponseEntity.accepted().body("{"status": "accepted", "messageId": "" + messageId + ""}");
        }).subscribeOn(Schedulers.boundedElastic());
    }

    // RabbitTemplate的ConfirmCallback配置 (在@Configuration类中)
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        template.setMandatory(true);
        template.setConfirmCallback((correlationData, ack, cause) -> {
            if (ack) {
                log.info("Message with ID {} confirmed by broker.", correlationData.getId());
            } else {
                log.error("Message with ID {} failed to reach broker. Cause: {}", correlationData.getId(), cause);
                // 此处可加入重发或告警逻辑
            }
        });
        return template;
    }
}

2. SpringBoot-Process-Service (处理服务)

application.yml 关键配置

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual # 手动ACK
        concurrency: 10
        max-concurrency: 20
        prefetch: 100
      direct:
        retry:
          enabled: true
          max-attempts: 3
          initial-interval: 2000ms

  data:
    mongodb:
      uri: mongodb://user:pass@mongos-router:27017/fab-database?replicaSet=rs0&w=majority&readPreference=secondaryPreferred # 读写分离配置
      auto-index-creation: true

  redis:
    cluster:
      nodes:
        - redis-node-1:6379
        - redis-node-2:6379
        - redis-node-3:6379
      max-redirects: 3

# 批量消费配置
batch:
  rabbit:
    listener:
      enabled: true
      batch-size: 500
      receive-timeout: 5000

核心Java代码 (RabbitMQConsumer.java)

@Component
@Slf4j
@RequiredArgsConstructor
public class RabbitMQConsumer {

    private final MongoTemplate mongoTemplate;
    private final RedisTemplate<String, Object> redisTemplate;

    @RabbitListener(queues = "data.persistence.queue", containerFactory = "batchContainerFactory")
    public void handleBatch(List<Message> messages, Channel channel) throws IOException {
        for (int i = 0; i < messages.size(); i++) {
            Message message = messages.get(i);
            DeviceData data = convertMessage(message);
            long deliveryTag = message.getMessageProperties().getDeliveryTag();

            try {
                processMessage(data, channel, deliveryTag);
                // 批量处理中,对最后一条消息进行批量ACK
                if (i == messages.size() - 1) {
                    channel.basicAck(deliveryTag, true); // multiple=true,确认所有直到该deliveryTag的消息
                }
            } catch (Exception e) {
                log.error("Process failed for messageId: {}", data.getMessageId(), e);
                channel.basicNack(deliveryTag, true, true); // 重试本批所有消息(需谨慎)或记录失败位置
                break;
            }
        }
    }

    private void processMessage(DeviceData data, Channel channel, long deliveryTag) throws Exception {
        String messageId = data.getMessageId();
        String redisLockKey = "msg:id:" + messageId;

        // 1. 幂等检查 - Redis原子操作
        Boolean isNew = redisTemplate.opsForValue().setIfAbsent(redisLockKey, "processing", Duration.ofMinutes(10));
        if (Boolean.FALSE.equals(isNew)) {
            log.info("Duplicate message detected, skipped: {}", messageId);
            return; // 直接跳过,等待批量ACK
        }

        try {
            // 2. 核心业务处理
            processData(data);

            // 3. 处理成功,可删除或保留幂等键
            // redisTemplate.delete(redisLockKey);

        } catch (Exception e) {
            // 4. 处理失败,删除幂等键,允许重试
            redisTemplate.delete(redisLockKey);
            throw e;
        }
    }

    @Transactional(rollbackFor = Exception.class) // 开启MongoDB事务
    public void processData(DeviceData data) {
        // 1. 转换并插入MongoDB
        DeviceDataDocument doc = convertToDocument(data);
        mongoTemplate.insert(doc);

        // 2. 使用Pipeline批量更新Redis
        redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            String statusKey = "device:status:" + data.getDeviceId();
            // 更新设备状态Hash
            connection.hashCommands().hSet(
                statusKey.getBytes(StandardCharsets.UTF_8),
                "lastUpdate".getBytes(StandardCharsets.UTF_8),
                String.valueOf(System.currentTimeMillis()).getBytes(StandardCharsets.UTF_8)
            );
            connection.hashCommands().hSet(
                statusKey.getBytes(StandardCharsets.UTF_8),
                "temperature".getBytes(StandardCharsets.UTF_8),
                String.valueOf(data.getTemperature()).getBytes(StandardCharsets.UTF_8)
            );
            // 更新全局设备最新数据Sorted Set (按时间戳排序)
            connection.zSetCommands().zAdd(
                "global:device:updates".getBytes(StandardCharsets.UTF_8),
                data.getTimestamp(),
                data.getDeviceId().getBytes(StandardCharsets.UTF_8)
            );
            return null;
        });
    }
}

3. MongoDB 数据模型与索引

Document 类 (DeviceDataDocument.java)

@Document(collection = "device_data")
@CompoundIndex(name = "device_timestamp_idx", def = "{'deviceId': 1, 'timestamp': -1}")
@Sharded(shardKey = {"deviceId", "timestamp"}) // 分片键注解(具体分片需在MongoDB中配置)
@Data
public class DeviceDataDocument {
    @Id
    private String id;
    private String messageId; // 业务唯一ID,可单独建唯一索引
    private String deviceId;
    private Instant timestamp;
    private Map<String, Object> metrics; // 灵活存储各种指标
    private String status;
    private String recipeId;
    private String lotId;

    // TTL索引,自动过期旧数据(如果需要)
    @Indexed(expireAfterSeconds = 2592000) // 30天后过期
    private Instant createdAt;
}

4. 补偿Job代码 (CompensationJob.java)

@Component
@Slf4j
@RequiredArgsConstructor
public class CompensationJob {

    private final MongoTemplate mongoTemplate;
    private final RedisTemplate<String, Object> redisTemplate;

    @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
    public void refreshRedisFromMongo() {
        log.info("Starting compensation job: Refreshing Redis from MongoDB");

        // 查找过去一小时内所有活跃设备
        Instant oneHourAgo = Instant.now().minus(1, ChronoUnit.HOURS);
        Query query = new Query(Criteria.where("timestamp").gte(oneHourAgo));
        query.fields().include("deviceId").include("metrics").include("timestamp");

        try (MongoCursor<DeviceDataDocument> cursor = mongoTemplate.stream(query, DeviceDataDocument.class)) {
            cursor.forEachRemaining(doc -> {
                String deviceId = doc.getDeviceId();
                String statusKey = "device:status:" + deviceId;

                // 获取Redis中该设备的最新时间戳
                Object redisTimestamp = redisTemplate.opsForHash().get(statusKey, "lastUpdate");
                long redisTime = redisTimestamp != null ? Long.parseLong(redisTimestamp.toString()) : 0L;

                // 如果MongoDB的数据更新,则以MongoDB为准
                if (doc.getTimestamp().toEpochMilli() > redisTime) {
                    log.debug("Refreshing data for device: {}", deviceId);
                    redisTemplate.opsForHash().putAll(statusKey, Map.of(
                        "temperature", String.valueOf(doc.getMetrics().get("temperature")),
                        "lastUpdate", String.valueOf(doc.getTimestamp().toEpochMilli())
                    ));
                }
            });
        }
        log.info("Compensation job finished.");
    }
}

九(附)、核心总结

  1. 架构清晰:通过五大层次(接入、缓冲、处理、存储、应用)实现职责分离与水平扩展。
  2. 一致性保障:通过 “幂等键 + 先DB后Cache + 重试 + 定时补偿” 四位一体的组合拳,稳健地保障最终一致性。
  3. 性能极致:在每一层采用最佳实践(WebFlux、批量、Pipeline、分片、索引)确保百万级数据处理能力。
  4. 高可用:所有组件均以集群模式部署,无单点故障,并通过监控告警体系实时感知系统状态。

十、SpringBoot + RabbitMQ + MongoDB + Redis 半导体生产数据处理平台核心总结

一、 核心架构思想:分层解耦,各司其职

  1. SpringBoot-Ingestion-Service (接入层)

    • 职责:利用WebFlux实现高性能非阻塞接入,快速接收设备数据,完成基本校验。
    • 关键动作:立即异步化,将消息送入RabbitMQ集群,并通过publisher-confirm机制确保消息可靠抵达消息队列,实现流量削峰,响应设备。
  2. RabbitMQ (异步缓冲层)

    • 职责:系统的稳定器解耦器。采用镜像队列确保高可用,承载流量洪峰,为下游处理提供缓冲。
  3. SpringBoot-Process-Service (处理层)

    • 职责:系统的核心大脑。从RabbitMQ并行消费消息,肩负保障MongoDBRedis双写一致性的重任。
    • 关键设计:多实例部署,配置concurrency实现并发消费,支持批量处理以提升吞吐量。
  4. MongoDB (权威数据源)

    • 职责:存储全量历史数据。采用分片集群架构,以deviceId等作为分片键,实现容量的水平扩展,应对海量数据存储。
  5. Redis (高速缓存与计算层)

    • 职责:存储热数据(如设备最新状态、排行榜、计数器)。提供微秒级查询,支撑实时监控大屏等场景。采用Cluster模式实现高可用与扩展性。

二、 核心难题解决方案:双写一致性保障

方案:幂等性 + 可靠消息 + 最终一致性补偿

  1. 幂等消费

    • 每条消息携带唯一messageId
    • 处理前,使用Redis的SETNX key value NX EX指令设置锁,Key为msg:id:{messageId}。若已存在,视为重复消息,直接ACK跳过。这是整个一致性流程的基石
  2. 严谨的双写顺序

    • 坚决采用“先写MongoDB,再更新Redis”
    • 原因:MongoDB是真相之源。若先写Redis成功但写MongoDB失败,会导致难以清理的脏缓存。反之,若写MongoDB成功但更新Redis失败,可以通过消息重试机制最终保证缓存更新。
  3. 异常处理与重试

    • 消费者采用手动ACK模式。
    • 只有业务处理完全成功(Mongo和Redis都写完),才basicAck确认消息。
    • 若过程中任何一步失败,则抛出异常,执行basicNack并要求消息重新入队重试。在捕获异常后,必须删除之前设置的幂等键,允许下一次重试能再次执行。
  4. 最终补偿机制

    • 对于重试多次失败进入死信队列的消息,进行监控告警。
    • 设立定时补偿任务,定期从MongoDB中拉取最新数据,与Redis中的数据进行比对并刷新,确保最终一致。

三、 百万级性能优化关键点

  1. 写入优化

    • RabbitMQ:生产者开启publisher-confirm,消费者开启批量模式(BatchListener)。
    • MongoDB:使用insertMany进行批量插入。
    • Redis:使用Pipeline一次性发送多个命令,极大减少网络往返开销。
  2. 查询优化

    • MongoDB:精心设计复合索引(如{deviceId: 1, timestamp: -1}),使用投影减少返回数据量。
    • Redis:充分利用丰富数据结构(如ZSET做排行榜、HASH存对象、HyperLogLog做基数统计),进行数据预计算和物化视图存储,避免实时查询数据库。
  3. 资源与部署

    • 所有中间件(RabbitMQ, MongoDB, Redis)均以集群模式部署,消除单点故障。
    • 应用服务(Ingestion, Process)无状态化,便于水平扩展。
    • JVM层面针对高并发场景进行GC调优(如使用G1GC并设定目标暂停时间)。

最后,本架构成功的关键在于:通过消息队列解耦并削峰,通过幂等性设计保障消息处理可靠性,通过“先库后缓”的顺序和补偿机制捍卫最终一致性,再通过批量、Pipeline、分片、索引等技术手段在每一个环节极致优化性能。这是一个经过实践检验的、可支撑半导体工厂海量数据场景的高性能、高可用、高并发解决方案。