MST
星途 面试题库

面试题:Redis缓存与消息队列集成的复杂业务场景处理

假设存在一个电商场景,用户下单后,消息队列需要处理库存扣减、订单状态更新等多个操作,同时Redis缓存用于存储商品信息、订单概要等数据。在这个过程中,既要保证业务的准确性,又要兼顾系统的高可用性和性能。请设计一套完整的集成方案,包括消息的发送、处理逻辑,Redis缓存的更新策略,以及如何应对可能出现的异常情况(如消息丢失、缓存雪崩等)。
21.9万 热度难度
数据库Redis

知识考点

AI 面试

面试题答案

一键面试

1. 消息发送

  1. 消息生产者
    • 在用户下单成功后,由下单服务作为消息生产者,构建包含订单详细信息(如订单ID、商品ID、购买数量等)的消息。
    • 使用消息队列客户端(如Kafka的Producer、RabbitMQ的Publisher等)将消息发送到指定的消息队列主题或队列中。例如,在Kafka中:
    ProducerRecord<String, String> record = new ProducerRecord<>("order - processing - topic", orderInfo);
    producer.send(record);
    
  2. 消息格式:采用JSON格式,这样既易于解析,又具有较好的跨语言兼容性。例如:
    {
        "orderId": "123456",
        "productId": "789",
        "quantity": 2,
        "userId": "user123"
    }
    

2. 消息处理逻辑

  1. 消息消费者
    • 创建多个消费者组分别处理不同的业务逻辑,如库存扣减组和订单状态更新组。
    • 以库存扣减消费者为例,使用消息队列客户端(如Kafka的Consumer、RabbitMQ的Consumer等)从队列中拉取消息。例如,在Kafka中:
    Consumer<String, String> consumer = KafkaConsumerFactory.createConsumer();
    consumer.subscribe(Arrays.asList("order - processing - topic"));
    while (true) {
        ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
        for (ConsumerRecord<String, String> record : records) {
            // 解析消息
            OrderInfo orderInfo = parseOrderInfo(record.value());
            // 调用库存服务扣减库存
            inventoryService.deductInventory(orderInfo.getProductId(), orderInfo.getQuantity());
        }
    }
    
  2. 事务性处理
    • 对于库存扣减和订单状态更新等操作,要保证原子性。可以使用数据库的事务机制。例如,在库存扣减和订单状态更新的业务逻辑中:
    @Transactional
    public void processOrder(OrderInfo orderInfo) {
        inventoryService.deductInventory(orderInfo.getProductId(), orderInfo.getQuantity());
        orderService.updateOrderStatus(orderInfo.getOrderId(), OrderStatus.PROCESSED);
    }
    

3. Redis缓存更新策略

  1. 写后更新
    • 在订单处理完成后(库存扣减和订单状态更新成功),更新Redis缓存。例如,更新商品库存信息:
    Jedis jedis = new Jedis("localhost");
    Product product = productService.getProductById(orderInfo.getProductId());
    jedis.set("product:" + orderInfo.getProductId(), JSON.toJSONString(product));
    
  2. 缓存过期策略
    • 对于商品信息等缓存,设置合理的过期时间。例如,热门商品设置较短的过期时间(如1小时),冷门商品设置较长的过期时间(如1天)。
    jedis.setex("product:" + orderInfo.getProductId(), 3600, JSON.toJSONString(product));
    
  3. 缓存预热
    • 在系统启动时,将热门商品信息、常用订单概要等数据预先加载到Redis缓存中,减少首次查询的响应时间。

4. 异常处理

  1. 消息丢失
    • 消息确认机制:使用消息队列的确认机制。例如,在RabbitMQ中,生产者发送消息时设置mandatory标志,并且监听ReturnCallback,如果消息无法路由到队列,会回调通知生产者。消费者端在处理完消息后,手动确认消息已消费,如在RabbitMQ中:
    channel.basicConsume("order - processing - queue", false, "myConsumerTag",
            (consumerTag, delivery) -> {
                try {
                    // 处理消息
                    processMessage(delivery.getBody());
                    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                } catch (Exception e) {
                    channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, true);
                }
            },
            consumerTag -> {});
    
    • 持久化:将消息队列和消息设置为持久化。在Kafka中,通过设置相应的主题配置参数,保证消息在服务器故障后不丢失。
  2. 缓存雪崩
    • 分散过期时间:避免大量缓存同时过期。在设置缓存过期时间时,在基础过期时间上加上一个随机值。例如:
    int baseExpireTime = 3600;
    int randomOffset = new Random().nextInt(1800);
    jedis.setex("product:" + orderInfo.getProductId(), baseExpireTime + randomOffset, JSON.toJSONString(product));
    
    • 缓存降级:当Redis出现大规模故障时,启用本地缓存(如Guava Cache)或者直接查询数据库,同时记录日志,以便后续分析和恢复。例如:
    LoadingCache<String, Product> localCache = CacheBuilder.newBuilder()
       .maximumSize(1000)
       .expireAfterWrite(10, TimeUnit.MINUTES)
       .build(new CacheLoader<String, Product>() {
            @Override
            public Product load(String key) throws Exception {
                return productService.getProductById(key);
            }
        });
    Product product = localCache.get(orderInfo.getProductId());