分布式事务框架很多:tcc-transaction、Hmily、ByteTCC、myth、EasyTransaction、tx-lcn、seata等等框架,这里有一篇关于这些框架压测的测试报告【不包括seata】:http://springcloud.cn/view/374 ,可以了解下 。
这里我们采用seata来实现分布式事务。
2019 年 1 月,阿里巴巴中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback),和社区一起共建开源分布式事务解决方案。Fescar 的愿景是让分布式事务的使用像本地事务的使用一样,简单和高效,并逐步解决开发者们遇到的分布式事务方面的所有难题。
Fescar 开源后,蚂蚁金服加入 Fescar 社区参与共建,并在 Fescar 0.4.0 版本中贡献了 TCC 模式。
为了打造更中立、更开放、生态更加丰富的分布式事务开源社区,经过社区核心成员的投票,大家决定对 Fescar 进行品牌升级,并更名为 Seata,意为:Simple Extensible Autonomous TransactionArchitecture,是一套一站式分布式事务解决方案。
Seata 融合了阿里巴巴和蚂蚁金服在分布式事务技术上的积累,并沉淀了新零售、云计算和新金融等场景下丰富的实践经验。
Seata简介
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
https://seata.io/zh-cn/
解决分布式事务问题,有两个设计初衷
**对业务无侵入:**即减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入 高性能:减少分布式事务解决方案所带来的性能消耗
Seata目前有三种分布式事务实现方案:AT、TCC及SAGA
Seata AT模式是基于XA事务演进而来的一个分布式事务中间件,XA是一个基于数据库实现的分布式事务协议,本质上和两阶段提交一样,需要数据库支持,Mysql5.6以上版本支持XA协议,其他数据库如Oracle,DB2也实现了XA接口。
事务协调器Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
事务管理器Transaction Manager(TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
**资源管理器Resource Manager (RM):**控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
Branch就是指的分布式事务中每个独立的本地局部事务。
在一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。任何提交的业务数据的更新一定有相应的回滚日志存在
基于这样的机制,分支的本地事务便可以在全局事务的第一阶段提交,并马上释放本地事务锁定的资源;这也是Seata和XA事务的不同之处,两阶段提交往往对资源的锁定需要持续到第二阶段实际的提交或者回滚操作,而有了回滚日志之后,可以在第一阶段释放对资源的锁定,降低了锁范围,提高效率,即使第二阶段发生异常需要回滚,只需找对undolog中对应数据并反解析成sql来达到回滚目的。
同时Seata通过代理数据源将业务sql的执行解析成undolog来与业务数据的更新同时入库,达到了对业务无侵入的效果。
二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和“after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
2019 年 3 月,蚂蚁金服加入分布式事务 Seata 的社区共建中,并贡献其 TCC 模式。TCC 模式通常用于非关系型数据库的分布式事务的实现,作为AT模式的补充。可以与AT模式混合使用。
Seata也针对TCC做了适配兼容,支持TCC事务方案,原理前面已经介绍过,基本思路就是使用侵入业务上的补偿及事务管理器的协调来达到全局事务的一起提交及回滚。
详细可参考 https://seata.io/zh-cn/docs/dev/mode/tcc-mode.html
第一步:下载:https://github.com/seata/seata/releases
第二步:解压 seata-server-1.3.0
第三步:运行bin下的seata-server.bat
seata需要在每个资源服务器中提供一张表:
CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8
此表主要用来做分布式事务日志记录,表的位置在seata服务中:
这里,我们无需单独创建这张表,直接执行资料中提供好的两个数据库sql文件即可:
效果如下:
宿舍有两个:
员工有一个:
如果宿舍楼切换了,男寝变成了女寝,女寝变成了男寝,对应员工住宿的宿舍楼号也要改变。
发送请求:
结果变成:
宿舍
员工
发送请求:
结果变成:
宿舍
员工
这时,明显出问题了,jackchen是男生,住到2号楼,而2号楼现在是女生宿舍。
宿舍切换失败,但是员工更新宿舍却成功了,没有回滚,导致了这个问题。
整体目录结构
首先要指定seata的配置方法
在file.conf中注意seata服务的默认端口
主配置类讲解
package com.itheima.seata.config; import com.alibaba.druid.pool.DruidDataSource; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.MybatisXMLLanguageDriver; import io.seata.rm.datasource.DataSourceProxy; import io.seata.spring.annotation.GlobalTransactionScanner; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.type.JdbcType; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.transaction.SpringManagedTransactionFactory; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.env.Environment; import javax.sql.DataSource; @Configuration public class DataSourceConfig {
/** * 创建原有微服务的数据源对象 * @return */ @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource() {
DruidDataSource druidDataSource = new DruidDataSource(); return druidDataSource; } /** * 把原有的微服务数据源对象放入Seate的代理数据源 * @param druidDataSource * @return */ @Primary @Bean("dataSource") public DataSourceProxy dataSource(DataSource druidDataSource) {
return new DataSourceProxy(druidDataSource); } /** * 因为我们项目的底层是MyBatis,又因为DataSource变化为代理数据源,所以SqlSessionFactoryBean重新构建,并设置新的代理数据源 * @param dataSourceProxy * @return * @throws Exception */ @Bean public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); MybatisConfiguration configuration = new MybatisConfiguration(); configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class); configuration.setJdbcTypeForNull(JdbcType.NULL); sqlSessionFactoryBean.setConfiguration(configuration); sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } /** * 修改每个注册到Seata的事务组名称 * 当前spring.application.name为空的时候,取seata.group.name * 当前spring.application.name不为空的时候,取spring.application.name * @param environment * @return */ @Bean public GlobalTransactionScanner globalTransactionScanner(Environment environment) {
//事务分组名称 String applicationName = environment.getProperty("spring.application.name"); String groupName = environment.getProperty("seata.group.name"); if (applicationName == null) {
return new GlobalTransactionScanner(groupName == null ? "my_test_tx_group" : groupName); } else {
return new GlobalTransactionScanner(applicationName, groupName == null ? "my_test_tx_group" : groupName); } } }
其实:到此我们可以发现,seata使用起来非常简单,只需要我们使用seata提供的数据源,而且为每个RM资源服务器起个名字即可。
导入seata启动器配置
启动类上面排除SpringBoot数据源自动配置
application.yml添加配置允许覆盖SpringBoot自带的对象
切记,需要在主业务方法上添加@GlobalTransactional注解
如图所示:
再次测试,不同服务链接不同的数据库,事务都是可以控制在一起的,满足了分布式事务特性。
确保本地仓库中有seata-spring-boot-starter的jar包和父工程的jar包
CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8
第三步:在启动类上排除SpringBoot内置数据源自动配置
第四步:在主业务方法中添加@GlobalTransactional注解
让Seata的数据源代理对象,覆盖SpringBoot默认的数据源对象
重新测试之前下单方法,看看抛出异常时是否可以回滚数据。
Memorial Day is 540 days |