SpringCloud —— 分布式事务管理Seata详解

一、前言

黑马商城的交易微服务,会同时远程调用商品微服务和购物车微服务,假设一个场景:有两个用户同时使用微服务,当同一个商品的库存为1时,一个用户进入下单界面了,但是另一个用户已经下单成功了,那么此时库存肯定就不够了,那么结算界面的用户点击下单时肯定会下单失败,但是这个时候会发现,下单虽然失败了,但是购物车却被清空了,这个场景显然是不便于正常用户体验的,所以我们需要解决这个问题。

这个问题的核心是,购物车微服务和交易微服务被同时远程调用,当交易微服务报错时,购物车微服务已经将事务提交了,所以无法恢复到下单前的购物车状态,此时我们发现我们需要一个全局的事务管理来控制整个下单的事务。

二、Seata

Seata是由阿里开发的分布式事务管理组件,可以用于全局管理微服务的事务。

1.环境搭建

(1)导入数据库

首先要导入seata的数据库:

(2)配置文件

将配置文件拷到虚拟机的根目录中:

(3)部署

在docker中部署seata:

bash 复制代码
docker run --name seata \
-p 8099:8099 \
-p 7099:7099 \
-e SEATA_IP=192.168.150.101 \
-v ./seata:/seata-server/resources \
--privileged=true \
--network hm-net \
-d \
seataio/seata-server:1.5.2

2.微服务集成Seata

首先需要在微服务中导Seata的包:

XML 复制代码
        <!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>

同时需要在项目中添加seata的配置,由于许多微服务都要集成seata,所以我们不妨将关于seata的配置写到nacos的共享配置中,这样不仅便于热更新,同时还可以简化项目文件中的配置书写。

bash 复制代码
seata:
  registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    type: nacos # 注册中心类型 nacos
    nacos:
      server-addr: 192.168.111.111:8848 # nacos地址
      namespace: "" # namespace,默认为空
      group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
      application: seata-server # seata服务名称
      username: nacos
      password: nacos
  tx-service-group: hmall # 事务组名称
  service:
    vgroup-mapping: # 事务组与tc集群的映射关系
      hmall: "default"
  data-source-proxy-mode: XA

于是在项目中就只需要声明共享配置即可:

然后我们在这里需要用注解@GlobalTransactional标记事务入口:

3.XA模式

在刚刚的seata共享配置中,我们可以看到有一行配置:

bash 复制代码
  data-source-proxy-mode: XA

这一行配置其实就是在选择seata的模式,seata由XA和AT两种模式,在这里我们先讲XA模式,首先需要明确,为啥之前我们创建订单会出现问题,再回顾一下问题:

当同一个商品的库存为1时,一个用户进入下单界面了,但是另一个用户已经下单成功了,那么此时库存肯定就不够了,那么结算界面的用户点击下单时肯定会下单失败,但是这个时候会发现,下单虽然失败了,但是购物车却被清空了。

这是因为流程的不同步,OrderService中调用的itemClient和cartClient是去调用的两个不同的微服务,由于这两个微服务是不在同一个模块中的,所以是无法进行常规事务管理的,所以当扣减库存抛异常时,购物车已经被清空了,并且由于没有事务,购物车无法回滚,清空的购物车也就不会恢复了。

此时XA模式就提供了一个解决办法,就是将购物车清空的行为不提交,等待扣减库存的行为完成后同时提交,如果扣减的时候抛出异常了,就回滚事务。

这个模式会出现一个问题,就是等待的时间问题,等待时会占用线程,所以使用XA模式时,整个事务的处理效率会比较低。

4.AT模式

只需要在共享配置中将这个改成AT即可:

bash 复制代码
  data-source-proxy-mode: AT

AT模式的原理和XA又不相同了,AT是通过保存快照来解决事务问题的,当cartClient调用时,首先给购物车保存一个快照,然后直接提交cartClient的清空行为,当扣减库存抛出异常时,回滚事务,将购物车恢复成之前保存的快照的状态。

AT模式就将等待的时间给消除了,使用快照来保存原始状态,可以说是使用空间换时间了。

值得一提的是,Seata默认的模式是AT。

5.番外篇

经过我的测试,其实这里是不一定要使用分布式事务的,如果将扣减库存和清空购物车的代码交换位置,那么只需要一个普通事务就可以解决这个问题了。

因为本身ItemService的deductStock方法是有事务的,也就是说如果这个远程调用抛出异常,在Item-sercice的微服务中,这个库存数目是会回滚的,同时,在orderSercice中,由于库存不足抛出了异常,这个方法肯定会被终止,自然也就执行不到后面清空购物车的代码了,所以购物车根本不会改变。整体从运行效果上也不会出现什么问题。

也就是如下代码就可以解决这个问题。

java 复制代码
@Override
    @Transactional
    public Long createOrder(OrderFormDTO orderFormDTO) {
        // 1.订单数据
        Order order = new Order();
        // 1.1.查询商品
        List<OrderDetailDTO> detailDTOS = orderFormDTO.getDetails();
        // 1.2.获取商品id和数量的Map
        Map<Long, Integer> itemNumMap = detailDTOS.stream()
                .collect(Collectors.toMap(OrderDetailDTO::getItemId, OrderDetailDTO::getNum));
        Set<Long> itemIds = itemNumMap.keySet();
        // 1.3.查询商品
        List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
        if (items == null || items.size() < itemIds.size()) {
            throw new BadRequestException("商品不存在");
        }
        // 1.4.基于商品价格、购买数量计算商品总价:totalFee
        int total = 0;
        for (ItemDTO item : items) {
            total += item.getPrice() * itemNumMap.get(item.getId());
        }
        order.setTotalFee(total);
        // 1.5.其它属性
        order.setPaymentType(orderFormDTO.getPaymentType());
        order.setUserId(UserContext.getUser());
        order.setStatus(1);
        // 1.6.将Order写入数据库order表中
        save(order);

        // 2.保存订单详情
        List<OrderDetail> details = buildDetails(order.getId(), items, itemNumMap);
        iOrderDetailService.saveBatch(details);

        // 4.扣减库存(交换)
        try {
            itemClient.deductStock(detailDTOS);
        } catch (Exception e) {
            throw new RuntimeException("库存不足!");
        }

        // 3.清理购物车商品(交换)
        cartClient.removeByItemIds(itemIds);

        return order.getId();
    }

当然,这是基于这个事务还是比较简单的基础上的,当事务的逻辑变得更复杂时,使用分布式事务绝对是更好的选择。

相关推荐
LJianK12 小时前
前后端接口常见传参
java·spring
小王师傅662 小时前
【轻松入门SpringBoot】actuator健康检查(中)
java·spring boot·spring
回家路上绕了弯3 小时前
分布式系统重试策略详解:可靠性与资源消耗的平衡艺术
分布式·后端
海南java第二人3 小时前
Spring事务注解@Transactional参数详解与实战指南
spring
努力的小郑3 小时前
Spring AOP + Guava RateLimiter:我是如何用注解实现优雅限流的?
后端·spring·面试
BD_Marathon4 小时前
Spring系统架构
java·spring·系统架构
无名小卒Rain4 小时前
Jmeter性能测试-分布式压测配置和执行过程
分布式·jmeter
a程序小傲4 小时前
蚂蚁Java面试被问:分布式Session的实现方案
java·分布式·面试
a努力。4 小时前
京东Java面试:如何设计一个分布式ID生成器
java·分布式·后端·面试