解决异步消息不重复不丢失的问题
对于异步消息系统,消息的准确可靠性是非常重要的,其中不重复和不丢失更是最基本的要求之一。在这里,我们将结合 RabbitMQ、Spring Boot、Redis 和数据库等技术,从三个方面来解决这个问题。
文章目录
是什么: 异步消息不重复不丢失
异步消息系统通常由消息生产者、消息队列和消息消费者等组成。生产者将消息发送到消息队列,消费者从队列中获取消息并处理。这种方式可以达到解耦、异步和可扩展的目的。
在我们的解决方案中,我们使用 RabbitMQ 作为消息队列,Spring Boot 作为框架,Redis 作为缓存,数据库作为持久化。RabbitMQ 是一个开源的消息代理,它可以实现 AMQP(Advanced Message Queuing Protocol)协议,支持持久化、可靠性、扩展性和灵活性等特性。Spring Boot 可以快速搭建 Web 应用,并且对 RabbitMQ 有良好的支持。Redis 是一个内存数据库,可以作为缓存和分布式锁等用途。数据库则提供消息的持久化。
为什么: 异步消息的重复和丢失的原因
-
异步消息的重复通常是由于消费者在消费消息后,没有正确地向消息队列确认消费成功,导致消息队列认为该消息未被消费,从而再次将该消息发送给消费者。
-
异步消息的丢失则可能是由于生产者在发送消息后,消息队列没有正确地接收到该消息,或者消费者在消费消息前,该消息已经从消息队列中被删除。
为了保证消息的不重复和不丢失,我们需要考虑以下几个方面:
1. 消息的幂等性
幂等性指多次调用同一接口,对于相同的输入参数,结果不会发生变化。在消息系统中,保证消息的幂等性可以避免消息的重复处理。为了实现消息的幂等性,我们需要为每个消息生成唯一的 ID,并将消息的处理结果和 ID 存储到缓存或数据库中。在处理消息前,先判断是否已经存在相同的 ID,如果存在则不处理该消息。
2. 消息的持久化
消息的持久化可以避免消息在处理中丢失。在 RabbitMQ 中,我们可以设置消息队列的持久化属性,使得队列和消息可以存储到磁盘中,在服务器重启后仍能恢复。同时,我们还可以将消息的 ID 和处理结果存储到数据库中,以保证数据的持久化。
3. 消息的重试
在消息处理失败时,我们需要重新处理该消息。为了避免重复处理已经成功的消息,我们需要记录每个消息的处理次数,并设置最大重试次数。如果消息处理失败,则将消息重新加入队列中,并将消息的处理次数加一。
4. 消息的消费确认
在消息处理完成后,我们需要向 RabbitMQ 发送消费确认,告知 RabbitMQ 该消息已经被成功处理。这样可以避免消息被重复消费。同时,我们还可以将消费确认和消息的处理结果一起存储到数据库中,以备后续查询和分析。
怎么办: 异步消息的重复与丢失的解决方案
在实现上述功能时,我们可以采用以下技术和方案:
1. 生成消息 ID
我们可以使用 UUID(Universally Unique Identifier)作为消息的 ID。UUID 是一种 128 位的数字,可以保证全球唯一。在 Spring Boot 中,我们可以使用 java.util.UUID 类来生成 UUID。
2. 消息的持久化
在 RabbitMQ 中,我们可以设置消息队列的持久化属性,使得队列和消息可以存储到磁盘中。同时,在发送消息时,我们可以设置消息的持久化属性,使得消息可以在队列中持久化存储。在消费消息时,我们可以使用 channel.basicAck() 方法来发送消费确认。
3. 消息的重试
在处理消息失败时,我们可以将消息重新加入队列中,并设置消息的 TTL(Time To Live)。TTL 是消息在队列中可以存活的时间,到期后将被自动删除。如果消息处理失败,则等待一段时间后重新加入队列中,再次尝试处理。在 RabbitMQ 中,我们可以使用 channel.basicReject() 方法来拒绝消息,并将消息重新加入队列。
4. 消息的消费确认
在消费消息并处理完成后,我们可以使用 channel.basicAck() 方法来发送消费确认。同时,我们可以将消费确认和消息的处理结果一起存储到数据库中。在查询和分析时,可以根据消息的 ID 来查询消息的处理结果。为了保证消息的可靠性,我们还可以使用 Redis 来提供分布式锁,避免消息的重复处理。
纸上的来终觉浅
1. 消息幂等的简单demo
以下是基于 RabbitMQ、Redis、Spring Boot、MySQL 的消息幂等的 Demo。
首先,定义一个 Order 消息类,用于发送和接收消息:
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
private String orderId;
private String userId;
private String productId;
private Integer amount;
// 省略 getter 和 setter 方法
}
接着,定义一个消息处理类,用于处理接收到的消息:
@Component
public class OrderConsumer {
private static final Logger logger = LoggerFactory.getLogger(OrderConsumer.class);
@Autowired
private OrderService orderService;
/**
* 消费消息
*/
@RabbitListener(queues = "order.queue")
public void consume(Order order) {
logger.info("接收到订单消息:{}", order);
try {
// 幂等性校验
if (orderService.isProcessed(order.getOrderId())) {
logger.info("订单已处理,忽略处理请求");
return;
}
// 处理订单
orderService.processOrder(order);
logger.info("订单处理成功");
} catch (Exception e) {
logger.error("订单处理失败:{}", e.getMessage());
}
}
}
在消息处理类中,首先进行幂等性校验,判断该订单是否已经被处理过了。如果已经被处理,就忽略该消息。如果没有被处理,就处理该订单并将处理结果保存到数据库中。
接着,定义一个 OrderService 类,用于处理订单:
@Service
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 处理订单
*/
public void processOrder(Order order) {
logger.info("开始处理订单:{}", order);
// 执行业务逻辑...
// 订单处理完成,将订单 ID 加入 Redis 缓存
redisTemplate.opsForValue().setIfAbsent(order.getOrderId(), "");
logger.info("订单处理完成,订单 ID:{}", order.getOrderId());
}
/**
* 判断订单是否已被处理过
*/
public boolean isProcessed(String orderId) {
return redisTemplate.hasKey(orderId) || jdbcTemplate.queryForObject(
"select count(*) from order_info where order_id = ?", Integer.class, orderId) > 0;
}
}
在 OrderService 类中,定义了处理订单和判断订单是否已被处理过的方法。处理订单时,先执行具体业务逻辑,然后将订单 ID 放入 Redis 缓存中表示该订单已经被处理过了。判断订单是否已被处理过时,先从 Redis 缓存中查找,如果不存在再从 MySQL 数据库中查找,如果都没有查到则表示订单未被处理过。
最后,定义一个 OrderController 类,用于发送订单消息:
@RestController
@RequestMapping("/orders")
public class OrderController {
private static final Logger logger = LoggerFactory.getLogger(OrderController.class);
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送订单消息
*/
@PostMapping("")
public String sendOrder(@RequestBody Order order) {
try {
// 发送订单消息
rabbitTemplate.convertAndSend("order.exchange", "order.routingKey", order);
logger.info("订单发送成功:{}", order);
return "订单发送成功";
} catch (Exception e) {
logger.error("订单发送失败:{}", e.getMessage());
return "订单发送失败";
}
}
}
在 OrderController 类中,定义了发送订单消息的方法。该方法通过 RabbitTemplate 对象发送订单消息。
至此,一个基于 RabbitMQ、Redis、Spring Boot、MySQL 的消息幂等的 Demo 就完成了。
2. 消息重试以及消息确认的简单demo
以下是基于 RabbitMQ、Redis、Spring Boot、MySQL 的消息重试以及消息确认的 Demo。
首先,定义一个 Order 消息类,用于发送和接收消息:
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
private String orderId;
private String userId;
private String productId;
private Integer amount;
// 省略 getter 和 setter 方法
}
接着,定义一个消息处理类,用于处理接收到的消息:
@Component
public class OrderConsumer {
private static final Logger logger = LoggerFactory.getLogger(OrderConsumer.class);
@Autowired
private OrderService orderService;
/**
* 消费消息
*/
@RabbitListener(queues = "order.queue")
public void consume(Order order, @Headers Map<String, Object> headers, Channel channel) throws IOException {
logger.info("接收到订单消息:{}", order);
try {
// 处理订单
orderService.processOrder(order);
logger.info("订单处理成功");
// 手动确认消息已消费
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
logger.error("订单处理失败:{}", e.getMessage());
// 消息重试
if (orderService.isRetryable(order.getOrderId())) {
logger.info("订单可重试,开始重试");
// 手动确认消息未消费,退回队列并开启重试
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
channel.basicNack(deliveryTag, false, true);
// 将订单 ID 加入 Redis 缓存,用于记录重试次数
orderService.addRetryTask(order.getOrderId());
logger.info("订单重试完成");
} else {
logger.info("订单不可重试,拒绝该消息");
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
channel.basicReject(deliveryTag, false);
}
}
}
}
在消息处理类中,先处理订单,如果处理成功就手动确认消息已消费,如果处理失败就进行消息重试。如果订单可重试,就退回消息队列并开启重试任务,将订单 ID 加入 Redis 缓存中用于记录重试次数。如果订单不可重试,就拒绝该消息。需要注意的是,在使用 basicNack
方法退回消息队列时,第三个参数 requeue
表示该消息是否重新放回队列,在使用 basicReject
方法时,第二个参数 requeue
表示该消息是否重新放回队列。
接着,定义一个 OrderService 类,用于处理订单:
@Service
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 处理订单
*/
@Transactional
public void processOrder(Order order) {
logger.info("开始处理订单:{}", order);
// 执行业务逻辑...
// 订单处理完成,将订单 ID 从 Redis 缓存中删除
redisTemplate.delete(order.getOrderId());
logger.info("订单处理完成,订单 ID:{}", order.getOrderId());
}
/**
* 判断订单是否可重试
*/
public boolean isRetryable(String orderId) {
// 从 Redis 缓存中读取重试次数,如果超过三次则不再重试
Integer retries = (Integer) redisTemplate.opsForValue().get(orderId);
return retries == null || retries < 3;
}
/**
* 添加重试任务
*/
public void addRetryTask(String orderId) {
redisTemplate.opsForValue().increment(orderId, 1L);
}
}
在 OrderService 类中,定义了处理订单、判断订单是否可重试、添加重试任务的方法。处理订单时,先执行具体业务逻辑,然后将订单 ID 从 Redis 缓存中删除。判断订单是否可重试时,从 Redis 缓存中读取重试次数,如果超过三次则不再重试。添加重试任务时,将订单 ID 加入 Redis 缓存中,并将重试次数加一。
最后,定义一个 OrderController 类,用于发送订单消息:
@RestController
@RequestMapping("/orders")
public class OrderController {
private static final Logger logger = LoggerFactory.getLogger(OrderController.class);
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送订单消息
*/
@PostMapping("")
public String sendOrder(@RequestBody Order order) {
try {
// 发送订单消息
rabbitTemplate.convertAndSend("order.exchange", "order.routingKey", order, new CorrelationData(order.getOrderId()));
logger.info("订单发送成功:{}", order);
return "订单发送成功";
} catch (Exception e) {
logger.error("订单发送失败:{}", e.getMessage());
return "订单发送失败";
}
}
}
在 OrderController 类中,定义了发送订单消息的方法。该方法通过 RabbitTemplate 对象发送订单消息,并通过 CorrelationData
类型的参数设置消息的唯一标识。
至此,一个基于 RabbitMQ、Redis、Spring Boot、MySQL 的消息重试以及消息确认的 Demo 就完成了。
注意
已上确认幂等的处理,如果在特别高并发的时候,仍然会造成消息的重复落库,如果遇到可以考虑用分布式锁或者唯一索引互斥落库。
使用分布式锁可以保证同一时刻只有一个线程能够处理消息,避免重复落库。常见的分布式锁实现方式有 Redis 分布式锁、Zookeeper 分布式锁等。使用唯一索引可以保证相同的消息只会被落库一次,避免重复落库。在 MySQL 中,可以通过在表上创建唯一索引来实现。
需要注意的是,使用分布式锁或唯一索引可能会影响系统的性能和可用性,因此需要根据具体情况进行权衡和优化。
总结
文章主要介绍了如何解决异步消息不重复不丢失的问题。通过使用 RabbitMQ 作为消息队列,Spring Boot 作为框架,Redis 作为缓存,数据库作为持久化,从生成消息 ID、消息的持久化、消息的重试、消息的消费确认等几个方面提出了解决方案。文章详细介绍了如何实现每个方面的解决方案,从而保证了消息的准确可靠性。