1、如何保证消息不丢失
一条消息从生产到消费这条链路中,有三个地方可能会造成消息丢失,分别如下:
- 消息从生产者写入到消息队列的过程投递失败。
- 消息在消息队列中,持久化失败
- 消息被消费者消费的过程出现异常
1.1 在消息生产过程中投递失败
消息生产者和消息系统一般都是独立部署在不同的服务器上,两台服务器之间要通信就要通过网络来完成,网络不稳定可能会发生抖动,那么数据就有可能会丢失,网络发生抖动会有以下两种情况:
- 情形一:消息在传给消息系统的过程中会发生网络抖动,数据直接丢失。
- 情形二:消息已经达到消息系统,但是消息系统再给生产这服务器返回信息室,网络发生抖动,此时的数据不一定真正的丢失,很可能只是生产者认为数据丢失。
针对消息在消息生产时丢失,可以采用重投机制,当程序检测到网络异常时,小消息再次投到消息系统。但是当重新投递在情形二情况下,可能造成数据重复,如何解决这个问题后面会提到。
1.2 在消息队列中持久化失败
消息系统是可以对消息进行持久化的,一般都是讲消息存储到本地磁盘中,当然也有少数消息中间件支持数据持久化到数据库中,那么消息系统的性能可能会下降。
如果你对redis的持久化有一定的了解的话,你会发现redis在持久化数据时并不是每次新增一条就立即存入本地磁盘,而是将数据先写入到操作系统的page cache中,当满足一定条件时,再将page cache中的数据刷入到磁盘。因为这样可以减少对磁盘的随机I/O操作,我们知道随机I/O操作时非常耗时的,这样也提高了系统的性能,消息中间件也不例外,在持久化时也采用这种方式。
在某些极端情况下,可能会造成page cache中的数据丢失,比如突然断电或者机器异常重启操作。要解决pagecache中数据丢失问题,可采用集群部署的方式,来尽量保证数据不丢失。
1.3 在消费过程中存在消息丢失
消息在消费过程中也是会发生丢失的,而且在消费过程中丢失的概率要比前两种大很多。一条消息消费过程大概分为三步:
- 消费者拉取消息
- 消费者处理消息
- 消费系统更新消费进度
第一步在消息拉取消息时会发生网络抖动异常,第二步在处理消息的时候可能发生一些业务异常,而导致而导致流程并没有走完,如果在第一步第二步发生异常的情况下通知消息系统更新消费进度,那么这条失败的消息就永远不会再处理了,自然就丢失了,其实我们的业务并没有跑完。
要避免消息在消费时丢失的情况,可以在消息接收和处理完成之后才更新消费进度,但是在极端情况下会出现消息重复消费的问题,比如某一条消息在处理完成之后消费者宕机了,这时还没有更新消费进度,消费者重启后,这条消息还是会被消费到。
2、如何保证消息只被消费一次
消息系统本身不能保证消息仅被消费一次,因为:
- 消费本身可能重复
- 下游系统启动拉取重复
- 失败重试带来的重复
- 补偿逻辑导致的重复
以上几点都有可能造成重复消息,要保证消息仅被消费一次可以利用幂等性来实现
等幂是数学上的概念,就是多次执行同一操作和执行一次操作,最终得到的结果是相同的
从等幂的概念上就可以看出来,就算消息执行多次也不会对系统造成影响,那么在使用消息系统时如何保证幂等性呢?因为生产者和消费者都有可能产生重复消息,所以要在生产者和消费者两端都保证等幂性。
- 保证生产者等幂性
保证生产者等幂性,再生产消息的时候,利用雪花算法给消息生成一个全局id,在消息系统中维护消息与id的映射关系,如果在映射表中已经存在相同id,则丢掉这条消息,虽然消息被投递了两次,但实际上就保存了一条,避免了消息重复问题。
生产者等幂性跟所选的消息中间件有关系,因为绝大多数情况下消息系统不需要我们自己实现,所以等幂性不太好控制的,消费者等幂性才是我们开发人员控制的重点方向。
- 保证消费者等幂性
在通用层面,在消费消息时产生全局唯一id,消息被处理成功后,把这个全局id存入数据库中,在处理下一条消息之前,先从数据库中查询这个全局id是否存在,如果存在,则直接放弃该消息。
利用这个全局id就实现了消息等幂性,伪代码如下:
boolean isIDExisted = selectByID(ID); // 判断ID是否存在
if(isIDExisted) {
return; //存在则直接返回
} else {
process(message); //不存在,则处理消息
saveID(ID); //存储ID
}