场景说明
订单服务order-service需要对外提供创建订单的接口,创建订单的业务逻辑如下:
先调用本地的orderService保存订单操作,然后通过feign调用远程的accout-service进行账户余额扣减,最后再通过feign调用远程的product-service进行库存扣减操作。
关键的逻辑代码如下:
- OrderController对外提供创建订单的接口
@PostMapping("/order/create")public ResultData 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)@Overridepublic 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)@Overridepublic 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)@Overridepublic 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-service、product-service、account-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下
binseata-server.bat
### 引入seata组件
com.alibaba.cloud spring-cloud-alibaba-seata
在配置文件中增加seata配置
spring: cloud: alibaba: seata: tx-service-group: ${spring.application.name}-seata
Seata Client 配置修改
- 将Seata Server 配置目录下的registry.conf、file.conf 2个文件拷贝到微服务中的resources文件夹下
- 修改拷贝后的registry.conf
registry{ type = "file" file { name = "file.conf" }}config{ type = "file" file { name = "file.conf" }}
- 修改file.conf
主要修改如下三处:
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")@Overridepublic 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控制台可以看到注册信息
接口测试
改造完成后对接口进行测试,如果其他服务抛出异常会看到如下错误日志,再结合数据库数据观察是否正常回滚
执行过程中我们通过debug可以发现undo_log表会不断插入数据,在执行后又会被删除。
通过上面几步我们使用Seata实现了分布式事务,保证了数据的一致性,最后说一句Seata真香,你们要不要感受一下。