一块儿叒来看分布式事务

事务是指将全部涉及到的操做放到一个不可分割的执行单元内. 一个事务内的全部操做, 要么所有都执行, 要么所有都不执行. 这就是事务的通俗理解.html

通常来讲, 事务都是针对数据库而言, 可是其实并非,一些消息队列例如RocketMq, kafka等也会涉及到事务. 这些组件有个专门的术语, 叫资源管理器(Resource Manager, 即RM)java

分布式事务,是随着分布式系统应用愈来愈普遍的过程当中衍生出来的一个新概念,通常是指RM在不一样的节点上.在微服务大行其道的今天, 分布式事务愈来愈值得被重视.mysql

本地事务

本文是想介绍分布式事务的, 可是要说分布式事务, 本地事务又是一个绕不开的话题. 因此咱们这里迅速过一下本地事务的相关概念.算法

ACID

ACID是事务必须具有的四个特性.其中分别是:spring

1. A是指原子性. 是指一个操做必须是一个不可分割的单元, 要么执行要么不执行, 不能存在指执行了一半,另一半没执行的状态
2. C是指一致性. 是指事务执行先后,系统都处在一个一致性的状态.
3. I是指隔离性. 隔离性是指不一样事务之间应该互相隔离,不受影响
4. D是持久性, 表示事务的执行应该是永久性的, 不能由于系统重启或奔溃就丢失
复制代码

通常地,ACID中的I又会引出另一组概念: 可见性问题和隔离等级.sql

可见性问题是指因为一个事务内的操做在另一个事务中的可见性而带来的问题. 通常来讲,可见性越高, 带来问题的可能性就越大.数据库

可见性从高到低, 问题有如下几种:编程

1. 读未提交. 一个事务能读到另一个事务未提交的更改.这是最严重的问题的,未提交的数据都是脏数据.

2. 不可重复读. 所谓的不可重复读是指一个事务第一次读某条记录
和第二次读同一条记录时会读到不同的内容.
缘由是该事务在这两次读之间, 有另一个事务更新了这条记录,而且提交了.

3. 幻读. 所谓幻读是指一个事务在两次读同一份数
据时, 第一次和第二次读到的数量不同. 缘由是该事务在这两次读之间,
有另一个事务新增/删除了记录, 并提交了.


能够看到, 其实不可重复读和幻读都是因为另一个事务更改了数据形成的.
二者的差异是另一个事务的操做是update仍是insert/delete
复制代码

隔离等级和可见性问题是遥相呼应的, 每一个隔离等级的存在都是为了解决掉可见性问题api

隔离等级有如下几种:bash

1. 读未提交. 这是最低级别的隔离等级, 很明显,什么可见性问题都没解决.
2. 读已提交. 解决了"读未提交"的问题
3. 可重复读. 解决"不可重复读"的问题
4. 串行化, 解决了"幻读"的问题.
复制代码

固然, 越高的隔离等级意味着越低的处理数据的吞吐量.

mysql中的事务

在mysql中, 能够用 begin, commit, rollback三个指令来实现事务.

1. begin用来开始一个事务.
2. commit 用来提交一个事务
3. rollback用来回滚一个事务
复制代码

mysql的事务是默认自动提交的.可使用

set autocommit = 0
或
set autocommit = 1
复制代码

来关闭/开启自动提交.

值得一提的是, mysql默认的隔离等级是"可重复读", 可是高版本的innodb(mysl5.7)实际上是经过间隙锁达到了"串行化"的标准了的.

spring中的事务

spring是支持事务操做的话, 可是spring中的事务操做其实都只是个代理, 最终都是依赖数据库的begin, commit, rollback实现的.

编程式事务

编程式事务是指经过transactionTemplate和TransactionManager来手动控制commit和rollback的事务.

编程式事务相对于声明式事务而言, 灵活度更高, 例如能够针对某个代码段提交或回滚.

声明式事务(代理)

声明式事务通俗来讲就是注解事务, 经过把spring的@Transactional注解添加到方法或类上来声明一个事务, 所以得名"声明式事务".

@Transactional经常使用的参数有:

1. propagation, 指定事务的传播等级
2. isolation, 指定隔离等级
3. norollback for, 指定不回滚事务的异常
4. rollback for, 指定须要回滚事务的异常
5. timeout, 指定事务的超时时间

