之前和大家聊过kafka
是如何保证消息不丢失的,今天再讲讲在不丢消息的同时,如何实现精确一次处理的语义实现。
消息组件对消息的可靠性保障,常见的模式有3种:
- 最多一次(at most once):消息可能会丢失,但不会重复
- 至少一次(at least once):消息不会丢失,但有可能重复
- 精确一次(exactly once):消息不会丢失,且不会重复,精准一次发送
kafka
默认情况下,提供的是至少一次
的可靠性保障。即broker
保障已提交
的消息的发送,但是遇上某些意外情况,如:网络抖动,超时等问题,导致Producer
没有收到broker
返回的数据ack
,则Producer
会继续重试发送消息,从而导致消息重复发送。
相应的,如果我们禁止Producer
的失败重试发送功能,消息要么写入成功,要么写入失败,但绝不会重复发送。这样就是最多一次
的消息保障模式。
但对于消息组件,排除特殊业务场景,我们追求的一定是精确一次
的消息保障模式。kafka
通过幂等性(Idempotence)和事务(Transaction)的机制,提供了这种精确的消息保障。
幂等
这里就不多说幂等的含义了,不清楚的自己查下资料。Producer
默认不是幂等性的,向分区发送数据时,可能会出现同一条消息被发送多次导致消息重复的情况。但只需增加一些参数,即可开启幂等性。
1 | props.put(“enable.idempotence”, ture) |
开启enable.idempotence
后,kafka
就会自动帮你做好消息去重的一系列工作。底层具体实现原理很简单,就是用空间换时间的优化思路,即在broker
端多存一些字段来标识数据的唯一性。当Producer
发送了具有相同字段值的消息后,broker
会进行匹配去重,丢弃重复的数据。实际的代码没这么简单,但大致是这么个处理逻辑。
官方的这个幂等实现看似简单高效,但也存在他的局限性。他只能保证单分区上的幂等性,即一个幂等性Producer
只能够保证某个topic
的一个分区上不出现重复消息,无法实现多分区的幂等。此外,如果Producer
重启,也会导致幂等重置。
事务
对于多分区保证幂等的场景,则需要事务特性来处理了。kafka
的事务跟我们常见数据库事务概念差不多,也是提供经典的ACID
,即原子性(Atomicity)、一致性 (Consistency)、隔离性 (Isolation) 和持久性 (Durability)。事务Producer
保证消息写入分区的原子性,即这批消息要么全部写入成功,要么全失败。此外,Producer
重启回来后,kafka
依然保证它们发送消息的精确一次处理。
事务特性的配置也很简单:
- 和幂等
Producer
一样,开启enable.idempotence = true
- 设置
Producer
端参数transctional.id
事务Producer
的代码稍微也有点不一样,需要调一些事务处理的API
。数据的发送需要放在beginTransaction
和commitTransaction
之间。Consumer
端的代码也需要加上isolation.level
参数,用以处理事务提交的数据。示例代码:
1 | producer.initTransactions(); |
事务Producer
虽然在多分区的数据处理上保证了幂等,但是处理性能上相应的是会有一些下降的。
依赖redis实现幂等
这里为什么还要额外讲通过依赖redis
来实现幂等呢?因为笔者在早期维护kafka
相关应用时,那会0.8系列版本的kafka
还没有这些自带的幂等事务特性,只能依靠开发者自己来实现。
常见的方式就是通过数据的业务属性来生成个uniqueId
来维护到redis
中,利用redis
的高并发,高吞吐,分布式锁特性,让写入kafka
多分区的数据前,先去redis
中校验一下uniqueId
等方式,来实现幂等。得益于redis
的高性能,在保证幂等同时,还能不让消息数据吞吐性能下降太多。当然,因为redis
的依赖引入,也增加了架构的复杂度,从运维上来说也增加了整体的故障点,其中取舍需要自己来全局判断。
这次大概先介绍了下
kafka
的幂等各种实现方式,实际在事务,和依赖redis分布式锁来实现幂等的方式中,还要许多点值得我们深究来聊一下的,篇幅所限,后续再细讲