Storm Transactional Batch Process原理

本文详细解析了Apache Storm中如何通过CoordinatedBolt实现事务性的实时批处理,介绍了TrackingInfo结构及其在处理流程中的作用。

Storm中实现实时批处理的模块由IBatchBolt, BatchBoltExecutor, CoordinatedBolt等数据结构实现,它们之间的关系如图所示:


Storm实现事物批处理是通过在普通的IBatchBolt外面包装一层CoordinatedBolt,通过CoordinatedBolt实现批次和批次之间的事务性协调,其中记录批次元数据的神秘数据结构就是TrackingInfo, 具体定义如下:

public static class TrackingInfo {
	// 记录该bolt针对该批次已经收到了多少个上游节点发送的coord tuple(即有多少个上游节点报告已经发完了本批数据).
        int reportCount = 0;

	// 记录该bolt针对该批次预期应该接收到的regular tuple(非控制tuple)总数. 该值是该bolt的所有上游节点通过coord stream发送的tuple中获得.
        int expectedTupleCount = 0;

	// 记录该批次已经接收到的regular tuple(非控制tuple)总量.
        int receivedTuples = 0;

	// 记录该bolt向下游task已发送过的tuple数量. key为taskId, value为已发送tuple总数.
        Map<Integer, Integer> taskEmittedTuples = new HashMap<Integer, Integer>();

	// 对于非committer bolt, 标识该bolt是否已经接收到了tuple; 
	// 对于committer bolt, 标识该committer bolt是否已经接收到了master bolt针对该batch发来的commit tuple, 将向下游发送coord tuple的时间推迟到接收到commit tuple后,而不是完成对于本批次的处理.
        boolean receivedId = false;

	// 在CoordinatedOutputCollector的ack中被置为true, 标识该批次中是否有tuple处理失败.
        boolean failed = false;

	// 标识该批次包含的所有tuple都已在该bolt中处理完成,该bolt对于该批次的元数据可以被删除.
        boolean finished = false;

	// 记录上游或者master bolt发送过来的coord tuple与commit tuple. 在checkFinishId调用时逐一进行ack.
        List<Tuple> ackTuples = new ArrayList<Tuple>();       
}

对于事务型批处理而言,无非要解决的核心问题是知道什么时候一个批次处理完成,成功完成后续该如何处理,失败了又该如何处理。
在CoordinatedBolt的execute会在接收到tuple时被调用。

public void execute(Tuple tuple) {
        Object id = tuple.getValue(0);
        TrackingInfo track;

	// 首先判断接收到的tuple类型
        TupleType type = getTupleType(tuple);
        synchronized(_tracked) {

	    // 根据batchId获得对应的Batch元数据BatchInfo.
            track = _tracked.get(id);
            if(track==null) {
                track = new TrackingInfo();

		// _idStreamSpec非空, 表示该bolt是一个committer bolt.
		// 如果不是committer bolt, 在第一次接收到tuple后,将batch的receivedId标志位置为true, 在后面执行checkFinishId时, 会将receivedId是否为true来作为bolt对一个批次是否处理完成条件之一.
                if(_idStreamSpec==null) track.receivedId = true;
                _tracked.put(id, track);
            }
        }
        
	// 如果接收到控制流tuple, commit tuple(committer bolt)或者coord tuple(非committer bolt),则调用checkFinishId检查bolt是否完成了本批次处理.
	// 对于committer bolt, 只有接收到了commit tuple后,才会将receiveId置为true.
        if(type==TupleType.ID) {
            synchronized(_tracked) {
                track.receivedId = true;
            }
            checkFinishId(tuple, type);            
        } else if(type==TupleType.COORD) {
	    // 每次接收到上游发来的coord tuple, 说明上游已经完成了本批次tuple处理, 从tuple中获取上游告知的本批次已发送tuple总数, 并进行相应元数据更新(该上游已经报备过).
            int count = (Integer) tuple.getValue(1);
            synchronized(_tracked) {
                track.reportCount++;
                track.expectedTupleCount+=count;
            }
            checkFinishId(tuple, type);
        } else {            
            synchronized(_tracked) {
                _delegate.execute(tuple);
            }
        }
}

// 判断收到的tuple类型
private TupleType getTupleType(Tuple tuple) {
	// committer tuple
        if(_idStreamSpec!=null
                && tuple.getSourceGlobalStreamid().equals(_idStreamSpec._id)) {
            return TupleType.ID;
	// coord tuple
        } else if(!_sourceArgs.isEmpty()
                && tuple.getSourceStreamId().equals(Constants.COORDINATED_STREAM_ID)) {
            return TupleType.COORD;
	// data tuple
        } else {
            return TupleType.REGULAR;
        }
}

