数据不丢失与准确类
# 数据不丢失与准确类
# 积分系统-积分消费问题
在积分系统中,下单后需要发放积分。如果 Kafka 发送失败,怎么保证消息最终不会丢失,同时又避免重复发放?
这明显是一个中台或者电商场景下的实际痛点问题。用户可能正在准备中高级Java岗位的面试,需要展示对分布式系统数据一致性的深入理解。
这种问题往往没有一个完美的唯一的答案,往往有多种不同方案。有些可能性能更强,有些可能对数据的一致性保证更好,一个优秀的开发者或者架构师的回答,应该是体现架构思维、熟悉主流技术栈、并能平衡业务需求与技术复杂度的回答。
# 核心关注
该问题,需要同时处理消息可靠性与幂等性,还涉及异步流程的数据一致性。用户可能希望听到既有理论支撑(如CAP定理、幂等性设计),又能落地到具体技术(Kafka特性、Redis/Lock实现、DB事务整合)。
# 一些分析
首先明确这是分布式系统典型问题,必须同时处理可靠投递和消费幂等;然后分阶段讨论——生产端保证不丢(确认机制+重试),消费端保证不重(幂等设计+去重表);最后要给出技术选型建议和权衡(比如Redis幂等的性能好但持久性弱,DB方案更可靠但复杂)。
# 参考回答
# 第一部分:如何保证消息100%不丢失(可靠性)
消息传递有三个环节:生产端 -> Kafka Broker -> 消费端。要保证不丢,就需要在这三个环节都做保障。
1. 生产端 (Producer) 保证不丢
- ACK确认机制:将Producer的
acks
设置为all
(或-1
)。这意味着Leader副本必须等待所有ISR(In-Sync Replicas)副本都成功接收到消息后,才会向Producer发送成功确认。这是最强程度的持久化保证。 - 重试机制:设置一个合理的
retries
参数(如10次)和retry.backoff.ms
。当遇到网络抖动或Leader选举等瞬时故障时,Producer会自动重试发送,避免因一次失败就丢弃消息。 - 同步发送+异常处理:在发送关键业务消息时,采用同步发送(
future.get()
)或在异步发送的回调(Callback
)中仔细处理异常。一旦发送失败,可以将消息持久化到本地数据库(或一个“发送中”状态表),然后启动一个补偿任务定时重试,直到发送成功才将本地状态更新为“已发送”。
2. Broker端保证不丢
- 副本机制 (Replication):创建Topic时,设置
replication.factor
>= 3。这样即使一个Broker节点宕机,数据也不会丢失。 - ISR集合:Kafka的Leader会维护一个同步中的副本集合(ISR)。只有ISR中的副本才有资格被选举为新的Leader,这保证了数据的可靠性。
- 持久化:Kafka本身的设计就是依赖磁盘顺序读写来持久化消息的,可靠性很高。
3. 消费端 (Consumer) 保证不丢
- 手动提交偏移量 (Manual Offset Commit):禁用自动提交(
enable.auto.commit = false
),在业务逻辑成功执行完毕后再手动提交偏移量。 - 正确的提交顺序:一定是先处理业务,再提交偏移量。如果顺序反了,业务处理失败但偏移量已提交,这条消息就永远丢失了。
小结一:通过 acks=all
+ 重试
+ 副本
+ 手动提交偏移量
这套组合拳,可以理论上保证消息在Kafka链条上不会被丢失。
# 第二部分:如何保证积分不重复发放(幂等性)
“解决了不丢的问题,由于网络重传、Consumer故障重启后的重试等原因,消息重复是必然会发生的事情。所以,我们必须让消费逻辑具备幂等性。”
幂等性 (Idempotence) 的意思是:同一个积分发放请求被多次执行,其效果与只执行一次的效果完全相同。
实现幂等性的常见方案:
数据库唯一键 (最常用、最有效)
- 在发放积分的SQL语句执行前,先执行插入操作。我们可以创建一个幂等表(deduplication_table),表结构主要包含:
biz_id
(String): 业务唯一ID,作为唯一索引或主键。user_id
(Long): 用户ID。points
(Int): 积分值。created_at
(DateTime): 创建时间。
- 这个
biz_id
可以是订单号 + 业务类型
(如:order_12345_points
),必须能全局唯一标识这一笔积分发放业务。 - 在消费消息时,先执行
INSERT INTO deduplication_table (biz_id, user_id, points) VALUES (?, ?, ?)
。 - 如果插入成功,说明是第一次处理,继续执行后续的积分增加操作。
- 如果插入失败(触发了唯一键冲突异常),说明这条消息之前已经处理过了,直接丢弃消息或记录日志后确认消费成功即可。这样就避免了重复发放。
- 在发放积分的SQL语句执行前,先执行插入操作。我们可以创建一个幂等表(deduplication_table),表结构主要包含:
Redis原子操作
- 利用Redis的
SET key value NX EX timeout
命令。Key可以是同样的业务唯一IDbiz_id
。 NX
表示只有Key不存在时才能设置成功,这是一个原子操作。- 如果设置成功,说明是第一次处理,执行业务逻辑。
- 如果设置失败,说明是重复消息,直接跳过。
- 优点:性能极高。缺点:需要维护Redis的可靠性,可能存在数据丢失风险(尽管可以持久化),严格场景下不如数据库可靠。
- 版本号或状态机 (更复杂的业务)
- 在用户积分账户表中增加一个
version
字段。 - 更新积分时,带上版本号条件:
UPDATE account SET points = points + 100, version = version + 1 WHERE user_id = ? AND version = ?
。 - 如果更新条数为0,说明版本号不对,可能是重复请求,不做处理。
小结二:首选方案是基于数据库唯一键的幂等设计,因为它简单、可靠,无需引入新技术栈,并且利用了数据库本身的能力。
所以,结合以上两点,我的最终方案是:
- 生产端:配置
acks=all
和充足的重试次数,同时做好发送失败后的落盘重试兜底方案,确保消息一定能到达Kafka。 - 消费端:采用手动提交偏移量,确保业务成功后再提交。并且,在积分发放的核心逻辑中,引入基于数据库唯一键的幂等设计,从根本上去除重复消息的影响。
此外,对于一个完备的系统,我们还可以加上定期对账的兜底措施,比如每天凌晨检查订单系统和积分系统的数据是否最终一致,及时发现并修复极端异常情况下可能产生的数据差异。”
“这个方案在保证了高可靠性的同时,也通过幂等性兼容了系统的高性能需求,是一个在实际项目中经过验证的成熟方案。”
# 追问
# 1.“如果幂等表的数据量非常大怎么办?”
- 回答:可以定期归档历史数据,比如只保留最近3个月的幂等记录。因为对账系统可以覆盖历史数据的正确性,近期的不重复由幂等表保证,远期的由对账系统保证。
# 2.“为什么不用Kafka自带的幂等Producer和事务功能?”
- 回答:Kafka的幂等Producer(
enable.idempotence=true
)只能保证单分区、单会话内消息不重复,即解决的是Producer端因重试导致的消息重复。而消费端的重复(如Consumer崩溃后重启)它无法解决。事务功能主要用于“精确一次(Exactly-Once)”语义,跨多个Topic-Partition的原子性写入,成本较高。对于我们这种单一积分Topic的场景,在消费端做业务幂等是更简单、更通用、性能也更好的选择。
# 4.“如果数据库插入成功了,但后续更新积分失败了怎么办?”
- 回答:这是一个很好的问题。我们需要将插入幂等记录和更新积分账户放在同一个数据库事务里。这样,如果更新失败事务回滚,幂等记录也会被回滚掉,同一个消息下次过来依然会被当作新消息处理,直到最终成功。
# kafka扩展
# kafka高效的原因
处理数据顺序性,和零拷贝技术,还和kafka特殊的设计有关。
在发送过程中,生产者采用record accumulator和sender线程配合工作。Record accumulator作为消息累加器提前排队消息,按照partition进行有序排列;sender线程则从record accumulator中找到一批消息(默认5条,可配置)组成批量发送给broker,并在中心线程中等待broker的响应,以减少网络开销。
# ack机制
ACK是producer端的一个配置参数,用于控制消息发送后的确认方式。
0表示仅发送不关心broker是否成功接收,1表示要求broker将消息写入leader partition后确认,而-1或2则表示希望broker同步完成所有follower partition后再给producer确认。
# 发送消息失败
如果一条消息发送失败,其他消息还能否继续发送?如果一号消息发送失败,而二号到五号消息都成功发送到broker,那么broker如何处理这些消息顺序问题?
当一条消息发送失败时,其他消息(如二号、三号、四号、五号消息)仍然可以继续发送并一同发送给broker。这样设计是为了提升网络的吞吐量,即使有消息重试的情况,也能保证其他消息及时发送和处理。在这种情况下,broker需要保证消息的顺序和幂等性。它通过维护一个针对每个partition的唯一ID(PID)和序列号(sequence number)来记录消息的顺序。即使一号消息重试,broker也会根据序列号确保消息按照正确的顺序写入日志文件,同时避免重复写入已确认的消息。
# broker端保证消息顺序和幂等
在broker端,它是如何根据PID和sequence number保证消息的顺序和幂等性的?
在broker端,它会对PID和sequence number组成的二元组进行唯一性维护,并维护一个SN(序列号)。当接收到producer发送的消息时,broker会按照SN的顺序入队,只有当确认之前发送的消息已成功写入后,才会继续处理后续的消息。此外,对于重试的消息,broker基于相同的sequence number识别并避免重复写入已确认的消息,确保消息的安全性。
# broker幂等
消在息重试时,broker如何处理重复发送的消息?
当出现消息重试时,broker通过检查接收到的message的PID和sequence number来判断是否为重复消息。对于重复的消息,broker不会进行写入操作,而是按机制给予响应,让producer去做进一步处理,比如重新尝试发送或其他错误处理。
总结
broker处理消息机制:
- 幂等性处理
- PID与Sequence Number:保证消息唯一性与顺序
- SN记录:记录PID与Sequence Number的顺序
- 消息顺序与重复处理
- 超时机制:处理长时间未到达的消息
- 重复消息:识别并忽略重试消息
- 乱序处理:等待缺失消息,保证顺序