在近几年流行的微服务架构中,由于对服务和数据库进行了拆分,原来的一个单进程本地事务变成多个进程的本地事务,这时要保证数据的一致性,就需要用到分布式事务了。分布式事务的解决方案有很多,其中国内比较主流的框架就是Seata了。
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
这里推荐使用AT模式,该模式具备代码侵入性小,性能高等优点,前提是:
基于支持本地 ACID 事务的关系型数据库
Java 应用,通过 JDBC 访问数据库。
版本清单:
名称 | 版本 |
---|---|
Nacos | 1.3.2 |
Seata | 1.4.2 |
spring-boot | 2.2.6.RELEASE |
spring-cloud-starter-alibaba-nacos-discovery | 2.2.6.RELEASE |
spring-cloud-starter-openfeign | 2.2.6.RELEASE |
seata-spring-boot-starter | 1.4.2 |
spring-cloud-starter-alibaba-seata | 2.2.1.RELEASE |
本地搭建nacos比较简单,首先通过github下载nacos(我的是1.3.2版本,下载地址),然后解压缩进入bin目录,打开命令行工具运行如下命令即可启动。
startup.sh -m standalone
启动后访问http://localhost:8848/nacos/index.html即可,默认账号密码是nacos/nacos。
1.下载解压
进入seata的发行页面,选择需要的版本下载,然后解压。
2.修改配置文件
进入conf目录(如/Users/ship/program/seata/seata-server-1.4.2/conf),可以看到有file.conf和registry.conf两个配置文件。
首先打开file.conf文件,修改配置如下
## transaction log store, only used in seata-server store { ## store mode: file、db、redis mode = "file" // 改为db ## rsa decryption public key publicKey = "" ## file store property file { ## store location dir dir = "sessionStore" # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions maxBranchSessionSize = 16384 # globe session size , if exceeded throws exceptions maxGlobalSessionSize = 512 # file buffer size , if exceeded allocate new buffer fileWriteBufferCacheSize = 16384 # when recover batch read size sessionReloadReadSize = 100 # async, sync flushDiskMode = async } ## database store property db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc. datasource = "druid" ## mysql/oracle/postgresql/h2/oceanbase etc. dbType = "mysql" driverClassName = "com.mysql.jdbc.Driver" ## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true"// 改成自己的数据库地址 user = "mysql" // 改成自己的数据库用户名 password = "mysql"// 改成自己的数据库密码 minConn = 5 maxConn = 100 globalTable = "global_table" branchTable = "branch_table" lockTable = "lock_table" queryLimit = 100 maxWait = 5000 } ... }
这个数据库会在下面的第四步再创建。
然后打开registry.conf文件,修改注册中心为nacos
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "file" // 改为nacos nacos { application = "seata-server" serverAddr = "127.0.0.1:8848" group = "SEATA_GROUP" namespace = "" // 默认名称空间为public cluster = "default" username = "" // 默认是不需要密码的,如果开启了安全验证则要填写 password = "" } eureka { serviceUrl = "http://localhost:8761/eureka" application = "default" weight = "1" } ... } config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "" password = "" dataId = "seataServer.properties" } ... }
3.同步配置到nacos
低版本的seata需要在项目的resource目录下创建file.conf和registry.conf文件,高版本的只需要将配置信息同步到nacos,然后读取即可。
首先需要在conf的同级目录(如/Users/ship/program/seata/seata-server-1.4.2)下创建config.txt(下载地址)文件,然后修改数据库配置信息。
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=seata // 改为自己的账号 store.db.password=nova2020 // 改为自己的密码 store.db.minConn=5 store.db.maxConn=30
最后进入conf目录(如/Users/ship/program/seata/seata-server-1.4.2/conf),下载nacos-config.sh(下载地址)并使用nacos-config.sh文件同步上传配置到Nacos,命令如下:
sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t 023933c2-2825-46b2-a05a-4a407b479877
命令参数介绍:
-h : 指定nacos的ip地址。
-p: 指定nacos的端口号。
-g: 指定分组
-t: 指定nacos的名称空间,建议为seata单独创建一个名称空间。
-u: nacos的用户名,开启认证才需要。
-w: nacos的密码,开启认证才需要。
打开nacos即可在对应的名称空间下看到那些配置信息,如图:
不得不吐槽一下,像config.text和nacos-config.sh这些文件在0.9.0版本的seata都是和安装包放一起的,高版本的还需要自己找真的坑。
4.创建数据库
为seata-server创建seata库并执行db_store.sql ,下载地址。
为每个业务库执行db_undo_log.sql以添加回滚日志的表,下载地址。
5.启动
进入bin目录,输入sh seata-server.sh即可启动,部分启动日志如下:
SLF4J: A number (18) of logging calls during the initialization phase have been intercepted and are SLF4J: now being replayed. These are subject to the filtering rules of the underlying logging system. SLF4J: See also http://www.slf4j.org/codes.html#replay 16:45:02.688 INFO --- [ main] io.seata.config.FileConfiguration : The file name of the operation is registry 16:45:02.693 INFO --- [ main] io.seata.config.FileConfiguration : The configuration file used is /Users/ship/program/seata/seata-server-1.4.2/conf/registry.conf 16:45:02.785 INFO --- [ main] io.seata.config.FileConfiguration : The file name of the operation is file.conf 16:45:02.785 INFO --- [ main] io.seata.config.FileConfiguration : The configuration file used is /Users/ship/program/seata/seata-server-1.4.2/conf/file.conf 16:45:03.411 INFO --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited 16:45:03.843 INFO --- [ main] i.s.core.rpc.netty.NettyServerBootstrap : Server started, listen port: 8091
前面环境已经搭好了,现在通过一个示例来验证分布式事务的AT模式。
场景是创建订单时会根据商品的金额来扣减用户余额,分别对应order服务和account服务。
表结构设计如下:
DROP TABLE IF EXISTS `order_tbl`; CREATE TABLE `order_tbl` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(255) DEFAULT NULL, `commodity_code` varchar(255) DEFAULT NULL, `count` int(11) DEFAULT 0, `money` int(11) DEFAULT 0, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `account_tbl`; CREATE TABLE `account_tbl` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(255) DEFAULT NULL, `money` int(11) DEFAULT 0, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;
创建一个order项目并添加nacos、fegin和seata的依赖,pom.xml部分如下:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2.2.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>2.2.6.RELEASE</version> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.4.2</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <version>2.2.1.RELEASE</version> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency>
这里为了引入最新版本的seata-spring-boot-starter,就在spring-cloud-starter-alibaba-seata作了排除,也是官方推荐的方式。如果发现你的项目启动不起来或者有其他问题,可能是版本依赖有问题。
启动类OrderApplication.java添加需要的注解
@EnableAutoDataSourceProxy //这个一定要加,如果不加通过配置文件开启也可以 @EnableFeignClients @EnableDiscoveryClient @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }
核心部分OrderService
/** * @Author: Ship * @Description: * @Date: Created in 2021/8/23 */ @Service public class OrderService { @Autowired private OrderDao orderDao; @Autowired private AccountClient accountClient; @GlobalTransactional @Transactional(rollbackFor = Exception.class) public OrderVO create(OrderDTO orderDTO) { // 创建订单 Order order = new Order(); BeanUtils.copyProperties(orderDTO,order); orderDao.insert(order); // 扣除账户余额 AccountDeductDTO accountDeductDTO = new AccountDeductDTO(); Integer total = orderDTO.getMoney() * orderDTO.getCount(); accountDeductDTO.setMoney(total); accountDeductDTO.setUserId(orderDTO.getUserId()); accountClient.deduct(accountDeductDTO); return new OrderVO(order.getId()); } }
只需一个@GlobalTransactional注解即可,用在分布式事务开启的方法上。
修改配置文件bootstrap.yml
spring: application: name: order datasource: username: root password: 1234 url: jdbc:mysql://127.0.0.1:3306/seata_order?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true type: com.alibaba.druid.pool.DruidDataSource server: port: 9900 seata: registry: type: nacos nacos: application: seata-server # 不配置名称空间,默认public server-addr: 127.0.0.1:8848 group: "SEATA_GROUP" config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: "SEATA_GROUP" namespace: 023933c2-2825-46b2-a05a-4a407b479877 # 配置名称空间为seata enabled: true tx-service-group: my_test_tx_group # 要与nacos上的配置一致 service: vgroup-mapping: my_test_tx_group: default # 要与nacos上的配置一致 disable-global-transaction: false
注意:seata.tx-service-group属性值要和seata-server的config.txt文件中的service.vgroupMapping.${分组名}=default分组名一一对应,这里我用的默认配置my_test_tx_group。关于事务分组还有很多种玩法,可以参考这里https://seata.io/zh-cn/docs/user/txgroup/transaction-group.html。
account服务项目结构基本与order服务一致,只是service代码不同。
/** * @Author: Ship * @Description: * @Date: Created in 2021/8/23 */ @Service public class AccountService { @Autowired private AccountDao accountDao; @Transactional(rollbackFor = Exception.class) public void deduct(AccountDeductDTO accountDeductDTO) { QueryWrapper<Account> wrapper = new QueryWrapper(); wrapper.lambda().eq(Account::getUserId, accountDeductDTO.getUserId()); Account account = accountDao.selectOne(wrapper); Integer money = account.getMoney() - accountDeductDTO.getMoney(); account.setMoney(money); // 更新余额 accountDao.updateById(account); // int i = 1 / 0; } }
启动seata-server
启动order服务和account服务,并能成功在nacos上看到注册实例。
首先给用户1111的账户初始100元的余额,sql如下
insert into account_tbl(user_id,money) VALUES(1111,100);
请求下单接口http://localhost:9900/order/create,POST body参数如下:
{ "userId":1111, "commodityCode":"code", "count":2, "money":10 }
查询数据库可以发现,order_tbl表已经有一条订单数据了,并且用户的1111的余额变成了80,说明事务提交成功。
这时将account服务的AccountService中的int i = 1 / 0;这行代码取消注释,并在重启account服务之后再次请求下单接口。
查看控制台日志发现抛异常了,再次查询order_tbl表发现还是一条订单数据,account_tbl表的用户余额也还是80,说明发生了全局事务回滚。 通过order服务的日志也可以看出,account服务扣减余额接口异常导致了回滚。
2021-09-04 21:18:28.323 INFO 72668 --- [h_RMROLE_1_1_16] i.s.c.r.p.c.RmBranchCommitProcessor : rm client handle branch commit process:xid=192.168.3.253:8091:3900290643103043585,branchId=3900290643103043591,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/seata_order,applicationData=null 2021-09-04 21:18:28.326 INFO 72668 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler : Branch committing: 192.168.3.253:8091:3900290643103043585 3900290643103043591 jdbc:mysql://127.0.0.1:3306/seata_order null 2021-09-04 21:18:28.327 INFO 72668 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler : Branch commit result: PhaseTwo_Committed 2021-09-04 21:36:17.280 INFO 72668 --- [nio-9900-exec-3] i.seata.tm.api.DefaultGlobalTransaction : Begin new global transaction [192.168.3.253:8091:3900290643103043595] 2021-09-04 21:36:17.282 ERROR 72668 --- [nio-9900-exec-3] c.a.druid.pool.DruidAbstractDataSource : discard long time none received connection. , jdbcUrl : jdbc:mysql://127.0.0.1:3306/seata_order?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true, jdbcUrl : jdbc:mysql://127.0.0.1:3306/seata_order?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true, lastPacketReceivedIdleMillis : 1068019 2021-09-04 21:36:17.782 INFO 72668 --- [nio-9900-exec-3] i.seata.tm.api.DefaultGlobalTransaction : Suspending current transaction, xid = 192.168.3.253:8091:3900290643103043595 2021-09-04 21:36:17.782 INFO 72668 --- [nio-9900-exec-3] i.seata.tm.api.DefaultGlobalTransaction : [192.168.3.253:8091:3900290643103043595] rollback status: Rollbacked //回滚
至此说明我们的分布式事务控制生效了,示例代码包括脚本都已经提交到我的github上,需要的请点击。
Seata框架上手不难,重点还是理解其实现原理和做到灵活使用,比如事务分组的设计就很巧妙,其AT模式比起之前用过的TCC框架好太多,阿里出品还是厉害啊。
参考资料:
seata官方文档,https://seata.io/zh-cn/docs/overview/what-is-seata.html
https://github.com/seata/seata-samples