上述逻辑解决了预期的问题,即我这个bolt对于该批次预期应该收到多少条数据。另一个就是实际收到了多少数据。

public class CoordinatedOutputCollector implements IOutputCollector {
	public void ack(Tuple tuple) {
            Object id = tuple.getValue(0);
            synchronized(_tracked) {
                TrackingInfo track = _tracked.get(id);

		// 每一条tuple消息被ack的时候,会更新实际收到并处理的tuple总数.
                if (track != null)
                    track.receivedTuples++;
            }
            boolean failed = checkFinishId(tuple, TupleType.REGULAR);
            if(failed) {
                _delegate.fail(tuple);                
            } else {
                _delegate.ack(tuple);
            }
        }
}

最后就是将实际收到的数据量和预期进行对比,看看本批次是否已经完成处理。

CoordinatedBolt的函数checkFinishId完成了这个工作。

private boolean checkFinishId(Tuple tup, TupleType type) {
        Object id = tup.getValue(0);
        boolean failed = false;
        
        synchronized(_tracked) {
            TrackingInfo track = _tracked.get(id);
            try {
                if(track!=null) {
                    boolean delayed = false;
                    if(_idStreamSpec==null && type == TupleType.COORD || _idStreamSpec!=null && type==TupleType.ID) {
                        track.ackTuples.add(tup);
                        delayed = true;
                    }
                    if(track.failed) {
                        failed = true;
                        for(Tuple t: track.ackTuples) {
                            _collector.fail(t);
                        }
                        _tracked.remove(id);

		    // 成功完成一个批次的条件:
		    // 1. 至少接收过一次tuple(对于非committer bolt可以是任意tuple, 对于committer bolt需要收到commit tuple后才向下游发送coord tuple)
		    // 2. 接收到了所有上游节点发来的coord tuple(所有上游节点都已通知自己已经处理完了本批次)
		    // 3. 已经接收到的tuple总数和所有上游宣称发送的数据总数一致
                    } else if(track.receivedId
                             && (_sourceArgs.isEmpty() ||
                                  track.reportCount==_numSourceReports &&
                                  track.expectedTupleCount == track.receivedTuples)){
                        if(_delegate instanceof FinishedCallback) {
                            ((FinishedCallback)_delegate).finishedId(id);
                        }
                        if(!(_sourceArgs.isEmpty() || type!=TupleType.REGULAR)) {
                            throw new IllegalStateException("Coordination condition met on a non-coordinating tuple. Should be impossible");
                        }

			// 向下游节点发送coord tuple消息,通知自己已经处理完了本批次以及发送给下游的数据量
                        Iterator<Integer> outTasks = _countOutTasks.iterator();
                        while(outTasks.hasNext()) {
                            int task = outTasks.next();
                            int numTuples = get(track.taskEmittedTuples, task, 0);
                            _collector.emitDirect(task, Constants.COORDINATED_STREAM_ID, tup, new Values(id, numTuples));
                        }

			// 向上游发送ack tuple, 告知上游自己已经处理完成本批次
                        for(Tuple t: track.ackTuples) {
                            _collector.ack(t);
                        }
                        track.finished = true;
                        _tracked.remove(id);
                    }
                    if(!delayed && type!=TupleType.REGULAR) {
                        if(track.failed) {
                            _collector.fail(tup);
                        } else {
                            _collector.ack(tup);                            
                        }
                    }
                } else {
                    if(type!=TupleType.REGULAR) _collector.fail(tup);
                }
            } catch(FailedException e) {
                LOG.error("Failed to finish batch", e);
                for(Tuple t: track.ackTuples) {
                    _collector.fail(t);
                }
                _tracked.remove(id);
                failed = true;
            }
        }
        return failed;
}


