好长时间没发文了,最近着实是有点忙,当爹的第 43 天,身心疲乏。这又赶上年底,公司冲 KPI
强制技术部加班到十点,早晨孩子隔两三个小时一醒,根本没睡囫囵觉的机会,天天处于迷糊的状态,孩子还时不时起一些奇奇怪怪的疹子,总让人担惊受怕的。
本就不多的写文章工夫又被有限宰割,哎~ 打工人真是太难了。
原本不晓得写点啥,正好手头有个新我的项目试着用阿里的 Seata
中间件做分布式事务,那就做一个实际分享吧!
介绍 Seata
之前在简略回顾一下分布式事务的基本概念。
分布式事务的产生
咱们先看看百度上对于分布式事务的定义:分布式事务是指事务的参与者、反对事务的服务器、资源服务器以及事务管理器别离位于不同的分布式系统的不同节点之上。
额~ 有点形象,简略的画个图好了解一下,拿下单减库存、扣余额来说举例:
当零碎的体量很小时,单体架构齐全能够满足现有业务需要,所有的业务共用一个数据库,整个下单流程或者只用在一个办法里同一个事务下操作数据库即可。此时做到所有操作要么全副提交 或 要么全副回滚很容易。
分库分表、SOA
可随着业务量的一直增长,单体架构慢慢扛不住微小的流量,此时就须要对数据库、表做 分库分表
解决,将利用 SOA
服务化拆分。也就产生了订单核心、用户核心、库存核心等,由此带来的问题就是业务间互相隔离,每个业务都保护着本人的数据库,数据的替换只能进行 RPC
调用。
当用户再次下单时,需同时对订单库 order
、库存库 storage
、用户库 account
进行操作,可此时咱们只能保障本人本地的数据一致性,无奈保障调用其余服务的操作是否胜利,所以为了保障整个下单流程的数据一致性,就须要分布式事务染指。
Seata 劣势
实现分布式事务的计划比拟多,常见的比方基于 XA
协定的 2PC
、3PC
,基于业务层的 TCC
,还有利用音讯队列 + 音讯表实现的最终一致性计划,还有明天要说的 Seata
中间件,下边看看各个计划的优缺点。
2PC
基于 XA 协定实现的分布式事务,XA 协定中分为两局部:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比方 Oracle、MYSQL 这些数据库都实现了 XA 接口,而事务管理器则作为一个全局的调度者。
两阶段提交(2PC
),对业务侵⼊很小,它最⼤的劣势就是对使⽤⽅通明,用户能够像使⽤本地事务⼀样使⽤基于 XA 协定的分布式事务,可能严格保障事务 ACID 个性。
可 2PC
的毛病也是不言而喻,它是一个强一致性的同步阻塞协定,事务执⾏过程中须要将所需资源全副锁定,也就是俗称的 刚性事务
。所以它比拟适⽤于执⾏工夫确定的短事务,整体性能比拟差。
一旦事务协调者宕机或者产生网络抖动,会让参与者始终处于锁定资源的状态或者只有一部分参与者提交胜利,导致数据的不统一。因而,在⾼并发性能⾄上的场景中,基于 XA 协定的分布式事务并不是最佳抉择。
3PC
三段提交(3PC
)是二阶段提交(2PC
)的一种改良版本,为解决两阶段提交协定的阻塞问题,上边提到两段提交,当协调者解体时,参与者不能做出最初的抉择,就会始终放弃阻塞锁定资源。
2PC
中只有协调者有超时机制,3PC
在协调者和参与者中都引入了超时机制,协调者呈现故障后,参与者就不会始终阻塞。而且在第一阶段和第二阶段中又插入了一个筹备阶段(如下图,看着有点啰嗦),保障了在最初提交阶段之前各参加节点的状态是统一的。
尽管 3PC
用超时机制,解决了协调者故障后参与者的阻塞问题,但与此同时却多了一次网络通信,性能上反而变得更差,也不太举荐。
TCC
所谓的 TCC
编程模式,也是两阶段提交的一个变种,不同的是 TCC
为在业务层编写代码实现的两阶段提交。TCC
别离指 Try
、Confirm
、Cancel
,一个业务操作要对应的写这三个办法。
以下单扣库存为例,Try
阶段去占库存,Confirm
阶段则理论扣库存,如果库存扣减失败 Cancel
阶段进行回滚,开释库存。
TCC 不存在资源阻塞的问题,因为每个办法都间接进行事务的提交,一旦出现异常通过则 Cancel
来进行回滚弥补,这也就是常说的补偿性事务。
本来一个办法,当初却须要三个办法来反对,能够看到 TCC 对业务的侵入性很强,而且这种模式并不能很好地被复用,会导致开发量激增。还要思考到网络稳定等起因,为保障申请肯定送达都会有重试机制,所以思考到接口的幂等性。
音讯事务(最终一致性)
音讯事务其实就是基于消息中间件的两阶段提交,将本地事务和发消息放在同一个事务里,保障本地操作和发送音讯同时胜利。
下单扣库存原理图:
- 订单零碎向
MQ
发送一条准备扣减库存音讯,MQ
保留准备音讯并返回胜利ACK
- 接管到准备音讯执行胜利
ACK
,订单零碎执行本地下单操作,为避免音讯发送胜利而本地事务失败,订单零碎会实现MQ
的回调接口,其内一直的查看本地事务是否执行胜利,如果失败则rollback
回滚准备音讯;胜利则对音讯进行最终commit
提交。 - 库存零碎生产扣减库存音讯,执行本地事务,如果扣减失败,音讯会从新投,一旦超出重试次数,则本地表长久化失败音讯,并启动定时工作做弥补。
基于消息中间件的两阶段提交计划,通常用在高并发场景下应用,就义数据的强一致性换取性能的大幅晋升,不过实现这种形式的老本和复杂度是比拟高的,还要看理论业务状况。
Seata
Seata
也是从两段提交演变而来的一种分布式事务解决方案,提供了 AT
、TCC
、SAGA
和 XA
等事务模式,这里重点介绍 AT
模式。
既然 Seata
是两段提交,那咱们看看它在每个阶段都做了点啥?下边咱们还以下单扣库存、扣余额举例。
先介绍 Seata
分布式事务的几种角色:
Transaction Coordinator(TC)
: 全局事务协调者,用来协调全局事务和各个分支事务(不同服务)的状态,驱动全局事务和各个分支事务的回滚或提交。Transaction Manager™
: 事务管理者,业务层中用来开启 / 提交 / 回滚一个整体事务(在调用服务的办法中用注解开启事务)。Resource Manager(RM)
: 资源管理者,个别指业务数据库代表了一个分支事务(Branch Transaction
),治理分支事务与TC
进行协调注册分支事务并且汇报分支事务的状态,驱动分支事务的提交或回滚。
Seata 实现分布式事务,设计了一个要害角色
UNDO_LOG
(回滚日志记录表),咱们在每个利用分布式事务的业务库中创立这张表,这个表的核心作用就是,将业务数据在更新前后的数据镜像组织成回滚日志,备份在UNDO_LOG
表中,以便业务异样能随时回滚。
第一个阶段
比方:下边咱们更新 user
表的 name
字段。
update user set name = '小富最帅' where name = '程序员内点事'
首先 Seata 的 JDBC
数据源代理通过对业务 SQL 解析,提取 SQL 的元数据,也就是失去 SQL 的类型(UPDATE
),表(user
),条件(where name = '程序员内点事'
)等相干的信息。
先查问数据前镜像,依据解析失去的条件信息,生成查问语句,定位一条数据。
select name from user where name = '程序员内点事'
紧接着执行业务 SQL,依据前镜像数据主键查问出后镜像数据
select name from user where id = 1
把业务数据在更新前后的数据镜像组织成回滚日志,将业务数据的更新和回滚日志在同一个本地事务中提交,别离插入到业务表和 UNDO_LOG
表中。
回滚记录数据格式如下:包含 afterImage
前镜像、beforeImage
后镜像、branchId
分支事务 ID、xid
全局事务 ID
{
"branchId":641789253,
"xid":"xid:xxx",
"undoItems":[
{
"afterImage":{
"rows":[
{
"fields":[
{
"name":"id",
"type":4,
"value":1
}
]
}
],
"tableName":"product"
},
"beforeImage":{
"rows":[
{
"fields":[
{
"name":"id",
"type":4,
"value":1
}
]
}
],
"tableName":"product"
},
"sqlType":"UPDATE"
}
]
}
这样就能够保障,任何提交的业务数据的更新肯定有相应的回滚日志。
在本地事务提交前,各分支事务需向
全局事务协调者
TC 注册分支 (Branch Id
),为要批改的记录申请 全局锁,要为这条数据加锁,利用SELECT FOR UPDATE
语句。而如果始终拿不到锁那就须要回滚本地事务。TM 开启事务后会生成全局惟一的XID
,会在各个调用的服务间进行传递。
有了这样的机制,本地事务分支(Branch Transaction
)便能够在全局事务的第一阶段提交,并马上开释本地事务锁定的资源。相比于传统的 XA
事务在第二阶段开释资源,Seata
升高了锁范畴提高效率,即便第二阶段产生异样须要回滚,也能够疾速 从UNDO_LOG
表中找到对应回滚数据并反解析成 SQL 来达到回滚弥补。
最初本地事务提交,业务数据的更新和后面生成的 UNDO LOG 数据一并提交,并将本地事务提交的后果上报给全局事务协调者 TC。
第二个阶段
第二阶段是依据各分支的决定做提交或回滚:
如果决定是全局提交,此时各分支事务已提交并胜利,这时 全局事务协调者(TC)
会向分支发送第二阶段的申请。收到 TC 的分支提交申请,该申请会被放入一个异步工作队列中,并马上返回提交胜利后果给 TC。异步队列中会异步和批量地依据 Branch ID
查找并删除相应 UNDO LOG
回滚记录。
如果决定是全局回滚,过程比全局提交麻烦一点,RM
服务方收到 TC
全局协调者发来的回滚申请,通过 XID
和 Branch ID
找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以实现分支的回滚。
留神:这里删除回滚日志记录操作,肯定是在本地业务事务执行之后
上边说了几种分布式事务各自的优缺点,下边实际一下分布式事务两头 Seata 感受一下。
Seata 实际
Seata 是一个需独立部署的中间件,所以先搭 Seata Server,这里以最新的 seata-server-1.4.0
版本为例,下载地址:https://seata.io/en-us/blog/download.html
解压后的文件咱们只须要关怀 \seata\conf
目录下的 file.conf
和 registry.conf
文件。
Seata Server
file.conf
file.conf
文件用于配置长久化事务日志的模式,目前提供 file
、db
、redis
三种形式。
留神:在抉择 db
形式后,须要在对应数据库创立 globalTable
(长久化全局事务)、branchTable
(长久化各提交分支的事务)、lockTable
(长久化各分支锁定资源事务)三张表。
-- the table to store GlobalSession data
-- 长久化全局事务
CREATE TABLE IF NOT EXISTS `global_table`
(`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
-- 长久化各提交分支的事务
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
-- 长久化每个分支锁表事务
CREATE TABLE IF NOT EXISTS `lock_table`
(`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
registry.conf
registry.conf
文件设置 注册核心 和 配置核心:
目前注册核心反对 nacos
、eureka
、redis
、zk
、consul
、etcd3
、sofa
七种,这里我应用的 eureka
作为注册核心;配置核心反对 nacos
、apollo
、zk
、consul
、etcd3
五种形式。
配置完当前在 \seata\bin
目录下启动 seata-server
即可,到这 Seata
的服务端就搭建好了。
Seata Client
Seata Server
环境搭建完,接下来咱们新建三个服务 order-server
(下单服务)、storage-server
(扣减库存服务)、account-server
(账户金额服务),别离服务注册到 eureka
。
每个服务的大体外围配置如下:
spring:
application:
name: storage-server
cloud:
alibaba:
seata:
tx-service-group: my_test_tx_group
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://47.93.6.1:3306/seat-storage
username: root
password: root
# eureka 注册核心
eureka:
client:
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:8761/eureka/
instance:
hostname: 47.93.6.5
prefer-ip-address: true
业务大抵流程:用户发动下单申请,本地 order 订单服务创立订单记录,并通过 RPC
近程调用 storage
扣减库存服务和 account
扣账户余额服务,只有三个服务同时执行胜利,才是一个残缺的下单流程。如果某个服执行失败,则其余服务全副回滚。
Seata 对业务代码的侵入性十分小,代码中应用只需用 @GlobalTransactional
注解开启一个全局事务即可。
@Override
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void create(Order order) {String xid = RootContext.getXID();
LOGGER.info("-------> 交易开始");
// 本地办法
orderDao.create(order);
// 近程办法 扣减库存
storageApi.decrease(order.getProductId(), order.getCount());
// 近程办法 扣减账户余额
LOGGER.info("-------> 扣减账户开始 order 中");
accountApi.decrease(order.getUserId(), order.getMoney());
LOGGER.info("-------> 扣减账户完结 order 中");
LOGGER.info("-------> 交易完结");
LOGGER.info("全局事务 xid:{}", xid);
}
前边说过 Seata AT 模式实现分布式事务,必须在相干的业务库中创立 undo_log
表来存数据回滚日志,表构造如下:
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id',
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME NOT NULL COMMENT 'modify datetime',
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
到这环境搭建的工作就完事了,残缺案例会在后边贴出
GitHub
地址,就不在这占用篇幅了。
测试 Seata
我的项目中的服务调用过程如下图:
启动各个服务后,咱们间接申请下单接口看看成果,只有 order
订单表创立记录胜利,storage
库存表 used
字段数量递增、account
余额表 used
字段数量递增则示意下单流程胜利。
申请后正向流程是没问题的,数据和料想的一样
而且发现 TM
事务管理者 order-server
服务的控制台也打印出了两阶段提交的日志
那么再看看如果其中一个服务异样,会不会失常回滚呢?在 account-server
服务中模仿超时异样,看是否实现全局事务回滚。
发现数据全没执行胜利,阐明全局事务回滚也胜利了
那看一下 undo_log
回滚记录表的变动状况,因为 Seata
删除回滚日志的速度很快,所以要想在表中看见回滚日志,必须要在某一个服务上打断点才看的更显著。
总结
上边简略介绍了 2PC
、3PC
、TCC
、MQ
、Seata
这五种分布式事务解决方案,还具体的实际了 Seata
中间件。但不论咱们选哪一种计划,在我的项目中利用都要审慎再审慎,除特定的数据强一致性场景外,能不必尽量就不要用,因为无论它们性能如何优越,一旦我的项目套上分布式事务,整体效率会几倍的降落,在高并发状况下弊病尤为显著。
本案例 github 地址:https://github.com/chengxy-nd…
如果有一丝播种,欢送 点赞、转发,您的认可是我最大的能源。
整顿了几百本各类技术电子书,有须要的同学能够,关注公号回复 [666] 自取。还有想要加技术群的能够加我好友,和大佬侃技术、不定期内推,一起学起来。