总结自己在项目中实现分布式事务的设计思路

序言

中国移动的BOSS项目(业务运营支撑系统)需要做异地机房改造,即要把原本部署在单个机房的系统集群再部署一套到新的机房(原有机房称为南基房,新增加的机房称为北机房)。这么做并不是为了实现异地容灾,而是因为随着业务量不断增大(目前6亿用户,预计未来半年将增长至8.5亿),现有机房已经无法分配新的服务器以供扩容使用,需要将一部分数据库中的数据从南基房迁移至北机房。并且由于机房两地使用的网络不是专网而是内部公网,可能存在网络不稳定的情况,需要两套系统独立部署,当新机房网络不通时不会影响原机房的业务,加上网络策略和安全策略的要求不得不分拆为两套独立的微服务系统,最终部署方案如下图所示:
由图示和实际业务分析,有以下几点需要考虑:

  1. 数据按照省份拆分,分别存储在不同机房的数据库,存在一些公共数据,当某业务处理时,两个机房都需要同步更新处理结果。
  2. 机房间的网络是隔离的,跨机房访问时只能通过物理负载均衡F5路由,所以机房需要单独部署注册中心和配置中心,无法共用一套。
  3. 实际业务比较复杂,BOSS系统需要处理异地补卡或开销户的业务涉及更新南北机房的数据。比如用户A在广东办理北京手机卡的补办或者销户,按照当前的部署方案,这个请求将会发送到南机房处理,但是A用户的数据存储在北机房,南基房通过调用北机房的DB公共服务来更新北机房数据。


    image

    显然,异地机房改造是涉及分布式事务的,于是查阅了不少这方面的资料,打算引入阿里的分布式事务框架seata,但是seata要求涉及分布式事务的应用都要以微服务的形式使用相同的注册中心和配置中心,这与上述第二点相违背。引入seata失败后,决定自己参考seata的一些实现原理自己实现一套简单可用的分布式事务,不强制依赖微服务体系。

实现思路

关于seata的官方介绍
官方文档:https://seata.io/zh-cn/docs/overview/what-is-seata.html
Github地址:https://github.com/seata/seata
因为我的实现思路大体上是参考了seata的AT 模式,所以有必要先了解seata是如何实现的。Seata的整体机制是两阶段提交协议的演变:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:

  • 提交异步化,非常快速地完成。
  • 回滚通过一阶段的回滚日志进行反向补偿。
    那么如果按照这个思路,关键是要实现生成回滚记录。

生成回滚记录

有了每一步操作对应的回滚记录就能做回滚。根据seata实现思路,需要先解析正向sql,生成对应的select查询语句,执行select语句获取beforeImage(变更前的数据)。执行完正向sql后再执行一遍select语句获得afterImage(变更后的数据),将这两个数据存储到一张名为undo_Log的表中,有了beforeImage和afterImage,自然就能对数据做回滚了。

TableRecords beforeImage = beforeImage();
T result = statementCallback.execute(statementProxy.getTargetStatement(), args);
TableRecords afterImage = afterImage(beforeImage);
prepareUndoLog(beforeImage, afterImage);

而我的思路是直接存储用于回滚执行的sql,而不是存储beforeImage和afterImage
,并且存储在缓存中而不是直接存到undo_log表,只有执行回滚失败后才存到undo_log表。这样将大大提升执行效率。


image

BOSS服务使用统一封装的RemoteDBCaller(跨机房数据库调用器),将sqlid和参数对象封装成请求,调用异地机房DB公共服务,由DB公共服务完成sql操作并返回执行结果。因此在执行sql操作前生成对应的回退sql是完全可行的。可以使用sqping的AOP对RemoteDBCaller做一个切面,在真正发起调用前,取出sqlid和参数对象,根据sqlid和对象生成具体将要执行的sql,再生成对应的回退sql。

具体如何根据正向sql生成反向的回滚sql呢?接着这里面其实有不少需要注意的地方。这里只对update进行详细说明,insert和delete是一样的原理。正向sql分为三类:

  • 正向是update,回滚是update。
  • 正向是insert,回滚是delete。
  • 正向是delete,回滚是insert。

生成update回滚sql

update语句形如:

update table set fieldA={afterValue},fieldB={afterValue} where FieldC='xx' and FieldA='xx';

先解析获取where条件和表名,拼接查询语:select * from table where FieldC='xx' and FieldA='xx';执行该语句可获取beforeImage。结合该表的主键生成用于回滚的update语句:

update table set fieldA={beforeValue},fieldB={beforeValue} where Primary_key={Id};

image

update的回滚是有局限性的,只适用于有主键的表。因为primary_key作为主键是不会变更的,非主键的字段值都可能在执行完update后改变,只有根据primary_key可以定位到涉及变更的数据。

事务流程控制

seata的事务控制过程如下图所示:


image

1)TM:事务的发起者。用来告诉 TC,全局事务的开始,提交,回滚。
2)RM:具体的事务资源,每一个 RM 都会作为一个分支事务注册在 TC。
3)TC 事务的协调者。也可以看做是 Fescar-server,用于接收我们的事务的注册,提交和回滚。

在seata中,seata-service作为TC管理全局事务,所有RM都需要和它通信,注册分支事务,上报事务状态。而PBOSS(因为机房间网络隔离)为了避免需要与所有RM通信,由TM兼任TC,事务控制过程如下图:


image

TC并不控制全局的事务,只控制它下级的事务。比如有ABC三个服务A调用B,B又调用C,那么A会记录B的undo_log,B记录C的undo_log。当C异常时,会触发B调用DB公共服务执行C的回滚sql,同时触发A调用DB公共服务执行B的回滚sql,从而完成全局事务的回滚。

完整程序流程图

结合自定义注解、spring aop实现非侵入式的全局事务控制,与seata类似,通过在方法中使用注解@GlobalTransactional开启全局事务即可。


你可能感兴趣的:(总结自己在项目中实现分布式事务的设计思路)