最近几天都在研究分布式事务, 当然, 研究是官方语言, 其实也都是在学习前人的经验, 本文姑且做个整理.

### 什么是事务? [百度百科][1]的解释一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit). 简单粗暴点: 就是在顺序昨晚好几件事情后, 才认为这件事情是完成了的. 比如转账(不要笑话我用这个老套的案例来说明, 谁让他简单好理解呢~): 账户A向账户B转账10元. 这个操作期间可能涉及到哪些操作呢? 1. 账户A减10元. 2. 账户B加10元. 3. 账户A记录一条转出记录. 4. 账户B记录一条转入记录.

上面是最基础的转账流程了. 不考虑其中的手续费等问题.
我们假设上面的四个操作有对应的方法来标识:

void decrease(A, 10);
void increase(B, 10);
void record(A, out, 10);
void record(B, in, 10);

这四个方法分别对应上面的四个步骤.
那么最原始的, 我们的系统还不是分布式的时候. 我们这么处理:

conn.setAutoCommit(false);
try{
    decrease(A, 10);
    increase(B, 10);
    record(A, out, 10);
    record(B, in, 10);
}catch(Exception e){
    try{
        conn.rollback();
    }catch(Exception e1){
        e1. printStackTrace();
    }
}finally{
    conn.setAutoCommit(true);
}
conn.commit();

这种情况使用了JDBC的事务处理, 完美地达到了数据库事务要求的ACID.
但是这种情况在分布式的系统中已经不适用了. 分布式系统中数据库和应用都是分开的. 完全不能这么简单地处理掉.
那要怎么办呢? 这才谈到我们要说的核心, 分布式事务.
分布式事务基本不可能完全地满足ACID了. 这个可以参考网上各位大神的文章. 具体原因就不扯了.
就在刚刚, 看到这篇文章: 分布式系统事务一致性到CAP,BASE理论, 里面对分布式事务的一致性追求讲的挺清晰的.
其实, 粗暴地总结一下. 分布式的事务处理目标就是可用, 并且能做到最终一致. 在达到最终一致的这个过程中, 尽可能地减少或者屏蔽掉脏读/幻读等问题.

那怎么在分布式事务中保持最终一致性呢?
等大家都做完了, 再统一提交. 这样就木有问题了么.
是呀, 找个东西做为数据中间件, 监控各个数据库操作的结果. 如果都ok, 那统一发送commit命令.要不然, 统一cancel掉. ok,问题解决了. 这种方案一般被称为两阶段提交(Two-Phase Commit), 简称2PC.

上面的假设都是理想化的. 当然, 问题都是要先往好的方面想, 然后再慢慢发现并解决其中可能发生的问题.
单举一例: 如果事务都要commit掉. 在commit通知发送出去的同时, 不管哪个服务挂掉. 不管哪个网络断掉, 都可能会引起脏数据的产生. 而我们认为, 在分布式的世界里, 网络是很不靠谱的一个东西. 所以, 我们要想办法解决这个问题.
再然后, 产生了三阶段提交(3PC). 引入了超时机制, 如果谁没有在规定时间内给回复, 就认为他挂了. 然后继续处理掉其他的相关事务.

但是, 不管是2PC还是3PC, 都没有很好地解决掉最终一致性, 说白了也就是会出现脏数据.
具体可以参见:关于分布式事务、两阶段提交协议、三阶提交协议.
再然后, Java作为一个那么那么大的平台, 搞出了一套标准.俗称JTA(Java Transaction API). 这个东西的实现依赖于Transaction Manager和Resource Manager.对这个感兴趣的可以看下这篇文章:JTA 深度历险 - 原理与实现. 这篇文章比较清楚地讲到了JTA的方方面面.
相对来说, JTA比较圆满地解决了分布式事务的最终一致性问题. 但是它对数据库有依赖, 具体为啥, 还是自己搜一下, 这个摊开就聊不完了.
其实还是有通过可靠的消息队列进行事务通知的, 挺多方案的.
我们重点提一下事务补偿. 当然, 继续简单理解:
先处理业务,然后定时或者在回调里检查状态是不是一致的,如果不一致采用某个策略,强制状态到某个结束状态(一般是失败状态),很典型的操作.
就相当于顺序4件事情, 做到3的时候, 发现2没做成功. 这个时候, 就回去处理一下2. 然后具体怎么处理, 其实可以根据业务来, 不一定就判死刑, 也可以重新做2, 直到成功. 只是这么干的成本有点大而已.
事务补偿机制用来处理分布式事务是一种挺不错的方案. 基本可以保证事务的最终一致了. 提一句, 有一种方案是基于消息队列的补偿机制, 这个相对来说会很慢, 所以--慎用...
事务补偿机制只能保证最终一致, 达不到实时的一致性, 那么如果补偿的过程中间有其他操作, 仍然会产生脏数据.
这个时候, TCC(Try-Confirm-Cancel)这种方案就出来了.
个人认为, 其实TCC是一种典型的2PC和事务补偿的综合体.
百特开源 这里有对TCC比较完整的描述.
简单归纳:
TCC将事务提交的过程分为了三部分.

  • Try: 做资源预留
  • Confirm: 在Try阶段资源预留的基础上直接执行业务. 一般认为, Confirm时候不用进行资源检查, 只要处理具体业务即可. 因为前面的Try阶段已经有针对性地进行了检查, 并且预留了资源.
  • Cancel: 失败, 回滚Try阶段的预留资源.

