微服务中不可避免的会发生服务间的调用,这就一定会涉及到事务相关的问题,在单体项目中我们可以直接很方便的实现事务回滚,但是在分布式系统中就不能像以前那么做了,因为各个服务是独立的一套系统; 而要实现跨服务的事务管理系统的复杂度必然会大大增加,因此我们应当尽可能的避免使用分布式事务;对于那种要求不是很严格的可以考虑忽略掉事务的问题,只对重要的数据才做分布式事务。下面我们使用spring-cloud-alibaba套件Seata来实现分布式事务的功能。
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。
Seata支持本地文件模式和远程配置中心模式,下面我们分别介绍相关的使用方式。注意示例中使用的是spring-cloud-alibaba的套件;下面是代码示例:
<!-- seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
spring-cloud-starter-alibaba-seata这个依赖中只依赖了spring-cloud-alibaba-seata,所以在项目中添加spring-cloud-starter-alibaba-seata和spring-cloud-alibaba-seata是一样的
seata的配置参数官方文档 https://seata.io/zh-cn/docs/user/configurations.html
在application.yml里面配置seata需要的信息
spring: cloud: alibaba: seata: # 这里定义seata服务分组名称,必须和下面的seata.service.vgroup-mapping对应,否则将无法获取seata服务端IP信息 tx-service-group: seata-dubbo-b-seata-service-group seata: registry: type: file service: # seata服务端的地址和端口信息,多个使用英文分号分隔 grouplist: default: 192.168.56.101:8091 vgroup-mapping: seata-dubbo-b-seata-service-group: default
上面的配置可以去看
io.seata.spring.boot.autoconfigure.properties.client.ServiceProperties
和io.seata.discovery.registry.FileRegistryServiceImpl
这2个类你就明白了为啥这样配置了。
在每一个业务库里面创建一个undo_log的表,这里表里面会记录事务信息,用于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 = 1 DEFAULT CHARSET = utf8
在需要开启全局事务的方法上添加 @GlobalTransactional
注解即可;只需要在起始的调用方法上加即可;注意对应异常情况想要回滚,直接抛出异常即可,否则将无法触发全局事务的回滚。 代码示例如下:
服务A
@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @DubboReference(interfaceClass = GoodsService.class, check = false) private GoodsService goodsService; /** * 预定 */ @GlobalTransactional @Override public String booking(Long goodsId, Integer num) throws SQLException { Order order = new Order(); order.setOrderNo(String.valueOf(System.currentTimeMillis())); order.setUid(1L); order.setGoodsId(goodsId); order.setIntegral(num*50); int count = orderMapper.insert(order); if (count!=1){ System.out.println("订单创建失败"); return "订单创建失败"; } boolean res = this.goodsService.deductInventory(goodsId, num); if(!res){ throw new SQLException("库存不足"); } return order.getOrderNo(); } }
服务B
@DubboService public class GoodsServiceImpl implements GoodsService { @Autowired private GoodsMapper goodsMapper; @DubboReference(interfaceClass = IntegralService.class, check = false) private IntegralService integralService; @Override public boolean deductInventory(Long id, int num) throws SQLException { Goods goods = goodsMapper.selectById(id); int count = goodsMapper.deductInventory(id, num); if (count!=1){ throw new SQLException("库存不足"); } boolean res = this.integralService.deductIntegral(id, num*goods.getIntegral()); System.out.println("积分扣除结果:"+res); if(!res){ throw new SQLException("积分不足"); } return true; } }
服务C
@DubboService public class IntegralServiceImpl implements IntegralService { @Autowired private MemberMapper memberMapper; @Override public boolean deductIntegral(Long id, int integral) { int count = memberMapper.deductIntegral(id, integral); return count==1; } }
之前我们的seata是没有集群的,要集群的话那么就不能使用文件模式了,这里我们使用nacos来实现seata集群间的通信;注意这里使用的是nacos-1.x,在实际测试中使用nacos-2.x的时候会偶发出现dubbo服务无法调用的问题。
修改application.yml的配置,将上面seata部分的配置改为如下所示:
seata: registry: type: nacos nacos: serverAddr: 192.168.56.1:8848 application: seata-server group: SEATA_GROUP service: vgroup-mapping: # 这个必须和上面的匹配,同时最大长度为32;否则需要修改创建seata库中的global_table表的transaction_service_group的长度限制 seata-dubbo-b-seata-service-group: default
其他的无需改动;直接即可使用;服务启动成功后,seata服务那边也会打印相关信息。最后不得不吐槽下加入分布式事务组件后系统的响应就变慢,因此不到万不得已最好不用分布式事务,哪怕是通过后期手动处理。