复制代码

声明式事务的原理是动态代理和AOP, 简单来讲就是在具体的方法执行先后加上begin, commit, rollback的逻辑.

声明式事务最大的好处就是简单, 对代码侵入性低.对应的缺点就是粒度很差控制, 最小的粒度也是要加到方法上.

事务的传播(嵌套事务)

事务的传播通俗地来说, 就是多个方法的调用链中, 若是涉及到事务的嵌套, spring应该如何处理.

这个概念也是由声明式事务的原理而引伸出来的. 声明式事务的原理就是由动态代理在方法的先后加上开启事务和提交事务的逻辑.

假设存在如下的一种场景:

@Transational
public void A(){
    B();
    // do something
}
@Transational
public void B(){
    //dosomething
    int a = 1/0
}
复制代码

上面两个方法都声明开启事务, 很明显B是会抛出异常的, B的事务会被回滚. 那么A会不会也被回滚呢. 这就须要用事务的传播机制来解决了.

spring的事务传播机制一共有7种:

1. propagation_require. 默认的传播类型. 表示当前方法须要再一个事务中执行, 若是没有开启事务, 则须要开启一个

2. propagation_support. 表示当前方法不须要事务, 可是若是事务存在,则在事务中执行

3. propagation_mandatory. 表示当前方法必需要在事务中执行, 若是不存在,则抛出异常.

4. propagation_requireNew. 表示当前方法须要在新的事务中执行.当前方法执行时, 若是已经存在一个事务, 则先挂起该事务

5. propagation_not_support. 表示当前方法不支持事务, 若是已经存在事务, 那就先挂起该事务.

6. propagation_never. 表示当前方法不该该在事务中执行, 若是存在事务, 则抛异常.

7. propagation_nested. 若是存在嵌套的事务, 那么各个方法在各自独立的方法里面提交和回滚.
复制代码

分布式事务

DTP, XA和JTA

DTP模型

DTP(Distributed Transaction Processing)是x/open组织提出来的分布式事务的模型.

一个DTP模型至少包含如下三个元素:

1. AP, 应用程序,用于定义事务开始和结束的边界. 说人话就是咱们开启事务的代码因此的应用.
2. RM, 资源管理器. 理论上一切支持持久化的数据库资源均可以是一个资源管理器.
3. TM, 事务管理器, 负责对事务进行协调,监控. 并负责事务的提交和回滚.

复制代码
XA规范

XA是x/open提出来的分布式事务的规范, 它是跟语言无关的.

XA规范定义了RM和TM交互的接口. 例如TM能够经过如下接口对RM进行管理:

1. xa_open和xa_close, 用于跟RM创建链接
  2. xa_star和xa_end, 开始和结束一个事务
  3. xa_prepare, xa_commit和xa_rollback, 用于预提交, 提交和回滚一个事务
  3. xa_recover 用于回滚一个预提交的事务
复制代码
JTA规范

JTA规范是能够认为是XA规范java语言实现版的规范.

JTA定义了一系列分布式事务相关的接口:

1. javax.transaction.Status: 定义了事务的状态,例如prepare, commit rollback等等等
2. javax.transaction.Synchronization:同步
3. javax.transaction.Transaction:事务
4. javax.transaction.TransactionManager:事务管理器
5. javax.transaction.UserTransaction:用于声明一个分布式事务
6. javax.transaction.TransactionSynchronizationRegistry:事务同步注册
7. javax.transaction.xa.XAResource:定义RM提供给TM操做的接口
8. javax.transaction.xa.Xid:事务id
复制代码

以上不一样的接口由不一样的角色(RM, RM等)来实现.

二阶段提交(2PC)

二阶段提交是最简单的分布式事务解决方案. 它把一个事务分红request commit和commit/rollback两个阶段组成.

第一阶段是请求阶段, 由协调者向因此的RM询问事务是否能够提交. 若是能够提交则回复YES,不然回复NO.

第二阶段是提交阶段, 协调者根据全部的RM的响应来决定该分布式事务是否能够提交. 若是全部的RM都回复了YES, 则能够提交,不然回滚该事务.