TCC有一个很大的优势就是和数据库没关系. 还可以保持事务一致性. 比如某个业务需要操作mysql和mongoDB, 甚至于还要写磁盘文件. 那么对磁盘文件这种, 没有事务概念的持久化怎么做到回滚呢? 只能是在业务上进行手工恢复. 那么, 这部分就可以在Cancel阶段有针对性地进行.

  • byteTCC: https://github.com/liuyangming/ByteTCC/ 这是一个TCC的开源实现, 但是他的本地事务还是依赖于数据库本身. 所以, 对数据库的依赖仍然存在.
  • LCN 分布式事务框架: https://github.com/1991wangliang/tx-lcn 这个是今晚刚才搜TCC的时候看到的. 看wiki应该和byteTCC差不太多. 仍然没有真正地做到脱离数据库.
  • tcc-transaction: https://github.com/changmingxie/tcc-transaction tcc的另一个开源实现. 他的本地事务提供了几种存放方式. 比如本地文件, 数据库, redis.
    相对来说, 比较忠于TCC本身.
    byteTCC和tcc-transaction用起来都很简单. 如果只是用起来, 看看官方的demo基本就ok了.
    但是, 如果要防坑, 用起来后还是要抽时间回去研究源码.

从22:00写道01:00了. 感觉有点烂尾了.
最后再写一点, 其实, 分布式事务如果可以在设计的时候避免掉是最好的了.
分布式事务不好处理, 这个大家都知道. 那么与其苦苦寻求方案, 为什么不能在设计之初就把他消灭掉呢? 除了金融,售票等一些特定的行业. 其实分布式事务这个东西完全可以在设计之初就扼杀掉.
不要说现在的微服务引发的分布式事务. 不能为了微服务而微服务. 一切技术都是服务于业务.
所以, 如果感觉烂尾或者有什么问题, 在博客底下留言, 如果对微服务感兴趣, 也欢迎留言交流.

上面的确实烂尾了, 今天补充一些关于JTA的东西.算是亡羊补牢吧.
分布式事务(Distributed Transaction)包括事务管理器(Transaction Manager)和一个或多个支持 XA 协议的资源管理器 ( Resource Manager )。

  • 资源管理器看做任意类型的持久化数据存储;
  • 事务管理器协调控制所有事务参与单元.
    附上一个典型的JTA处理:
public void transferAccount() { 
        
        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();
            
            // 将 A 账户中的金额减少 500 
            stmtA = connA.createStatement(); 
            stmtA.execute("
           update t_account set amount = amount - 500 where account_id = 'A'");
            
            // 将 B 账户中的金额增加 500 
            stmtB = connB.createStatement(); 
            stmtB.execute("\
            update t_account set amount = amount + 500 where account_id = 'B'");
            
            // 提交事务
            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(); 
        } 
    }

上面的代码引用自:JTA深度历险, 这篇文章对JTA的讲解很深刻. 再次强烈推荐.
可以看到,JTA将分布式事务给搞成了JDBC事务的样子(最起码看起来很像).
在这个基础上, 本来很难搞的分布式事务就变得相对来说简单了很多.
但是通过上面的代码同样可以看到, JTA使用起来还是挺麻烦的. 和JDBC的事务处理一样的麻烦.

推荐一个JTA的开源方案.byteJTA, 去试一试, 事务交给spring管理后, 一个注解就能搞定你想要的, 爽歪歪.

好了, 事务就说到这里了. 上次烂尾的教训以后得吸取. 下次一定不在半夜困倦的时候写文章了.
有问题欢迎留言, 随时交流.