kafka 事务

 

目录

概述

幂等

事务

2PC协议

代码示例

事务流程

事务状态

Server侧状态

LSO


概述

kafka 从0.11版本开始支持exactly-once语义。从此,流式处理框架数据准确性语义at-most-once、at-least-once、exactly-once全部支持。exactly-once语义的支持复杂度是最高的,单纯从语义角度上可理解为exactly-once=at-least-once+幂等,kafka事务的引入实现exactly-once语义。kafka事务对ACID做了非完全性的支持,不支持事务回滚,事务隔离级别针对Consumer对数据的可见性提出LSO(last-stable offset)概念,Broker端对同一个txnid的事务做串行处理。

Kakfa事务保证以下三点:

1、跨多个Topic-partitoion原子性写入消息

2、事务中的所有消息要么全部可见,要么全部不可见

3、Consumers必须支持跳过Abort或者未提交事务的消息

其中2、3两点是LSO概念保证的,本文从Consumer、Producer、Broker三个角色分析kafka事务。(全文基于2.3版本)

幂等

kafka中幂等这个概念是针对Proudcer--->Broker写场景讲的,基于at-least-once语义一条消息Broker只能成功存储一次。kafka引入了pid+sequence number实现了topic-partition局性幂等性。pid全局唯一,使用curator框架由zk生成(curator生成全局ID方案),sequence number由Producer累加生成,Broker侧会缓存5条最近pid+sequence组合标识做幂等判断。

配置:enable.idempotence=true,会自动开启幂等,默认为false。(注意:如果设置为true,以下几个配置必须设置正确,否则会抛出ConfigException)

max.in.flight.requests.per.connection小于等于5
retries大于0
acks必须设置为all

 

 

 

 

如果Producer僵死或者Producer服务重启将会打破幂性,使程序失效。解决跨会话、跨topic-partiton写幂等需要结合kafka提供的事务

事务

事务ACID四个特性,kafka做了非传统意义上的支持。

原子性:跨topic-partitionProducer会话原子性写入。事务不支持回滚,但失败的事务数据会打上abort标识,也确保了要么成功,要么失败的语义

一致性:通过LSO水位,确保已commit事务产生的数据对Consumer可见

隔离性:概念很弱化,结合LSO支持Consumer支持read_committed和read_uncomitted

持久性:采用多副本策略,支持高可用(涉及 Quorm机制,acks为all就是Write all Read one,Kafka提出了ISR列表,简化了Quorm机制)。

分布式事务有2PC、3PC、ZAB、Paxos等协议,kafka采用了类似2PC协议,引入了事务协调者TransactionCoordinator概念。

2PC协议:

角色:

1. 协调者(Coordinator)或者叫TM(TransactionManagger)

kafka事务协调者为TransactionsCoordinator,负责事务状态管理、Transaction Marker

2. 事务参与者(participants)

kafka事务参与者为Producer、Broker,但kafka支持consumer-transform-producer模式,因此还有Consumer。如果用RM(ResourceManager)概念只能表达Broker。

Producer:Consumer:Broker为1:1:N

kafka分布式事务并没有2PC协议的TM单点,Participants僵死的问题,TransactionCoordinator支持高可用,Producer支持跨会话。

代码示例

代码示例中把事务处理流程(DataFlow)和Producer关键的配置项均做了说明。具体的流程图可见Exactly Once Delivery and Transactional Messaging