二阶段提交思想虽然简单, 可是它存在很是多的问题.

  1. 协调者单点问题
  2. 第一阶段的阻塞问题
  3. 第二阶段因为网络问题, RM没有收到commit/rollback指令而致使数据不一致的问题.

三阶段提交(3PC)

三阶段提交是为了解决二阶段算法存在的问题而提出来的.它把事务的提交分红3个阶段:

1. cancommit阶段, 和2PC中的请求阶段相似
 2. precommit阶段. 若是cancommit阶段不是所有响应YES或者有RM超时, 那么回滚整个事务. 
 不然, 发送precommit指令, 让各个RM执行事务操做,执行完后响应ACK.
 3. docommit阶段.若是precommit阶段由RM没有响应ACK或者超时, 那么回滚整个事务.
 不然发送docommit指令, 让各个RM真正提交事务.
复制代码

TCC

TCC是指try-comfirm-cancel.是这些年来大火的一种柔性分布式事务解决方案.

所谓"柔性", 是针对2PC和3PC等"刚性事务"而言的. 柔性事务再也不一味追求强一致性, 只要求最终一致性.

TCC把一个分布式事务拆成如下三个步骤:

1. try阶段. 各个事务参与者检查业务一致性, 预留系统资源.例如锁定库存
2. comfirm阶段. 事务参与者使用try阶段预留的资源,执行业务操做.
3. cancel阶段. 若是try阶段任意一个事务参与者try失败, 则作cancel操做. cancel包括释放资源和反向补偿
复制代码

其实仔细一看, T-C-C是恰好跟2PC中的request-commit-rollback一一一对应的.从这点上看, TCC本质上也是一个2PC思想的解决方案.

在TCC中还有两个概念, 主业务服务和从业务服务.

主业务服务能够通俗地理解成发起事务的那个服务.例如一个购买的服务, 它分别调用库存服务和订单服务. 那么购买服务就能够看作是主业务服务.

对应地, 上面所说的"库存服务"和"订单服务"就是从业务服务.

为何要先区分这两种服务呢? 由于它们的职责是不同的:

1. 从业务服务必需要提供try, comfirm, cancel方法.
2. 主业务服务须要记录事务日志, 并在事务管理器的协调下, 适当地调用从业务服务的tcc三个方法.
复制代码

TCC的模型以下图所示:

image

图片来自www.tianshouzhi.com/api/tutoria…, 同时也极力推荐这个博客, 受益不浅.

消息队列

利用消息队列来实现最终一致性是另一种柔性分布式事务的思想. 它的主要思想经过消息队列异步完成一个分布式事务, 结合定时任务作重试和补偿, 必要的时候须要人工介入.

总结地来讲, 一共有"尽最大努力通知", "本地消息表"和"MQ事务消息"三种思想.

尽最大努力通知

尽最大努力通知就是主动通知方会尽最大努力把处理结果通知到接收方, 若是通知失败,会作最多X次重试.若是最终仍是失败, 主动方提供了查询的接口, 能够由接收方主动查询.

这种思想是最简单的, 其实应用的也是比较多的.典型的有:

1. 运营商短信发送状态回传
2. 微信和支付宝支付状态回传
复制代码
本地消息表

顾名思义, 本地消息表就是利用一个本地数据库维护事务完成的中间状态. 在分布式事务执行的过程当中,各方事务参与者完成操做后更新消息表的状态,逐步完成一个总体的事务.

对于异常的状况, 由定时调度定时检测消息表中未完成的事务, 发起重试. 定时调度的解决方案见java中执行定时任务的6种姿式

若是最终仍是有一方未能完成事务操做,则由人工介入进行补偿.

image

若有上面一张图:

1. 生产者先写本地消息表和业务数据, 用本地事务保证成功.再发送MQ消息.
  
  2. 消费者消费数据,一样是执行本地事务. 成功后更新本地消息表的状态. 失败怎么办呢? 能够发送消息给生产者进行回滚, 可是那样复杂度
  就高了(要求生产者也要实现TCC, 那就还不如用TCC了). 因此更现实的方案是重现+人工补偿
  
 3. 生产者可能会写业务数据成功, 可是发送MQ消息失败, 这个时候本地消息表仍是会有对应未完成的事务, 那么定时任务会扫描出来, 重试.最终仍是能完成整个分布事务.
 
