将使用 Seata 的 AT 模式,解决多个 Spring Cloud 服务下的分布式事务的问题,即服务提供者通过 SpringMVC 提供 Restful HTTP API 接口,服务消费者通过 Feign 进行 HTTP 调用。
Seata 提供了 spring-cloud-starter-alibaba-seata
项目,对 Spring Cloud 进行集成。实现原理是:
- 服务消费者,使用 Seata 封装的 SeataFeignClient 过滤器,在使用 Feign 发起 HTTP 调用时,将 Seata 全局事务 XID 通过 Header 传递。
- 服务提供者,使用 Seata 提供的 SpringMVC SeataHandlerInterceptor 拦截器,将 Header 中的 Seata 全局事务 XID 解析出来,设置到 Seata 上下文 中。
如此,我们便实现了多个 Spring Cloud 应用的 Seata 全局事务的传播。
我们以用户购买商品的业务逻辑,来作为具体示例,一共会有三个 Spring Cloud 服务,分别对应自己的数据库。整体如下图所示:
本文的示例代码及SQL可从Gitee下载。
主要代码说明
OrderServiceImpl
创建 OrderServiceImpl 类,实现创建订单的方法。代码如下:
@Service
public class OrderServiceImpl implements OrderService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private OrderDao orderDao;
@Autowired
private AccountServiceFeignClient accountService;
@Autowired
private ProductServiceFeignClient productService;
@Override
@GlobalTransactional // <1>
public Integer createOrder(Long userId, Long productId, Integer price) {
Integer amount = 1; // 购买数量,暂时设置为 1。
logger.info("[createOrder] 当前 XID: {}", RootContext.getXID());
// <2> 扣减库存
productService.reduceStock(new ProductReduceStockDTO().setProductId(productId).setAmount(amount));
// <3> 扣减余额
accountService.reduceBalance(new AccountReduceBalanceDTO().setUserId(userId).setPrice(price));
// <4> 保存订单
OrderDO order = new OrderDO().setUserId(userId).setProductId(productId).setPayAmount(amount * price);
orderDao.saveOrder(order);
logger.info("[createOrder] 保存订单: {}", order.getId());
// 返回订单编号
return order.getId();
}
}
<1>
处,在类上,添加 Seata @GlobalTransactional
注解,声明全局事务。
<2>
处,调用 ProductServiceFeignClient 的 #reduceStock(productId, amount)
方法,通过 Feign 远程 HTTP 调用商品服务,进行扣除库存。代码如下:
// ProductServiceFeignClient.java
/**
* `product-service` 服务的 Feign 客户端
*/
@FeignClient(name = "product-service")
public interface ProductServiceFeignClient {
@PostMapping("/product/reduce-stock")
void reduceStock(@RequestBody ProductReduceStockDTO productReduceStockDTO);
}
// ProductReduceStockDTO.java
/**
* 商品减少库存 DTO
*/
public class ProductReduceStockDTO {
/** 商品编号 */
private Long productId;
/** 数量 */
private Integer amount;
// ... 省略 setter/getter 方法
}
- 远程调用失败,又或是扣除库存失败,都会抛出 Exception 异常,从而回滚 Seata 全局事务。
<3>
处,调用 AccountServiceFeignClient 的 #reduceBalance(userId, price)
方法,通过 Feign 远程 HTTP 调用账户服务,进行扣除余额。代码如下:
// AccountServiceFeignClient.java
/**
* `account-service` 服务的 Feign 客户端
*/
@FeignClient(name = "account-service")
public interface AccountServiceFeignClient {
@PostMapping("/account/reduce-balance")
void reduceBalance(@RequestBody AccountReduceBalanceDTO accountReduceBalanceDTO);
}
// AccountReduceBalanceDTO.java
/**
* 账户减少余额 DTO
*/
public class AccountReduceBalanceDTO {
/** 用户编号 */
private Long userId;
/** 扣减金额 */
private Integer price;
// ... 省略 setter/getter 方法
}
- 远程调用失败,又或是扣除余额失败,都会抛出 Exception 异常,从而回滚 Seata 全局事务。
<4>
处,在全部调用成功后,调用 OrderDao 保存订单。
测试
使用 Postman 模拟调用 http://127.0.0.1:8081/order/create 创建订单的接口。