public static void sendWithTransaction(String topic, Consumer codeBlock){
        //下面列出些核心的配置
        Properties pro = new Properties();
        pro.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"host:port");
        pro.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        pro.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        //Leader/Follower都存储成功后返回(涉及ISR列表机制和Quorum机制)
        pro.put(ProducerConfig.ACKS_CONFIG,"all");
        //单个Producer实例可使用的总内存,默认为32M,内存会循环使用
        pro.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 32 * 1024 * 1024L);
        //2.3版本,当设置事务时必须小于等于5,大于5会影响Producer发送消息的顺序
        //2.0以前的版本要保证顺序只能配置1
        pro.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 1);
        //2.3版本,当设置事务时必须大于1
        pro.put(ProducerConfig.RETRIES_CONFIG, 1);
        //开启幂等
        pro.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true");
        //开启事务,每个事务ID与Producer 的PID是一一对应的,出现重复会抛异常
        pro.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "TXNID_"+Thread.currentThread().getId());
        //事务处理时间,必须小于transaction.max.timeout.ms,默认为60秒
        pro.put(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG, 10000L);
        //默认为16KB,每个Partition独立缓存大小为16KB,消息达到16KB后发出
        pro.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384L);
        //默认为0,配合batch_size使用,5毫秒后消息内容没有16KB,也发发出
        pro.put(ProducerConfig.LINGER_MS_CONFIG, 5);
        //初始化kafkaProducer对象,kafkaProduer是线程安全的,一般采用单例模式
        KafkaProducer producer = new KafkaProducer(pro);
        try{
            /**
             * 实始化事务准备阶段的信息。如:
             * 1、寻找TransactionCoordinator
             * 2、申请PID或者PID+epoch
             * 3、根据txid寻找未处理结束(指未Commit或者Abort)的事务
             * 4、检查txid与PID一一对应关系
             */
            producer.initTransactions();
            //开始事务(Producer状态转换为in_transaction),不会与TransactionCoordinator交互
            producer.beginTransaction();
            /**
             * 发送消息
             * 1、发送AddPartitionsToTxnRequest,Producer ---> TransactionCoordinator,持久化到txn_log中
             * 2、发送ProducerRequest,Producer ---> Broker(涉及多个Borker),持久化到user_topic
             */
            producer.send(new ProducerRecord(topic, "key", "value"));
            producer.send(new ProducerRecord(topic, "key", "value"));
            /**
             * consumer-transform-produce模式中使用,发送consumer消费的offset
             * 1、发送AddOffsetCommitToTxnRequest,Producer ---> TransactionCoordinator,持久化到txn_log中
             * 2、发送TxnOffsetCommitRequest,Producer ---> GroupCoordinator,持久化到_consumer-offsets topic中
             */
            producer.sendOffsetsToTransaction(null, "consumerGroup");
            /**
             * 提交事务
             * 发送EndTxnRequest,Producer--->TransactionCoordinator--->Borker(涉及多个Borker)
             * 1、写入一个PREPARE_COMMIT或者PREPARE_ABORT消息到txn_log中
             * 2、发送WriteTxnMarkerRequest,TransactionCoordinator--->Broker(涉及多个Borker)
             * 3、TransactionCoordinator写入Commit或者Abort到 txn_log中
             */
            producer.commitTransaction();
        }catch (Exception e){
            //终止事务
            producer.abortTransaction();
        }finally {
            //关闭Producer,不关闭会导致连接泄露
            producer.close();
        }
    }

Producer是线程安全的,支持配置Tpoic list,单例示例代码:

public class ProducerUtils {
    /**
     * 如果要启用事务建议一个Topic使用一个KafkaProducer。如果使用一个KafkaProducer,
     * 所有的Topic事务操作都由同一个TransactionCoordinator处理。
     **/
    private static final Map<String, KafkaProducer> producerMap = new ConcurrentHashMap<String, KafkaProducer>();
    /**做为添加producer的竞争对象**/
    private static final Object LOCK = new Object();

    /**
     * 添加新的Topic相应的Producer
     * @param topic topic
     * @param pro kafka producer的设置
     * @param <K>
     * @param <V>
     */
    public static <K,V> void addProducer(String topic, Properties pro){
        Optional<String> topicS = Optional.of(topic);
        Optional<Properties> proS = Optional.of(pro);
        //判断是否存在,若不存在添加,如果存在覆盖
        synchronized (LOCK){
            //创建一个新的producer
            KafkaProducer<K,V> newProducer = new KafkaProducer<K, V>(proS.get());
            KafkaProducer oldProducer = producerMap.get(topicS.get());
            //覆盖老的producer
            producerMap.put(topic, newProducer);
            //按存在处理
            if (null != oldProducer){
                //关闭老的
                oldProducer.close();
            }
        }

    }