复制代码

固然, 上图也不是百分百完善的 可是本地消息表更多的只是一种思想, 具体实现可能会有所不一样,也要结合具体的业务场景和业务要求来实现.

MQ事务消息

本地消息表的方案就在MQ广泛都尚未实现事务消息的时候提出的. 可是如今无论是kafka仍是rocketMQ都开始支持事务消息了.

有了事务消息, 其实本地表和定时任务的工做就由MQ的事务机制来完成了.

例如www.tianshouzhi.com/api/tutoria… 里面介绍的方案.

分布式事务框架

在实际的应用中, 分布式事务出现的场景能够总结为两种. 仍是以一个购买服务为例, 那么这两种分布式事务的场景多是:

  1. 第一种, 同一个服务中对多个RM进行操做

image

  1. 第二种, 一个服务经过RPC调用多个服务, 间接操做了多个RM

image

在微服务化大行其道的今天,按业务分库应该是大多公司搭建架构的一个基本准则了. 因此这样来看, 貌似是第二种场景更符合实际了.

固然第一种场景确定也仍是有存在的. 例如上面"本地消息表"的解决方案中, 就有须要再同一个服务中跟多个RM交互.

分布式事务开源框架其实市面上也挺多的,例如tcc-transactio等等, 这里咱们来看看atomikos和seata这两个

atomikos

atomikos是一个很是有名的分布式事务开源框架. 它有JTA/XA规范的实现, 也有TCC机制的实现方案, 前者是免费开源的, 后者是商业付费版的.

这里介绍一下JTA/XA规范的实现.

上面JTA规范那一小节说到JTA定义了一系列的接口,那些接口是由不一样的角色去实现的. atomikos的角色是一个事务管理器, 它实现的接口主要有:

1. javax.transaction.UserTransaction
   对应的实现是com.atomikos.icatch.jta.UserTransactionImp,用户只须要直接操做这个类就是实现一个JTA分布式事务

 2. javax.transaction.TransactionManager
   对应的实现是com.atomikos.icatch.jta.UserTransactionManager, atomikos使用这个实现类来对事务进行管理

 3. javax.transaction.Transaction
    对应的实现是com.atomikos.icatch.jta.TransactionImp
复制代码

应用atomikos的简单实例(仍是来自www.tianshouzhi.com/api/tutoria…):

  1. 引入依赖
<dependency>
   <groupId>com.atomikos</groupId>
   <artifactId>transactions-jdbc</artifactId>
   <version>4.0.6</version>
</dependency>
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>5.1.39</version>
</dependency>
复制代码
  1. demo实例
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.jdbc.AtomikosDataSourceBean;

import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;

public class AtomikosExample {

  private static AtomikosDataSourceBean createAtomikosDataSourceBean(String dbName) {
     // 链接池基本属性
     Properties p = new Properties();
     p.setProperty("url", "jdbc:mysql://localhost:3306/" + dbName);
     p.setProperty("user", "root");
     p.setProperty("password", "your password");

     // 使用AtomikosDataSourceBean封装com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
     AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
     //atomikos要求为每一个AtomikosDataSourceBean名称,为了方便记忆,这里设置为和dbName相同
     ds.setUniqueResourceName(dbName);
     ds.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");
     ds.setXaProperties(p);
     return ds;
  }

