SpringCloud Alibaba 微服务实战七 - 分布式事务

本贴最后更新于 1561 天前,其中的信息可能已经时移世改

导读:本篇作为 SpringCloud Alibaba 微服务实战系列的第七篇,主要内容是使用 Seata 解决分布式事务问题。系列文章,欢迎持续关注。

场景说明

订单服务 order-service 需要对外提供创建订单的接口,创建订单的业务逻辑如下:
image.png

先调用本地的 orderService 保存订单操作,然后通过 feign 调用远程的 accout-service 进行账户余额扣减,最后再通过 feign 调用远程的 product-service 进行库存扣减操作。

关键的逻辑代码如下:

  • OrderController 对外提供创建订单的接口
@PostMapping("/order/create")
public ResultData<OrderDTO> create(@RequestBody OrderDTO orderDTO){
	log.info("create order:{}",orderDTO);
	orderDTO.setOrderNo(UUID.randomUUID().toString());
	orderDTO.setAmount(orderDTO.getPrice().multiply(new BigDecimal(orderDTO.getCount())));
	orderService.createOrder(orderDTO);
	return ResultData.success("create order success");
}
  • OrderServiceImpl 负责处理创建订单的业务逻辑
@Transactional(rollbackFor = RuntimeException.class)
@Override
public void createOrder(OrderDTO orderDTO) {
	Order order = new Order();
	BeanUtils.copyProperties(orderDTO,order);
	//本地存储Order
	this.saveOrder(order);
	//库存扣减
	productFeign.deduct(orderDTO.getProductCode(),order.getCount());
	//账户余额扣减
	accountFeign.reduce(orderDTO.getAccountCode(), orderDTO.getAmount());
}

@Transactional(rollbackFor = RuntimeException.class)
void saveOrder(Order order) {
	orderMapper.insert(order);
}

本地先保存,然后调用两个远程服务进行扣减操作。

  • AccountServiceImpl 扣减账户余额
@Transactional(rollbackFor = RuntimeException.class)
@Override
public void reduceAccount(String accountCode, BigDecimal amount) {
	Account account = accountMapper.selectByCode(accountCode);
	if(null == account){
		throw new RuntimeException("can't reduce amount,account is null");
	}
	BigDecimal subAmount = account.getAmount().subtract(amount);
	if(subAmount.compareTo(BigDecimal.ZERO) < 0){
		throw new RuntimeException("can't reduce amount,account'amount is less than reduce amount");
	}
	account.setAmount(subAmount);
	accountMapper.updateById(account);
}

做些简单的校验,当账户余额不足的时候不允许扣减操作。

  • ProductServiceImpl 扣减产品库存
@Transactional(rollbackFor = RuntimeException.class)
@Override
public void deduct(String productCode, Integer deductCount) {
	Product product = productMapper.selectByCode(productCode);
	if(null == product){
		throw new RuntimeException("can't deduct product,product is null");
	}
	int surplus = product.getCount() - deductCount;
	if(surplus < 0){
		throw new RuntimeException("can't deduct product,product's count is less than deduct count");
	}
	product.setCount(surplus);
	productMapper.updateById(product);
}

做些简单的校验,当产品库存不足时不允许扣减操作。

order-serviceproduct-serviceaccount-service 分属不同的服务,当其中一个服务抛出异常无法提交时就会导致分布式事务,如当使用 feign 调用 account-service 执行扣减账户余额时,account-service 校验账户余额不足抛出异常,但是 order-service 的保存操作不会回滚;或者是前两步执行成功但是 product-service 校验不通过前面的操作也不会回滚,这就导致了数据不一致,也就是分布式事务问题!

Seata 解决方案

在 Springcloud Alibaba 体系中使用 Seata 作为分布式事务解决方案,大家可以访问 seata 官网去了解详情。
这次我们先使用 Seata 的 file 配置解决上面出现的问题,后面再来对其改造。

下载安装 Seata Server。

  • Release 页面下载 Seata Server
  • 下载完成后直接启动 Server 端服务。
    在 Linux/Mac 下
    $ sh ./bin/seata-server.sh
    在 Windows 下
    bin\seata-server.bat

引入 seata 组件

<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-alibaba-seata</artifactId>
</dependency>

在配置文件中增加 seata 配置

spring:
  cloud:
    alibaba:
      seata:
        tx-service-group: ${spring.application.name}-seata

Seata Client 配置修改

  • 将 Seata Server 配置目录下的 registry.conffile.conf 2 个文件拷贝到微服务中的 resources 文件夹下
    image.png

  • 修改拷贝后的 registry.conf

registry{
  type = "file"

  file {
    name = "file.conf"
  }
}

config{
  type = "file"

  file {
    name = "file.conf"
  }
}
  • 修改 file.conf
    image.png
    主要修改如下三处:
    service.vgroup_mapping. 后面的值修改为配置文件 spring.cloud.alibaba.seata.tx-service-group 的属性
    service.default.grouplist= 修改为 Seata Server 的 ip:端口
    support.spring.datasource.autoproxy 的值修改为 true,开启 datasource 自动代理

生成 undo_log 表

在微服务的业务库下执行如下语句,生成 undo_log 表

-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库)
drop table `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;

开启全局事务

在分布式事务方法入口添加注解 @GlobalTransactional,这里只需要在 createOrder 方法上添加此注解即可!

@GlobalTransactional(name = "TX_ORDER_CREATE")
@Override
public void createOrder(OrderDTO orderDTO) {
	Order order = new Order();
	BeanUtils.copyProperties(orderDTO,order);
	//本地存储Order
	this.saveOrder(order);
	log.info("ORDER XID is: {}", RootContext.getXID());
	//账户余额扣减
	accountFeign.reduce(orderDTO.getAccountCode(), orderDTO.getAmount());
	//库存扣减
	productFeign.deduct(orderDTO.getProductCode(),orderDTO.getCount());
}

在代码中可以使用 RootContext.getXID() 获取全局 xid

启动服务

服务正常启动后在 Seata Server 控制台可以看到注册信息
image.png

接口测试

改造完成后对接口进行测试,如果其他服务抛出异常会看到如下错误日志,再结合数据库数据观察是否正常回滚
image.png

执行过程中我们通过 debug 可以发现 undo_log 表会不断插入数据,在执行后又会被删除。
image.png

通过上面几步我们使用 Seata 实现了分布式事务,保证了数据的一致性,最后说一句 Seata 真香,你们要不要感受一下。
至此本期的“SpringCloud Alibaba 微服务实战七 - 分布式事务”篇也就该结束啦,咱们下期有缘再见!
image.png
再见之前让我在求一波关注吧,O(∩_∩)O 哈哈~!
image.png

系列文章

  • 云计算
    77 引用 • 91 回帖 • 1 关注
  • 架构

    我们平时所说的“架构”主要是指软件架构,这是有关软件整体结构与组件的抽象描述,用于指导软件系统各个方面的设计。另外还有“业务架构”、“网络架构”、“硬件架构”等细分领域。

    139 引用 • 441 回帖
  • 微服务

    微服务架构是一种架构模式,它提倡将单一应用划分成一组小的服务。服务之间互相协调,互相配合,为用户提供最终价值。每个服务运行在独立的进程中。服务于服务之间才用轻量级的通信机制互相沟通。每个服务都围绕着具体业务构建,能够被独立的部署。

    96 引用 • 155 回帖 • 1 关注
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    938 引用 • 1456 回帖 • 163 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • someone

    调用第三方系统接口怎么处理

  • someone 1
    作者

    来,打一架!

  • someone 1

    头像图裂了