Spring Cloud Alibaba seata
seata简介
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
Seata的三大角色
TC(Transaction Coordinator)- 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。TM(Transaction Manager)- 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务RM(Resource Manager)- 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。其中,TC为单独部署的Server服务端,TM和RM为嵌入到应用中的Client客户端。
事务模式
AT模式,用户只需关注自己的业务sql,用户的业务sql作为一阶段,Seata框架会自动生成事务的二阶段提交和回滚操作。
AT模式如何做到对业务的无侵入:
- 一阶段:在一阶段,Seata会拦截业务sql,首先解析sql语义,找到业务sql要更新的业务数据,在业务数据被更新之前,将其保存成before image,然后执行业务sql更新业务数据,在业务数据更新之后,再将其保存成after image,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
- 二阶段提交:二阶段如果是提交的话,因为业务sql在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删除,完成数据清理即可。
- 二阶段回滚:二阶段如果是回滚的话,Seata就需要回滚一阶段已执行的业务sql,还原业务数据。回滚方式便是用before image 还原业务数据,但在还原前要首先校验脏写,对比数据库当前业务数据和after image,如果两份数据完全一致就说明没有脏写,可以还原业务数据。如果不一致,就说明有脏写,出现脏写就需要转人工处理。
TCC模式需要用户根据自己的业务场景实现Try、Confirm和Cancel三个操作。事务发起方在一阶段执行Try方法,在二阶段提交执行Confirm方法,二阶段回滚执行Cancel方法。
优点:在整个过程中基本没有锁,性能更强。
缺点: 侵入性比较强,并且需要用户自己实现相关事务控制逻辑。
安装配置seata
下载
解压配置registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = "seata"
cluster = "default"
username = "nacos"
password = "nacos"
}
...
...
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = "seata"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
Nacos中添加配置文件
- 新建命名空间seata
- 在seata下创建配置文件seataServer.properties
- 分组为SEATA_GROUP(本次使用的是redis,需要seata1.3版本以上)
store.redis.host=127.0.0.1 store.redis.port=6379 store.redis.maxConn=10 store.redis.minConn=1 store.redis.database=9 store.redis.queryLimit=100
- 其中也能使用mysql:
service.vgroupMapping.my_tx_group=default store.mode=redis -----db----- store.db.datasource=druid store.db.dbType=mysql store.db.driverClassName=com.mysql.jdbc.Driver store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true store.db.user=root store.db.password=root ----client---- client.undo.logTable=undo_log
启动
- windows:bin目录下双击seata-server.bat启动
- linux:命令行启动 seata-server.sh -h 127.0.0.1 -p 8091
搭建项目
添加依赖
- 在执行业务的项目下添加依赖,参与业务的项目也需要
<dependency> groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>RELEASE</version> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> <version>1.4.0</version> </dependency>
seata配置文件
seata:
enabled: true
#seata注册的服务名称
application-id: seata-service
#此处配置自定义的seata事务分组名称
tx-service-group: my_tx_group
#开启数据库代理
enable-auto-data-source-proxy: false
config:
type: nacos
nacos:
#nacos地址
server-addr: 127.0.0.1:8848
#配置文件的命名空间
namespace: seata
dataId: "seataServer.properties"
#配置文件的分组名
group: SEATA_GROUP
username: nacos
password: nacos
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
#注册服务的命名空间
namespace: seata
#注册服务的名称
application: seata-server
#注册服务的分组名
group: SEATA_GROUP
username: nacos
password: nacos
- 然后在nacos控制台添加一个配置文件(seata命名空间下)
- Data ID: service.vgroupMapping.my_tx_group
- group: SEATA_GROUP
- 内容:
default
- 添加Seata需要用到的undo_log表,该表用于在分布式事务发生异常时执行回滚的依据。每个参与分布式事务的数据库都需要加该表。
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=1 DEFAULT CHARSET=utf8;
- 在调用的service上添加@GlobalTransactional即可开启事务
测试
- 测试seata的事务是否有效
数据库
- 随便创建几个测试的数据库,比如user, good, user_good, record
- 其中可以在每个项目上配置不同的数据库,比如项目1配置1数据库(user表),项目2配置2数据库(good, user_good表),项目3配置3数据库(record表)
- 三个业务,扣除用户gold,user_good新增记录,record新增记录
- 抛出异常查看是否回滚
- 数据库:
CREATE TABLE `user` ( `user_id` int(11) NOT NULL AUTO_INCREMENT, `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `user_gold` decimal(6, 1) NULL DEFAULT NULL, PRIMARY KEY (`user_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; INSERT INTO `user` VALUES (1, 'zwq', 136.1); CREATE TABLE `good` ( `good_id` int(11) NOT NULL AUTO_INCREMENT, `good_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `price` decimal(10, 1) NULL DEFAULT NULL, PRIMARY KEY (`good_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; INSERT INTO `good` VALUES (1, '测试商品', 34.5); CREATE TABLE `user_good` ( `ug_id` int(11) NOT NULL AUTO_INCREMENT, `ug_user_id` int(11) NOT NULL, `ug_good_id` int(11) NOT NULL, PRIMARY KEY (`ug_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1691336706 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; CREATE TABLE `record` ( `record_id` int(11) NOT NULL AUTO_INCREMENT, `record_content` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `record_user_id` int(11) NULL DEFAULT NULL, `record_good_id` int(11) NULL DEFAULT NULL, `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0), PRIMARY KEY (`record_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1519374338 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
项目代码
- 结构
- nacos, sentinel, record分别放用户,用户商品,记录的业务。
- nacos
@Override public UserDO getUser(int id) { log.info("Seata全局事务id=================>{}", RootContext.getXID()); com.zzz.pped.first.pojo.dos.UserDO userDO = userMapper.selectById(id); com.zzz.dubboapi.test.pojo.dos.UserDO userDOTarget = new com.zzz.dubboapi.test.pojo.dos.UserDO(); BeanUtils.copyProperties(userDO, userDOTarget); return userDOTarget; }
- sentinel
@Override public GoodDO getGood(int id) { com.zzz.pped.test.pojo.dos.GoodDO goodDO = goodMapper.selectById(id); GoodDO target = new GoodDO(); BeanUtils.copyProperties(goodDO, target); return target; } @Override public int insertUserGood(UserGoodDO userGoodDO) { com.zzz.pped.test.pojo.dos.UserGoodDO target = new com.zzz.pped.test.pojo.dos.UserGoodDO(); BeanUtils.copyProperties(userGoodDO, target); return userGoodMapper.insert(target); }
- record
@Override public int insertRecord(RecordDO recordDO) { com.zzz.pped.test.pojo.dos.RecordDO target = new com.zzz.pped.test.pojo.dos.RecordDO(); BeanUtils.copyProperties(recordDO, target); return recordMapper.insert(target); }
- 测试整个业务的接口
@Override @GlobalTransactional public String test() { GoodDO goodDO = goodService.getGood(1); UserDO userDO = userService.getUser(1); userDO.setGold(userDO.getGold().subtract(goodDO.getPrice())); userService.updateUser(userDO); if (userDO != null) { throw new RuntimeException("回滚"); } RecordDO recordDO = new RecordDO(); recordDO.setUserId(userDO.getId()); recordDO.setGoodId(goodDO.getId()); recordDO.setContent(new Date() + userDO.getName() + "买了" + goodDO.getName()); recordService.insertRecord(recordDO); UserGoodDO userGoodDO = new UserGoodDO(); userGoodDO.setUserId(userDO.getId()); userGoodDO.setGoodId(goodDO.getId()); userGoodService.insertUserGood(userGoodDO); return userDO.toString(); }
- 调用接口,查看正常调用和抛出异常的区别
常见问题
- linux启动seata在nacos显示的IP不对,微服务访问不到,修改linux hostname不能为localhost, hostname test
- seata控制台rollback successful 但是数据库未回滚,启动类加上@EnableAutoDataSourceProxy可能解决