  public static void main(String[] args) {

     AtomikosDataSourceBean ds1 = createAtomikosDataSourceBean("db_user");
     AtomikosDataSourceBean ds2 = createAtomikosDataSourceBean("db_account");

     Connection conn1 = null;
     Connection conn2 = null;
     PreparedStatement ps1 = null;
     PreparedStatement ps2 = null;

     UserTransaction userTransaction = new UserTransactionImp();
     try {
        // 开启事务
        userTransaction.begin();

        // 执行db1上的sql
        conn1 = ds1.getConnection();
        ps1 = conn1.prepareStatement("INSERT into user(name) VALUES (?)", Statement.RETURN_GENERATED_KEYS);
        ps1.setString(1, "tianshouzhi");
        ps1.executeUpdate();
        ResultSet generatedKeys = ps1.getGeneratedKeys();
        int userId = -1;
        while (generatedKeys.next()) {
           userId = generatedKeys.getInt(1);// 得到自动生成的userId
        }

        // 模拟异常 ,直接进入catch代码块,2个都不会提交
//        int i=1/0;

        // 执行db2上的sql
        conn2 = ds2.getConnection();
        ps2 = conn2.prepareStatement("INSERT into account(user_id,money) VALUES (?,?)");
        ps2.setInt(1, userId);
        ps2.setDouble(2, 10000000);
        ps2.executeUpdate();

        // 两阶段提交
        userTransaction.commit();
     } catch (Exception e) {
        try {
           e.printStackTrace();
           userTransaction.rollback();
        } catch (SystemException e1) {
           e1.printStackTrace();
        }
     } finally {
        try {
           ps1.close();
           ps2.close();
           conn1.close();
           conn2.close();
           ds1.close();
           ds2.close();
        } catch (Exception ignore) {
        }
     }
  }
}
复制代码

很明显, 这个例子是属于场景1的分布式事务. 因此若是有场景1的分布式事务的话, 直接使用atomikos就能够了, 简单直接高效.

可是话又说回来了, 实际场景的分布式事务更多的仍是属于场景2的. 很明显简单的JTA事务是处理不了场景2的分布式事务的.场景2下的分布式事务, 还得须要像TCC或消息队列柔性事务等解决方案去实现.

seata

seata就是Fescar(TXC/GTC/FESCAR)和tcc-transaction整合后开源的一个分布式事务落地解决方案框架,实现了AT, TCC, SAGA三种模式, 大有一统江湖的意思.

官网地址是seata.io/zh-cn/docs/…, 文档方面相对来讲还不够完善, 可是做为了解仍是足够了. 这里也是简单地介绍一下.

术语

TC - 事务协调者 维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM - 事务管理器 定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM - 资源管理器 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

AT模式

AT 即Automatic Transaction, 所谓AUTO, 表示是这种模式是对业务无侵入的, 不须要业务改造.可是对业务有要求:

1. 基于支持本地 ACID 事务的关系型数据库。
2. Java 应用,经过 JDBC 访问数据库。
复制代码

AT模式整体逻辑以下图:

image

AT模式采用的也是2PC的思想, 加入了补偿的机制, 补偿的机制跟innodb里面的undo日志相似.

undo日志其实就是一个反向补偿, 例如insert的语句, 事务回滚时,会执行一个对应的delete语句

用大白话翻译了一下模式(个人理解)就是:

1. 第1阶段, 先生成undo日志, undo日志和业务的操做在本地事务中一并提交
2. 第2阶段, 在TC的协调下, 若是能够提交则迅速提交. 须要回滚时根据回滚日志作反向补偿.
复制代码

固然具体应用没有那么简单, 更多的参考官网

TCC模式

TCC模式就是上面介绍的TCC的思想, SEATA的tcc模式以下图:

image

TCC模式其实跟AT模式也是相似的, 也是一个2PC的演化版, 在事务协调器(TC)的协调下, 进行多个子事务的提交和回滚.

不一样的是, AT模式回滚是在数据库资源层面的补偿(执行回滚日志), 而TCC是调用自定义的逻辑进行回滚(执行回滚代码逻辑).

SAGA模式

saga是一种长事务解决方案. 在Saga模式中,业务流程中每一个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现

image

saga的思想虽然在1987年提出来了, 可是seata的saga模式是今年的8月份才正式支持的.我对它的理解也不够深刻,因此也不在班门弄斧了.了解一下便可

总结

分布式系统历来就不是一个简单的概念, 分布式系统中的分布式事务更是如此.

也许分布式事务的思想算是比较简单, 可是实现起来的确有不少的细节和困难须要咱们去注意和克服.于是大多数据公司企业都会有根据本身的业务实际去作不一样的实践, 而不是完彻底全地照搬思想.

这一点体现出来的另一面就是, 如今市面上确实也没有一个完善的分布式解决方案, 能让咱们照搬就能够了.阿里的seata开源也不久, 但愿有一天, 它真的能一统江湖, 真正的能够一次性一站式地解决分布式事务的问题

引用

www.tianshouzhi.com/api/tutoria…

seata.io/zh-cn/docs/…

相关文章
相关标签/搜索