在我面试招行外包的时候,与三位面试官进行了半个多小时的交锋,从java基础到框架,其中让我记忆深刻的有一个问题。我说到我们系统采用了微服务架构,是根据不同岗位划分成几个服务,服务之间的调用是用openFeign。A服务减库存并调用B服务增加相应库存,使用事务管理防止操作失败。
面试官:等一下,你说一下事务有什么特性?
我:事务有四个特性,原子性、一致性、隔离性、额还有一个忘了(我太紧张了)
面试官:那你大概解释一下每个特性
我:原子性就是事务要么全部成功要么全部失败,一致性就是事务执行前和执行后是一致的,假设A和B共100块钱,无论怎么相互转账,总共还是100块钱,
隔离性就是每个事务之间是相互隔离不影响的,(补充:持久性就是事务一旦提交了,对数据库的改变就是永久性的)
面试官: 事务的隔离级别有几种
我:巴拉巴拉。。。
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | 是 | 是 | 是 |
不可重复读(read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
面试官: 你是怎么保证数据不出错的
我:通过事务管理(我指的是spring本地事务,我还不知道分布式事务),如果有异常则数据回滚,B服务的方法执行完之后,校验数据是否插入成功,如果没有插入成功则手动抛出异常。
面试官:如果网络延迟B服务没有及时插入,或者其他原因导致你校验的时候B数据库并没有插入数据,但是随后又插入成功了,这种情况你是如何解决的
我:。。。(我就此被KO了)
过后我才了解到分布式事务这种东西,一个很经典的场景,在微服务架构中,订单服务下订单,就要调用库存服务减库存,如果库存不足,就下单失败。常见的分布式事务类型有 2PC、3PC、TCC、本地消息表、消息事务、最大努力通知,对于这几种我在此先不讲了,给大家推荐一篇我很喜欢的博主(敖丙)的文章,他写的很好,我经常看他的文章,https://zhuanlan.zhihu.com/p/183753774,我今天就只谈一下阿里开源的分布式事务框架Seata,Seata 是 Simple Extensible Autonomous Transaction Architecture 的简写,详细介绍可参考官网https://seata.io/zh-cn/还有在码云上的仓库https://gitee.com/itCjb/spring-cloud-alibaba-seata-demo里面有示例代码和相关文档。这些东西我觉得没必要写了,我写的不一定有大佬和官方写的好,所以我就分享一下我在安装部署使用中碰到的一些坑:
1、安装seata服务端并注册到nacos(我是放在阿里云ECS上面)
简单粗暴的方法就是到seata的gitbub上去下载linux版本的压缩包,我就是是这样的,或者其他途径也行,然后通过工具上传到服务器,解压,启动就完事了,但是这样是不行的,需要改配置才能成功连接数据库和注册到nacos
file.conf (在conf文件夹里)
1 ## transaction log store, only used in seata-server 2 store { 3 ## store mode: file、db、redis 4 mode = "db" 5 6 ## database store property 7 db { 8 ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc. 9 datasource = "druid" 10 ## mysql/oracle/postgresql/h2/oceanbase etc. 11 dbType = "mysql" 12 driverClassName = "com.mysql.jdbc.Driver" 13 url = "jdbc:mysql://127.0.0.1:3306/seata" 14 user = "root" 15 password = "123456" 16 minConn = 5 17 maxConn = 30 18 globalTable = "global_table" 19 branchTable = "branch_table" 20 lockTable = "lock_table" 21 queryLimit = 100 22 maxWait = 5000 23 } 24 }
这是我的配置,默认配置里还有redis和file模式的默认配置,我没用到就删掉了,这个文件只需要修改几个地方,其余的用默认的就行,如果有兴趣可以自行研究。
1 # 这个改成自己的数据库类型,注意是seata要连接的数据库,不是业务数据库 2 dbType = "mysql" 3 # 数据库驱动,根据自己的数据库及版本选择 4 driverClassName = "com.mysql.jdbc.Driver" 5 # 数据源配置(懂的都懂),需要自己先建这个库,然后导入官方提供的sql脚本 6 url = "jdbc:mysql://127.0.0.1:3306/seata" 7 user = "root" 8 password = "123456"
相关sql脚本,可以到看官方文档,我就不贴了,如果以后更新了贴了可能还会误导,我尽量避免版本更新了我这篇文章就白写了。然后还需要在每个参与全局事务的数据库中加入undo_log表,这是用来存储事务回滚所需的一些数据。
第二步就是修改config.txt文件并添加到nacos
1 # my_test_tx_group 可以根据自定义,多个服务可以配置多个 2 service.vgroupMapping.my_test_tx_group=default 3 # 因为我的数据库也是在ECS上,所以IP是127.0.0.1,这里与上一步几乎一样 4 store.mode=db 5 store.db.datasource=druid 6 store.db.dbType=mysql 7 store.db.driverClassName=com.mysql.jdbc.Driver 8 store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true 9 store.db.user=root 10 store.db.password=123456 11 # 以下配置我没动,采用默认配置 you can you do 12 store.db.minConn=5 13 store.db.maxConn=30 14 store.db.globalTable=global_table 15 store.db.branchTable=branch_table 16 store.db.queryLimit=100 17 store.db.lockTable=lock_table 18 store.db.maxWait=5000
改完之后可以用官方提供的脚本nacos-config.sh将配置添加到nacos配置中心,如果无聊也可以自己一个一个添加
第三步,修改registry.conf
1 registry { 2 # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa 3 # 类型选nacos 4 type = "nacos" 5 nacos { 6 # 应用名默认 7 application = "seata-server" 8 # IP 我改成我服务器IP 9 serverAddr = "127.0.0.1:8848" 10 # 分组就默认,注意:需要参与全局事务的服务都要跟seata-server在同一个命名空间同一个分组 11 group = "SEATA_GROUP" 12 # 我是全部放在自己建的test空间,若没有配置新的命名空间则默认就行 13 namespace = "" 14 cluster = "default" 15 username = "nacos" 16 password = "nacos" 17 } 18 } 19 # 下面配置也一样的,不然无法读取在nacos配置中心的配置 20 config { 21 # file、nacos 、apollo、zk、consul、etcd3 22 # 类型选nacos 23 type = "nacos" 24 nacos { 25 serverAddr = "127.0.0.1:8848" 26 namespace = "" 27 group = "SEATA_GROUP" 28 username = "nacos" 29 password = "nacos" 30 } 31 }
到此seata服务端就安装成功,然后就可以启动seata-server,但是直接使用下面第一个命令启动的话,会有2个问题,一个是关闭终端服务及关闭,二是本地服务会连接不上服务端,因为没有指定IP的话,默认是内网IP,需要改成服务器IP,最好也定一下端口,然后加上其他命令后台启动+后台运行
注意:阿里云服务器的话需要在安全组新增策略开放8091端口,并在内部防火墙开通8091端口或者关闭内部防火墙
1 # 1、 2 ./seata-server.sh 3 # 2、将日志输出到nohup.out文件中 4 sudo nohup ./seata-server.sh -p 8091 -h xxxxxxxxx >nohup.out 2>1 &
然后就可以登录nacos查看seata-server是否启动并注册成功
2、配置seata客户端
客户端就简单了,只需三步,添加依赖,添加配置,添加注解
添加依赖(至于版本可以根据官方推荐自行选择)
1 <dependency> 2 <groupId>com.alibaba.cloud</groupId> 3 <artifactId>spring-cloud-starter-alibaba-seata</artifactId> 4 <version>2.2.2.RELEASE</version> 5 <exclusions> 6 <exclusion> 7 <groupId>io.seata</groupId> 8 <artifactId>seata-spring-boot-starter</artifactId> 9 </exclusion> 10 </exclusions> 11 </dependency> 12 <dependency> 13 <groupId>io.seata</groupId> 14 <artifactId>seata-spring-boot-starter</artifactId> 15 <version>1.3.0</version> 16 </dependency>
添加配置
1 seata: 2 enabled: true 3 # 我直接用服务名 4 application-id: user-service 5 # 这个就是 service.vgroupMapping.my_test_tx_group=default 中的my_test_tx_group 我也用服务名,这2个要一致 6 tx-service-group: user-service 7 enable-auto-data-source-proxy: true 8 config: 9 type: nacos 10 nacos: 11 namespace: "" 12 serverAddr: 127.0.0.1:8848 13 group: SEATA_GROUP 14 username: "nacos" 15 password: "nacos" 16 registry: 17 type: nacos 18 nacos: 19 application: seata-server 20 server-addr: 127.0.0.1:8848 21 group: SEATA_GROUP 22 namespace: “” 23 username: "nacos" 24 password: "nacos"
添加注解
1 @GlobalTransactional 2 @Override 3 public Result publish(Article article) { 4 5 articleMapper.insert(article); 6 userFeign.updatePublishNum(article.getAuthor()); 7 return null; 8 }
注意:如果采用本地事务@Transactional,insert方法执行成功,调用user-service有问题的话,article是没法回滚的,这就需要使用分布式事务
seata目前官方提供四种模式,我使用的是最便捷的AT模式,TCC、Saga、XA模式,you can you do,还有所有的服务,包括seata-server一定要在nacos的同一个空间同一个分组
最后,码字不容易,用你发财的小手点个赞吧,欢迎志同道合的同志,有什么问题或者意见,可以评论私信或者加我的企鹅群876083754(海绵宝宝的菠萝屋)