    /**
     * 按topic发送消息
     * @param topic topic
     * @param msg 发送内容
     */
    public static void send(String topic, String key, String msg){
        try{
            KafkaProducer producer = Objects.requireNonNull(producerMap.get(topic));
            // 发送消息
            producer.send(new ProducerRecord(topic, key, msg));
        }catch (NullPointerException e){
            //打印日志
        }
    }

    public static void sendWithTransaction(String topic, Consumer codeBlock){
        KafkaProducer producer = Objects.requireNonNull(producerMap.get(topic));
        try{
            producer.initTransactions();
            producer.beginTransaction();
            //逻辑处理代码使用函数编程的Consumer接口封装
            codeBlock.accept(producer);
            producer.commitTransaction();
        }catch (NullPointerException e){
            //打印日志
        }catch (ProducerFencedException e){
            producer.abortTransaction();
        }catch (KafkaException e){
            producer.abortTransaction();
        }
    }
}

事务流程

代码示例中已经把事务中每行代码操作内容做了说明,我们只需要把这些内容串起来即可。下面是事务处理的流程,总共有五个阶段,具体详情查看Exactly Once Delivery and Transactional Messaging。Transaction log存储在__transaction_state topic中,Partition 默认为50,replic factor 为3。

                                                  (图片来源:Data Flow URL

1、随机找一台Broker,寻找TransactionCoordinator。hash(txid)% size(partitions)来确定TransactionCoordinator

2、申请PID,并写入Transaction log中

4、4.1 Transaction log存储 partiton信息 4.2发送本次消息内容给Brokers 4.3 Transaction log存储consumer offset 4.4 consumer coordinator 提交offset

5、5.1 更新Meta为PREPAR_COMMIT或者PREPARE_ABORT  5.2 user topic中写入Transaction marker 5.3 Transaction log 写入COMMIT或者ABORT

整个事务流程相当复杂,事务的每个状态、操作信息和结果都持久化在Transaction log(_transaction_state topic中,默认50个partiton)中。其中事务4.x阶段最为关键,4.2阶段为Producer把消息内容发送给相应的Broker存储,4.3阶段把consumer的offset信息存储到Transaction log中,4.4阶段把consumer offset信息提交给GroupCoordinator。阶段4、5都是先写Transction log 再做实际处理。

事务状态

Kafka通过状态机管理事务的状态,有Server侧状态、Producer侧状态。(状态机设计模式:状态和相应的行为一起封装,不同的状态有不同的行为,目的是将特定状态相关的逻辑分散到一些类的状态中。使用场景:订单、物流、会员管理、流程引擎等)

Server侧状态

状态状态码说明
Empty0事务尝未存在
Ongoing1启动事务
PrepareCommit2TransactionCoordinator提交PrepareCommit,Producer可以发送消息给Broker
PrepareAbort3TransactionCoordinator提交PrepareAbort,终止事务
CompleteCommit4group提交了Commit,事务成功处理
CompleteAbort5group提交了Abort,将会删除事务缓存信息
Dead6TransactiontalId已经失效,默认15天
PrepareEpochFence7同一个Txid对应两个Producer,处理掉老的Producer

 

 

 

 

 

 

 

 

 

状态流转图:

LSO

LSO(last stable offset)概念可以确保Consumer只会消费到事务Commit成功的数据,事务Abort产生的数据不会被消费。LSO开关关闭也可以消费到事务Abort产生的数据。Consumer侧提供了isolation.level控制,有两个可选择配置:READ_COMMITTED、READ_UNCOMMITTED,默认为READ_COMMITTED。

Kafka 几个位置的关系

LSO小于等于HW/LEO,LSO可见的位置肯定是事务已经结束了(已经Commit或者Abort)。LSO指向4时,说明5、6、7的数据相关的事务还没有Commit或者Abort。

Consumer读到Abort数据后,如果isolation.level配置的是READ_UNCOMMITTED,Consumer会根据Transaction Marker日志过滤掉这些数据。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值