本地事务
本地事务,即单体应用中单个线程内对一个数据库的事务提交。例如充值操作,充值成功后订单表状态更新为成功,账户表加钱,对应的数据库更新操作如下。
begin transaction
update 订单表 set 状态='成功' where id = 110;
update 账户表 set 余额=余额+10 where id = 111;
commit
相应的程序代码也很简单:
@Transactional
public void update() {
update订单表();
update账户表();
}
本地事务示例图如下所示。
本地事务可以保证单体应用内对同一数据库操作的ACID特性,但是对分布在不同物理节点上的服务进行数据操作时,本地事务已经不能满足需求了。
分布式事务
分布式事务,事务操作跨多个服务、多个服务器、多个数据库。简单的说,就是将一次大的数据操作分成很多小操作,而这些小操作分别在属于不同应用的服务器上执行,这些小操作要么都执行成功,要么都失败。
充值成功后的操作在分布式环境下的示例图如下所示。
交易平台远程调用订单系统更新订单状态、远程调用账户系统给账户加钱,这两个操作要么都成功,要么都失败,保证数据的一致性。
XA规范
介绍两阶段提交之前,了解一下XA规范。
XA规范是开放群组关于分布式事务处理(DTP)的规范。规范描述了全局的事务管理器与局部的资源管理器之间的接口。XA规范允许多个资源(如数据库,消息队列等)在同一事务中访问,这样可以使ACID属性跨越应用程序而保持有效。XA使用两阶段提交来保证所有资源同时提交或回滚任何特定的事务。
通俗的意思就是XA规范规定了处理分布式事务的规则,通过两阶段提交保证分布式事务数据的一致性。
两阶段提交
两阶段提交(Two-phase Commit,缩写为2PC),即为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法。通常,两阶段提交也被称为是一种协议。
对于分布式系统来说,每个服务节点虽然能够清楚的掌握本身操作的成功与失败,但不能知道其它服务节点相应操作的执行结果。分布式事务中,一个事务往往需要跨多个服务节点,所以,为了保证事务的ACID特性,需要引入一个协调者来掌握所有参与事务的参与者的执行情况,并根据参与者的执行结果来决定是否把一系列操作进行最终的提交。两阶段提交算法简单的描述就是,第一阶段是准备阶段,第二阶段是提交阶段,下面分别详细描述这两个阶段。
第一阶段-准备阶段,过程如下图所示。
具体描述过程如下:
协调者向所有参与者询问是否可以执行提交操作,然后开始等待各参与者节点的响应;
参与者收到询问后开始事务执行的准备工作,如资源锁定、预留资源、将Undo信息和Redo信息写入日志等;
各参与者准备工作完成后,响应协调者。如果参与者的准备工作执行成功,则返回一个"同意"消息;如果参与者准备工作执行失败,则返回一个"中止"消息。
第二阶段-提交阶段,过程如下图所示。
当第一阶段所有参与者响应的消息都是“同意”时:
协调者向所有参与者发送"提交"的请求;
参与者收到提交请求后,完成提交操作,并释放在整个事务期间所内占用的资源;
参与者向协调者响应"完成"消息。
协调者收到所有参与者响应的"完成"消息后,事务完成。
当第一阶段中有参与者响应的消息是“终止”,或有参与者未在超时时间内给出响应,则:
协调者向所有参与者发出"回滚"的请求;
参与者收到回滚请求后,使用之前写入的Undo信息执行回滚,然后释放在整个事务期间所占用的资源;
回滚完成后,参与者向协调者响应"回滚完成"消息;
协调者收到所有参与者响应的"回滚完成"消息后,事务取消完成。
通过上面的描述,可以得出两阶段提交的缺点:
协调者需要和所有参与者进行多次通信,通信时间太长,事务的处理时间变长了;
在这个过程中,需要长时间的锁定资源,导致其它操作阻塞;
协调者出现故障时,容易出现单点故障问题,可能导致数据不一致的问题。
JTA分布式事务
JTA(Java Transaction API)是符合DTP模型的,在JavaEE平台下JTA可以用JTS协作XA的数据源实现两阶段提交,WebLogic、Webshare等主流商用的应用服务器提供了JTA的实现和支持。而Tomcat没有实现,这就需要借助第三方的框架Jotm、Automikos等来实现,两者均支持Spring事务整合。
但是,JTA方式分布式事务存在很严重的性能问题,不适合高并发和高性能要求的场景。大部分高并发服务都在避免使用分布式事务,往往通过其他途径来解决数据一致性问题,后续篇幅将记录如何用消息系统避免分布式事务,解决数据一致性问题。