微服务中不可避免的会发生服务间的调用,这就一定会涉及到事务相关的问题,在单体项目中我们可以直接很方便的实现事务回滚,但是在分布式系统中就不能像以前那么做了,因为各个服务是独立的一套系统; 而要实现跨服务的事务管理系统的复杂度必然会大大增加,因此我们应当尽可能的避免使用分布式事务;对于那种要求不是很严格的可以考虑忽略掉事务的问题,只对重要的数据才做分布式事务。下面我们使用spring-cloud-alibaba套件Seata来实现分布式事务的功能。
Seata简介
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。
相关文档
功能特性如下
- 微服务框架支持:目前已支持 Dubbo、Spring Cloud、Sofa-RPC、Motan 和 grpc 等RPC框架,其他框架持续集成中;
- TCC模式:支持 TCC 模式并可与 AT 混用;
- XA模式:支持已实现 XA 接口的数据库的 XA 模式;
- AT模式:提供无侵入自动补偿的事务模式,目前已支持 MySQL、 Oracle 、PostgreSQL和 TiDB的AT模式,H2 开发中;
- 高可用:支持基于数据库存储的集群模式,水平扩展能力强;
- SAGA模式:为长事务提供有效的解决方案;
Seata的使用
Seata支持本地文件模式和远程配置中心模式,下面我们分别介绍相关的使用方式。注意示例中使用的是spring-cloud-alibaba的套件;下面是代码示例:
使用file模式
添加maven依赖:
<!-- 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配置文件
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;
}
}
使用注册中心nacos进行集群
之前我们的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服务那边也会打印相关信息。最后不得不吐槽下加入分布式事务组件后系统的响应就变慢,因此不到万不得已最好不用分布式事务,哪怕是通过后期手动处理。