记录一次数据库驱动配置引发的惨案
最近,在我们项目中,有个服务在前台办理业务的时候,一直报:Transation does not exist
,由于这块业务的代码已经好久没有改动,所以初步推测可能是配置的问题,但是经过核查也没有人改动过配置,而且改了配置以后问题依然存在。由于这个问题影响业务办理一周了,必须尽快处理,当然幸运的是我们解决了问题。
昨天我们就开始从代码入手,经过最后的排查测试,最后确认还还是设置的问题,当然和刚开始简单推测不同的是我们清楚了为什么说是配置的问题——对代码事务处理不清楚——不清楚业务代码事务处理机制。这里有不清楚的原因有很多,我觉得核心的有两点,一个是项目管理混乱,代码不够规范,每个人都按照自己的理解编写代码,没有统一规范和注释要求;另一个原因是这里的业务代码是从其他项目移植过来的,之前该部分业务处理是另外一个项目中处理的,后来因为业务调整,将相关业务直接移植过来,可能由于没出现什么问题,所以大家从来都没有关心和研究过这里的代码,当然从这个角度来说,出现问题也算好事,至少能推动我们去深入研究代码。
好了,我们开始今天的正文。
项目详情
首先,我们先了解下这里的业务情况,大概流程是这样的:业务A,涉及到两个系统,系统H、系统W,由系统W上报基础数据,在系统H审核数据,然后完成系统H相关数据操作(读写校验),同时将相关审核结果同步到系统W,由于两个系统并没有数据交互接口,所以项目是通过配置多数据源来同步数据的。也就是说,在一个完整业务中,我们要操作两个数据库库,也就是要处理跨数据源事务,属于分布式事务范畴。
排查过程
经过我们排查发现,问题发生在最后事务提交的时候,我们打印了事务的状态信息,然后查询了相关资料,了解到以下信息,依据我们掌握的情况,最终我们确切地定位了错误原因,然后问题解决。
事务处理机制
事务控制采用的是JTA机制,事务控制过程大致如下:
UserTransaction userTx = null;
Connection connA = null;
Statement stmtA = null;
Connection connB = null;
Statement stmtB = null;
try{
// 获得 Transaction 管理对象
userTx = (UserTransaction)getContext().lookup("\
java:comp/UserTransaction");
// 从数据库 A 中取得数据库连接
connA = getDataSourceA().getConnection();
// 从数据库 B 中取得数据库连接
connB = getDataSourceB().getConnection();
// 启动事务
userTx.begin();
// 这里处理相关业务
...
// 提交事务
userTx.commit();
// 事务提交:操作同时成功(数据库 A 和数据库 B 中的数据被同时更新)
} catch(SQLException sqle){
try{
// 发生异常,回滚在本事务中的操纵
userTx.rollback();
// 事务回滚:转账的两步操作完全撤销
//( 数据库 A 和数据库 B 中的数据更新被同时撤销)
stmt.close();
conn.close();
...
}catch(Exception ignore){
}
sqle.printStackTrace();
} catch(Exception ne){
e.printStackTrace();
}
事务状态信息
后台打印的事务状态,开始前是6,开始后是0,搜索了相关资料,参照[1]发现事务并未被正常开启。
序号 | 事务状态 | 备注 |
---|---|---|
1 | 0 | 事务尚未完全初始化 |
2 | 1 | 事务已初始化但尚未启动 |
3 | 2 | 事务处于活动状态 |
4 | 3 | 事务已结束。该状态用于只读事务 |
5 | 4 | 已对分布式事务启动提交进程 |
6 | 5 | 事务处于准备就绪状态且等待解析 |
7 | 6 | 事务已提交 |
8 | 7 | 事务正在被回滚 |
9 | 8 | 事务已回滚 |
JTA要点
虽然了解到,事务的状态,得知事务并未被正常启动,但依然不清楚原因在哪,然后我又查了JTA UserTransaction的相关信息,然后另一篇博客解开了我的疑惑:[2]
要想使用用 JTA 事务,那么就需要有一个实现
javax.sql.XADataSource
、javax.sql.XAConnection
和javax.sql.XAResource
接口的 JDBC 驱动程序。一个实现了这些接口的驱动程序将可以参与 JTA 事务。一个XADataSource
对象就是一个XAConnection
对象的工厂。XAConnection
是参与 JTA 事务的 JDBC 连接。要使用JTA事务,必须使用
XADataSource
来产生数据库连接,产生的连接为一个XA连接。XA连接(
javax.sql.XAConnection
)和非XA(java.sql.Connection
)连接的区别在于:XA可以参与JTA的事务,而且不支持自动提交。
根据上面的描述,要正常开启JTA事务,那么我的数据库驱动必须是XA的相关驱动,但是由于系统W核心数据库驱动不能是XA,所以事务并未正常开启,由于事务没有正常开启,那么最后在事务最后提交的时候,自然会报事务不存在,到这里,所有问题都解决了。我只能放弃JTA事务处理方式,将另一个数据库也换成非XA驱动,然后问题解决了。
但新问题又来了,后续该如何合理控制事务,确保事务一致,这个问题还需要进一步处理。当然,也进一步说明,原来将代码移植过来的时候,事务本身就有问题,多数据源事务问题一直没有被解决,所以才会产生此次乌龙事件。
这里,我想再分享以下分布式事务相关的知识点。
分布式事务
如果你有过weblogic的使用经历,那么你一定在weblogic控制台看到过这样的配置页面:
虽然我经常使用weblogic,但是对weblogic事务处理机制却不是特别清楚,更不了解支不支持全局事务有什么影响,也不清粗仿真两阶段、一阶段提交、记录上一个资源有什么区别,大概是过惯了从来如此的生活,觉得项目不出现问题就行了。所以,我要感谢这次这个问题,让我有机会来了解这些问题,今天我就是想通过自己收集资料来搞清楚这些问题。
事务从本质上讲就是为了保证数据的一致性,分布式事务也一样:
分布式事务是指会涉及到操作多个数据库的事务。其实就是将对同一库事务的概念扩大到了对多个库的事务。目的是为了保证分布式系统中的数据一致性。分布式事务处理的关键是必须有一种方法可以知道事务在任何地方所做的所有动作,提交或回滚事务的决定必须产生统一的结果(全部提交或全部回滚)
XA规范
X/Open 组织(即现在的 Open Group )定义了分布式事务处理模型。 X/Open DTP 模型( 1994 )包括应用程序( AP )、事务管理器( TM )、资源管理器( RM )、通信资源管理器( CRM )四部分。一般,常见的事务管理器( TM )是交易中间件,常见的资源管理器( RM )是数据库,常见的通信资源管理器( CRM )是消息中间件。 通常把一个数据库内部的事务处理,如对多个表的操作,作为本地事务看待。数据库的事务处理对象是本地事务,而分布式事务处理的对象是全局事务。
所谓全局事务,是指分布式事务处理环境中,多个数据库可能需要共同完成一个工作,这个工作即是一个全局事务,例如,一个事务中可能更新几个不同的数据库。对数据库的操作发生在系统的各处但必须全部被提交或回滚。此时一个数据库对自己内部所做操作的提交不仅依赖本身操作是否成功,还要依赖与全局事务相关的其它数据库的操作是否成功,如果任一数据库的任一操作失败,则参与此事务的所有数据库所做的所有操作都必须回滚。
一般情况下,某一数据库无法知道其它数据库在做什么,因此,在一个 DTP 环境中,交易中间件是必需的,由它通知和协调相关数据库的提交或回滚。而一个数据库只将其自己所做的操作(可恢复)影射到全局事务中。
XA 就是 X/Open DTP 定义的交易中间件与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。 XA 接口函数由数据库厂商提供。
二阶提交协议和三阶提交协议就是根据这一思想衍生出来的。可以说二阶段提交其实就是实现XA分布式事务的关键(确切地说:两阶段提交主要保证了分布式事务的原子性:即所有结点要么全做要么全不做)
两阶段提交
参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)。
准备阶段
事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。
具体步骤如下:
-
1)协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
-
2)参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
-
3)各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。
提交阶段
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)
-
1)协调者节点向所有参与者节点发出”正式提交(commit)”的请求。
-
2)参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
-
3)参与者节点向协调者节点发送”完成”消息。
-
4)协调者节点受到所有参与者节点反馈的”完成”消息后,完成事务。
回滚操作
两阶段的缺点
1、同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
2、单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
3、数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。
4、二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
三阶段提交
三阶段提交(Three-phase commit),也叫三阶段提交协议(Three-phase commit protocol),是二阶段提交(2PC)的改进版本。
与两阶段提交不同的是,三阶段提交有两个改动点。
1、引入超时机制。同时在协调者和参与者中都引入超时机制。
2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
以上分布式内容参考:[3]