<think>我们正在讨论的是@Transactional注解的底层原理。根据用户的要求,我们需要参考之前提供的引用内容,特别是引用[2]、[3]、[4]和[5],因为它们直接涉及@Transactional注解的原理。 用户的问题焦点:@Transactional注解的底层原理。 根据引用[2]和[4],我们可以知道: 1. @Transactional注解是基于Spring AOP(面向切面编程)实现的。 2. 底层使用了动态代理(JDK动态代理或CGLIB)来拦截带有@Transactional注解的方法。 3. 在方法调用前后,会通过事务拦截器(TransactionInterceptor)加入事务管理逻辑。 引用[3]提到,除了Spring的@Transactional,它还支持JTA 1.2和EJB3的事务注解。 引用[4]详细列出了事务执行的主要流程和关键组件。 因此,我们的回答应该围绕这些点展开。 结构建议: 1. 概述:简要说明@Transactional注解的作用和实现方式(基于AOP和动态代理)。 2. 核心流程:描述事务从开始到结束的调用链(可结合引用[4]的流程)。 3. 关键组件解析:介绍事务管理中的核心组件(如TransactionInterceptor, PlatformTransactionManager等)。 4. 源码分析(简化):简要分析Spring事务管理的核心源码(如TransactionAspectSupport)。 5. 注意事项:使用@Transactional时需要注意的一些点(如自调用问题、事务传播行为等)。 注意:用户要求使用中文,并且要按照指定的格式(行内公式用$...$,独立公式用$$...$$)。由于这个问题主要是原理性的,可能公式不多,但如果有数学表达式(比如事务的传播行为常量值),则按格式处理。 最后,生成相关问题。 根据引用[2]的目录,我们可以深入解析的部分包括: - 注解驱动事务启用机制 - TransactionInterceptor事务拦截器 - TransactionAspectSupport核心支持类 - 事务传播机制深度解析 但考虑到篇幅和用户问题的直接性,我们聚焦底层原理的核心部分。 下面开始组织回答: </think>### 📚 @Transactional 注解底层原理详解 #### 一、核心实现机制 Spring 的 `@Transactional` 注解基于 **AOP(面向切面编程)** 实现,底层通过 **动态代理** 技术(JDK 动态代理或 CGLIB)拦截目标方法,并在方法前后注入事务管理逻辑[^4]。事务执行流程如下图所示: ```mermaid graph LR A[客户端调用] -->|被代理拦截| B[开启事务] B --> C[执行业务逻辑] C --> D{是否异常?} D -->|是| E[回滚事务] D -->|否| F[提交事务] E & F --> G[返回结果] ``` #### 二、核心组件与交互 | 组件 | 作用描述 | |--------------------------|--------------------------------------------------------------------------| | `TransactionInterceptor` | 核心拦截器,处理事务开启/提交/回滚逻辑[^4] | | `PlatformTransactionManager` | 事务管理器接口(如 `DataSourceTransactionManager`)[^5] | | `TransactionDefinition` | 封装事务属性(传播行为、隔离级别等) | | `TransactionStatus` | 表示当前事务状态(是否新事务、需回滚等) | #### 三、关键源码解析(简化逻辑) ```java public class TransactionInterceptor extends TransactionAspectSupport { public Object invoke(MethodInvocation invocation) throws Throwable { // 1. 获取事务属性 TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(...); // 2. 获取事务管理器 PlatformTransactionManager tm = determineTransactionManager(txAttr); // 3. 开启事务 TransactionStatus status = tm.getTransaction(txAttr); try { // 4. 执行业务逻辑 Object result = invocation.proceed(); // 5. 提交事务 tm.commit(status); return result; } catch (Throwable ex) { // 6. 异常处理:回滚事务 completeTransactionAfterThrowing(txAttr, status, ex); throw ex; } } } ``` #### 四、事务传播机制原理 Spring 通过 `TransactionStatus` 实现嵌套事务控制。传播行为常量值(如 `PROPAGATION_REQUIRED`)对应不同的事务策略: - `PROPAGATION_REQUIRED`:存在事务则加入,否则新建(默认值) - `PROPAGATION_REQUIRES_NEW`:始终新建独立事务 - `PROPAGATION_NESTED`:创建事务保存点(嵌套事务) 数学表达:事务传播状态转换可建模为有限状态机: $$ S_{next} = f(S_{current}, \text{Propagation Behavior}) $$ 其中 $S$ 表示事务状态集合[^2]。 #### 五、动态代理实现差异 | 代理类型 | 触发条件 | 原理说明 | |----------------|-------------------------|------------------------------| | JDK 动态代理 | 目标实现接口 | 基于 `java.lang.reflect.Proxy` | | CGLIB 代理 | 目标未实现接口 | 通过字节码增强生成子类 | #### 六、注意事项 1. **自调用失效**:同类内方法互相调用时,代理无法拦截(解决方案:通过 `AopContext.currentProxy()` 获取代理对象) 2. **异常回滚规则**:默认只回滚 `RuntimeException` 和 `Error`,需通过 `rollbackFor` 自定义 3. **事务管理器匹配**:多数据源需明确指定 `transactionManager` 名称[^5] 4. **事务边界**:注解应加在实现类而非接口(避免 JDK 代理的局限) --- ### ❓ 相关问题 1. 如何解决 `@Transactional` 在同类方法调用失效的问题? 2. 多数据源环境下如何配置不同的事务管理器? 3. 事务传播机制中的 `NESTED` 和 `REQUIRES_NEW` 有何本质区别? 4. 如何通过源码调试验证事务的开启/提交时机? 5. Spring Boot 如何自动配置 `PlatformTransactionManager`?
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值