分布式事务(三)--Seata TCC模式事务

TCC 基本原理

TCC 与 Seata AT 事务一样都是两阶段事务,它与 AT 事务的主要区别为:

  • TCC 对业务代码侵入严重
    每个阶段的数据操作都要自己进行编码来实现,事务框架无法自动处理。
  • TCC 效率更高
    不必对数据加全局锁,允许多个事务同时操作数据。

分布式事务(三)--Seata TCC模式事务_第1张图片

第一阶段 Try

以账户服务为例,当下订单时要扣减用户账户金额:

分布式事务(三)--Seata TCC模式事务_第2张图片

假如用户购买 100 元商品,要扣减 100 元。

TCC 事务首先对这100元的扣减金额进行预留,或者说是先冻结这100元:

分布式事务(三)--Seata TCC模式事务_第3张图片

第二阶段 Confirm

如果第一阶段能够顺利完成,那么说明“扣减金额”业务(分支事务)最终肯定是可以成功的。当全局事务提交时, TC会控制当前分支事务进行提交,如果提交失败,TC 会反复尝试,直到提交成功为止。

当全局事务提交时,就可以使用冻结的金额来最终实现业务数据操作:
分布式事务(三)--Seata TCC模式事务_第4张图片

第二阶段 Cancel

如果全局事务回滚,就把冻结的金额进行解冻,恢复到以前的状态,TC 会控制当前分支事务回滚,如果回滚失败,TC 会反复尝试,直到回滚完成为止。

分布式事务(三)--Seata TCC模式事务_第5张图片

多个事务并发的情况

多个TCC全局事务允许并发,它们执行扣减金额时,只需要冻结各自的金额即可:

分布式事务(三)--Seata TCC模式事务_第6张图片

Seata TCC事务模式

Seata 支持 TCC 事务模式,与 AT 模式相同的,也需要以下组件来支持全局事务的控制:

  • TC 事务协调器
  • TM 事务管理器
  • RM 资源管理器

准备订单项目案例

新建 seata-tcc 工程

新建 Empty Project:
在这里插入图片描述
工程命名为 seata-tcc,存放到 seata-samples 文件夹下,与 seata-at 工程存放在一起:

分布式事务(三)--Seata TCC模式事务_第7张图片

导入订单项目,无事务版本

下载项目代码

  1. 访问 git 仓库 https://gitee.com/benwang6/seata-samples
  2. 访问项目标签
    a
  3. 下载无事务版
    a

解压到 seata-tcc 目录

压缩文件中的 7 个项目目录解压缩到 seata-tcc 目录:

分布式事务(三)--Seata TCC模式事务_第8张图片

导入项目

在 idea 中按两下 shift 键,搜索 add maven projects,打开 maven 工具:
a

然后选择 seata-tcc 工程目录下的 7 个项目的 pom.xml 导入:

分布式事务(三)--Seata TCC模式事务_第9张图片

order启动全局事务,添加“保存订单”分支事务

在订单项目中执行添加订单:

分布式事务(三)--Seata TCC模式事务_第10张图片

我们要添加以下 TCC 事务操作的代码:

  • Try - 第一阶,冻结数据阶段,向订单表直接插入订单,订单状态设置为0(冻结状态)。

分布式事务(三)--Seata TCC模式事务_第11张图片

  • Confirm - 第二阶段,提交事务,将订单状态修改成1(正常状态)。

分布式事务(三)--Seata TCC模式事务_第12张图片

  • Cancel - 第二阶段,回滚事务,删除订单。

分布式事务(三)--Seata TCC模式事务_第13张图片

order-parent 添加 seata 依赖

打开 order-parent 中注释掉的 seata 依赖:




    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.3.2.RELEASE
         
    
    cn.tedu
    order-parent
    1.0-SNAPSHOT
    pom
    order-parent

    
        3.3.2
        1.1.23
        1.3.0
        2.0.0.RELEASE
        Hoxton.SR6
    

    
        
            org.springframework.boot
            spring-boot-starter
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.cloud
            spring-cloud-starter-netflix-eureka-client
        
        
            org.springframework.cloud
            spring-cloud-starter-openfeign
        

        
            com.baomidou
            mybatis-plus-boot-starter
            ${mybatis-plus.version}
        

        
            mysql
            mysql-connector-java
        
        
            com.alibaba
            druid-spring-boot-starter
            ${druid-spring-boot-starter.version}
        

        
        
          com.alibaba.cloud
          spring-cloud-alibaba-seata
          ${spring-cloud-alibaba-seata.version}
          
            
              seata-all
              io.seata
            
          
        
        
          io.seata
          seata-all
          ${seata.version}
        

        
            org.projectlombok
            lombok
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
            
                
                    org.junit.vintage
                    junit-vintage-engine
                
            
        
    

    
        
            
                org.springframework.cloud
                spring-cloud-dependencies
                ${spring-cloud.version}
                pom
                import
            
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

 

配置

application.yml

设置全局事务组的组名:

spring:
  application:
    name: order

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost/seata_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    username: root
    password: root

  # 事务组设置
  cloud:
    alibaba:
      seata:
        tx-service-group: order_tx_group

......

registry.conf 和 file.conf

与 AT 事务中的配置完全相同:

registry.conf

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "eureka"

  nacos {
    serverAddr = "localhost"
    namespace = ""
    cluster = "default"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    # application = "default"
    # weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = "0"
    password = ""
    cluster = "default"
    timeout = "0"
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
  type = "file"

  nacos {
    serverAddr = "localhost"
    namespace = ""
    group = "SEATA_GROUP"
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    app.id = "seata-server"
    apollo.meta = "http://192.168.1.204:8801"
    namespace = "application"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

file.conf

transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  # the client batch send request enable
  enableClientBatchSendRequest = true
  #thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThread-prefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    bossThreadSize = 1
    #auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  #transaction service group mapping
  # order_tx_group 与 yml 中的 “tx-service-group: order_tx_group” 配置一致
  # “seata-server” 与 TC 服务器的注册名一致
  # 从eureka获取seata-server的地址,再向seata-server注册自己,设置group
  vgroupMapping.order_tx_group = "seata-server"
  #only support when registry.type=file, please don't set multiple addresses
  order_tx_group.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

client {
  rm {
    asyncCommitBufferLimit = 10000
    lock {
      retryInterval = 10
      retryTimes = 30
      retryPolicyBranchRollbackOnConflict = true
    }
    reportRetryCount = 5
    tableMetaCheckEnable = false
    reportSuccessEnable = false
  }
  tm {
    commitRetryCount = 5
    rollbackRetryCount = 5
  }
  undo {
    dataValidation = true
    logSerialization = "jackson"
    logTable = "undo_log"
  }
  log {
    exceptionRate = 100
  }
}

OrderMapper 添加更新订单状态、删除订单

根据前面的分析,订单数据操作有以下三项:

  • 插入订单
  • 修改订单状态
  • 删除订单

在 OrderMapper 中已经有插入订单的方法了,现在需要添加修改订单和删除订单的方法(删除方法从BaseMapper继承):

package cn.tedu.order.mapper;

import cn.tedu.order.entity.Order;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

public interface OrderMapper extends BaseMapper {
    void create(Order order);
    void updateStatus(@Param("orderId") Long orderId, @Param("status") Integer status);
}

那么对应的 OrderMapper.xml 中也要添加 sql:




    
        
        
        
        
        
        
    
    
        INSERT INTO `order` (`id`,`user_id`,`product_id`,`count`,`money`,`status`)
        VALUES(#{id}, #{userId}, #{productId}, #{count}, #{money}, ${status});
    
    
        UPDATE `order` SET `status`=#{status} WHERE `id`=#{orderId};
    
    
        DELETE FROM `order` WHERE `id`=#{orderId}
    

Seata 实现订单的 TCC 操作方法

  • 第一阶段 Try
  • 第二阶段

    • Confirm
    • Cancel

第二阶段为了处理幂等性问题这里首先添加一个工具类 ResultHolder

这个工具也可以在第二阶段 Confirm 或 Cancel 阶段对第一阶段的成功与否进行判断,在第一阶段成功时需要保存一个标识。

ResultHolder可以为每一个全局事务保存一个标识:

package cn.tedu.order.tcc;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ResultHolder {
    private static Map, Map> map = new ConcurrentHashMap, Map>();

    public static void setResult(Class actionClass, String xid, String v) {
        Map results = map.get(actionClass);

        if (results == null) {
            synchronized (map) {
                if (results == null) {
                    results = new ConcurrentHashMap<>();
                    map.put(actionClass, results);
                }
            }
        }

        results.put(xid, v);
    }

    public static String getResult(Class actionClass, String xid) {
        Map results = map.get(actionClass);
        if (results != null) {
            return results.get(xid);
        }

        return null;
    }

    public static void removeResult(Class actionClass, String xid) {
        Map results = map.get(actionClass);
        if (results != null) {
            results.remove(xid);
        }
    }
}

Seata 实现 TCC 操作需要定义一个接口,我们在接口中添加以下方法:

  • Try - prepareCreateOrder()
  • Confirm - commit()
  • Cancel - rollback()
package cn.tedu.order.tcc;

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

import java.math.BigDecimal;

@LocalTCC
public interface OrderTccAction {
    // T (try - 预留资源,冻结订单)
    /*
    第一阶段的方法
    通过注解指定第二阶段的两个方法名
    
    BusinessActionContext 上下文对象,用来在两个阶段之间传递数据
    @BusinessActionContextParameter 注解的参数数据会被存入 BusinessActionContext
    @TwoPhaseBusinessAction 提交回滚的默认值可以不写
     */
    @TwoPhaseBusinessAction(name = "orderTccAction", commitMethod = "commit", rollbackMethod = "rollback")
    boolean prepareCreateOrder(BusinessActionContext businessActionContext,
                      @BusinessActionContextParameter(paramName = "orderId") Long orderId,
                      @BusinessActionContextParameter(paramName = "userId") Long userId,
                      @BusinessActionContextParameter(paramName = "productId") Long productId,
                      @BusinessActionContextParameter(paramName = "count") Integer count,
                      @BusinessActionContextParameter(paramName = "money") BigDecimal money);

    //C (confirm - 确认,提交)
    boolean commit(BusinessActionContext businessActionContext);

    //C (cancel - 取消,回滚)
    boolean rollback(BusinessActionContext businessActionContext);

}

实现类:

package cn.tedu.order.tcc;

import cn.tedu.order.entity.Order;
import cn.tedu.order.mapper.OrderMapper;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
@Component
public class OrderTccActionImpl implements OrderTccAction {
    @Autowired
    private OrderMapper orderMapper;

    @Override
    public boolean prepareCreateOrder(BusinessActionContext ctx, Long orderId, Long userId, Long productId, Integer count, BigDecimal money) {
        orderMapper.create(new Order(orderId, userId, productId, count, money, 0));

        //保存一阶段的成功标记
        ResultHolder.setResult(OrderTccAction.class, ctx.getXid(), "p");
        return true;
    }

    @Override
    public boolean commit(BusinessActionContext ctx) {
        //检查标记是否存在,如果标记不存在不重复提交
        String p = ResultHolder.getResult(OrderTccAction.class, ctx.getXid());
        if (p == null){
            return true;
        }

        /**
         * 上下文对象从第一阶段向第二阶段传递时,先转成了json数据,然后还原成上下文对象
         * 其中的整数比较小的会转成Integer类型,所以如果需要Long类型,需要先转换成字符串在用Long.valueOf()解析。
         */
        Long orderId = Long.valueOf(ctx.getActionContext("orderId").toString());
        orderMapper.updateStatus(orderId, 1);

        //提交完成后,删除标记
        ResultHolder.removeResult(OrderTccAction.class, ctx.getXid());
        return true;
    }

    @Override
    public boolean rollback(BusinessActionContext ctx) {
        //第一阶段没有完成的情况下,不必执行回滚
        //因为第一阶段有本地事务,事务失败时已经进行了回滚。
        //如果这里第一阶段成功,而其他全局事务参与者失败,这里会执行回滚
        //幂等性控制:如果重复执行回滚则直接返回
        
        //检查标记是否存在,如果标记不存在不重复提交
        String p = ResultHolder.getResult(OrderTccAction.class, ctx.getXid());
        if (p == null){
            return true;
        }

        Long orderId = Long.valueOf(ctx.getActionContext("orderId").toString());
        orderMapper.deleteById(orderId);

        //提交完成后,删除标记
        ResultHolder.removeResult(OrderTccAction.class, ctx.getXid());
        return true;
    }
}

在业务代码中调用 Try 阶段方法

业务代码中不再直接保存订单数据,而是调用 TCC 第一阶段方法prepareCreateOrder(),并添加全局事务注解 @GlobalTransactional

package cn.tedu.order.service;

import cn.tedu.order.entity.Order;
import cn.tedu.order.feign.AccountClient;
import cn.tedu.order.feign.EasyIdGeneratorClient;
import cn.tedu.order.feign.StorageClient;
import cn.tedu.order.mapper.OrderMapper;
import cn.tedu.order.tcc.OrderTccAction;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Random;

@Service
public class OrderServiceImpl implements OrderService {
    // @Autowired
    // private OrderMapper orderMapper;
    @Autowired
    EasyIdGeneratorClient easyIdGeneratorClient;
    @Autowired
    private AccountClient accountClient;
    @Autowired
    private StorageClient storageClient;

    @Autowired
    private OrderTccAction orderTccAction;

    @GlobalTransactional
    @Override
    public void create(Order order) {
        // 从全局唯一id发号器获得id
        Long orderId = easyIdGeneratorClient.nextId("order_business");
        order.setId(orderId);

        // orderMapper.create(order);

        // 这里修改成调用 TCC 第一节端方法
        //orderTccAction是一个动态代理对象,其中添加了前置拦截器,所以底层会创建,所以传null值即可
        orderTccAction.prepareCreateOrder(
                null,
                order.getId(),
                order.getUserId(),
                order.getProductId(),
                order.getCount(),
                order.getMoney());

        // 修改库存
        //storageClient.decrease(order.getProductId(), order.getCount());

        // 修改账户余额
        //accountClient.decrease(order.getUserId(), order.getMoney());

    }
}

启动 order 进行测试

按顺序启动服务:

  1. Eureka
  2. Seata Server
  3. Easy Id Generator
  4. Order

调用保存订单,地址:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100

观察控制台日志:
分布式事务(三)--Seata TCC模式事务_第14张图片

查看数据库表中的订单数据:

分布式事务(三)--Seata TCC模式事务_第15张图片

storage添加“减少库存”分支事务

在库存项目中执行减少库存:

分布式事务(三)--Seata TCC模式事务_第16张图片

我们要添加以下 TCC 事务操作的代码:

  • Try - 第一阶,冻结数据阶段,将要减少的库存量先冻结:

分布式事务(三)--Seata TCC模式事务_第17张图片

  • Confirm - 第二阶段,提交事务,使用冻结的库存完成业务数据处理:

分布式事务(三)--Seata TCC模式事务_第18张图片

  • Cancel - 第二阶段,回滚事务,冻结的库存解冻,恢复以前的库存量:

分布式事务(三)--Seata TCC模式事务_第19张图片

配置

有三个文件需要配置:

  • application.yml
  • registry.conf
  • file.conf

这三个文件的设置与上面 order 项目的配置完全相同,请参考上面订单配置一章进行配置。

StorageMapper 添加冻结库存相关方法

根据前面的分析,库存数据操作有以下三项:

  • 冻结库存
  • 冻结库存量修改为已售出量
  • 解冻库存

在 StorageMapper 中添加三个方法:

package cn.tedu.storage.mapper;

import cn.tedu.storage.entity.Storage;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

public interface StorageMapper extends BaseMapper {
    void decrease(Long productId, Integer count);
    // 冻结库存
    void updateFrozen(@Param("productId") Long productId, @Param("residue") Integer residue, @Param("frozen") Integer frozen);
    // 提交时,把冻结量修改到已售出
    void updateFrozenToUsed(@Param("productId") Long productId, @Param("count") Integer count);
    // 回滚时,把冻结量修改到可用库存
    void updateFrozenToResidue(@Param("productId") Long productId, @Param("count") Integer count);
} 

那么对应的 StorageMapper.xml 中也要添加 sql:




    
        
        
        
        
        
    
    
    UPDATE storage SET used = used + #{count},residue = residue - #{count} WHERE product_id = #{productId}
  
    

    
        UPDATE storage SET `residue`=#{residue},`frozen`=#{frozen} WHERE `product_id`=#{productId}
    

    
        UPDATE storage SET `frozen`=`frozen`-#{count}, `used`=`used`+#{count} WHERE `product_id`=#{productId}
    

    
        UPDATE storage SET `frozen`=`frozen`-#{count}, `residue`=`residue`+#{count} WHERE `product_id`=#{productId}
    

Seata 实现库存的 TCC 操作方法

工具类 ResultHolder

package cn.tedu.storage.tcc;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ResultHolder {
    private static Map, Map> map = new ConcurrentHashMap, Map>();

    public static void setResult(Class actionClass, String xid, String v) {
        Map results = map.get(actionClass);

        if (results == null) {
            synchronized (map) {
                if (results == null) {
                    results = new ConcurrentHashMap<>();
                    map.put(actionClass, results);
                }
            }
        }

        results.put(xid, v);
    }

    public static String getResult(Class actionClass, String xid) {
        Map results = map.get(actionClass);
        if (results != null) {
            return results.get(xid);
        }

        return null;
    }

    public static void removeResult(Class actionClass, String xid) {
        Map results = map.get(actionClass);
        if (results != null) {
            results.remove(xid);
        }
    }
}

添加 TCC 接口,在接口中添加以下方法:

  • Try - prepareDecreaseStorage()
  • Confirm - commit()
  • Cancel - rollback()
package cn.tedu.storage.tcc;

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

@LocalTCC
public interface StorageTccAction {

    @TwoPhaseBusinessAction(name = "storageTccAction", commitMethod = "commit", rollbackMethod = "rollback")
    boolean prepareDecreaseStorage(BusinessActionContext businessActionContext,
                                   @BusinessActionContextParameter(paramName = "productId") Long productId,
                                   @BusinessActionContextParameter(paramName = "count") Integer count);

    boolean commit(BusinessActionContext businessActionContext);

    boolean rollback(BusinessActionContext businessActionContext);

}

实现类:

package cn.tedu.storage.tcc;

import cn.tedu.storage.entity.Storage;
import cn.tedu.storage.mapper.StorageMapper;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
@Slf4j
public class StorageTccActionImpl implements StorageTccAction {

    @Autowired
    private StorageMapper storageMapper;

    @Transactional
    @Override
    public boolean prepareDecreaseStorage(BusinessActionContext businessActionContext, Long productId, Integer count) {
        log.info("减少商品库存,第一阶段,锁定减少的库存量,productId="+productId+", count="+count);

        Storage storage = storageMapper.selectById(productId);
        if (storage.getResidue()-count<0) {
            throw new RuntimeException("库存不足");
        }

        /*
        库存减掉count, 冻结库存增加count
         */
        storageMapper.updateFrozen(productId, storage.getResidue()-count, storage.getFrozen()+count);

        //保存标识
        ResultHolder.setResult(getClass(), businessActionContext.getXid(), "p");
        return true;
    }

    @Transactional
    @Override
    public boolean commit(BusinessActionContext businessActionContext) {
        long productId = Long.parseLong(businessActionContext.getActionContext("productId").toString());
        int count = Integer.parseInt(businessActionContext.getActionContext("count").toString());
        log.info("减少商品库存,第二阶段提交,productId="+productId+", count="+count);

        //防止重复提交
        if (ResultHolder.getResult(getClass(), businessActionContext.getXid()) == null) {
            return true;
        }

        storageMapper.updateFrozenToUsed(productId, count);

        //删除标识
        ResultHolder.removeResult(getClass(), businessActionContext.getXid());
        return true;
    }

    @Transactional
    @Override
    public boolean rollback(BusinessActionContext businessActionContext) {

        long productId = Long.parseLong(businessActionContext.getActionContext("productId").toString());
        int count = Integer.parseInt(businessActionContext.getActionContext("count").toString());
        log.info("减少商品库存,第二阶段,回滚,productId="+productId+", count="+count);

        //防止重复回滚
        if (ResultHolder.getResult(getClass(), businessActionContext.getXid()) == null) {
            return true;
        }

        storageMapper.updateFrozenToResidue(productId, count);

        //删除标识
        ResultHolder.removeResult(getClass(), businessActionContext.getXid());
        return true;
    }
} 

在业务代码中调用 Try 阶段方法

业务代码中调用 TCC 第一阶段方法prepareDecreaseStorage(),并添加全局事务注解 @GlobalTransactional

package cn.tedu.storage.service;

import cn.tedu.storage.tcc.StorageTccAction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class StorageServiceImpl implements StorageService {
    // @Autowired
    // private StorageMapper storageMapper;

    @Autowired
    private StorageTccAction storageTccAction;

    @Override
    public void decrease(Long productId, Integer count) throws Exception {
        // storageMapper.decrease(productId,count);
        storageTccAction.prepareDecreaseStorage(null, productId, count);
    }

}

启动 storage 进行测试

按顺序启动服务:

  1. Eureka
  2. Seata Server
  3. Easy Id Generator
  4. Storage
  5. Order

调用保存订单,地址:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100

观察 storage 的控制台日志:
分布式事务(三)--Seata TCC模式事务_第20张图片

查看数据库表中的库存数据:

分布式事务(三)--Seata TCC模式事务_第21张图片

account添加“扣减金额”分支事务

扣减金额 TCC 事务分析请见《分布式事务(六)Seata TCC模式-TCC模式介绍

配置

有三个文件需要配置:

  • application.yml
  • registry.conf
  • file.conf

这三个文件的设置与上面 order 项目的配置完全相同,请参考上面订单配置一章进行配置。

AccountMapper 添加冻结库存相关方法

根据前面的分析,库存数据操作有以下三项:

  • 冻结库存
  • 冻结库存量修改为已售出量
  • 解冻库存

在 AccountMapper 中添加三个方法:

package cn.tedu.account.mapper;

import cn.tedu.account.entity.Account;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

import java.math.BigDecimal;

public interface AccountMapper extends BaseMapper {
    void decrease(Long userId, BigDecimal money);

    void updateFrozen(@Param("userId") Long userId, @Param("residue") BigDecimal residue, @Param("frozen") BigDecimal frozen);

    void updateFrozenToUsed(@Param("userId") Long userId, @Param("money") BigDecimal money);

    void updateFrozenToResidue(@Param("userId") Long userId, @Param("money") BigDecimal money);
}

那么对应的 AccountMapper.xml 中添加 sql:




    
        
        
        
        
        
        
    
    
      UPDATE account SET residue = residue - #{money},used = used + #{money} where user_id = #{userId};
    
    

    
        UPDATE account SET `residue`=#{residue},`frozen`=#{frozen} WHERE `user_id`=#{userId}
    

    
        UPDATE account SET `frozen`=`frozen`-#{money}, `used`=`used`+#{money} WHERE `user_id`=#{userId}
    

    
        UPDATE account SET `frozen`=`frozen`-#{money}, `residue`=`residue`+#{money} WHERE `user_id`=#{userId}
    

Seata 实现库存的 TCC 操作方法

工具类 ResultHolder

package cn.tedu.account.tcc;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ResultHolder {
    private static Map, Map> map = new ConcurrentHashMap, Map>();

    public static void setResult(Class actionClass, String xid, String v) {
        Map results = map.get(actionClass);

        if (results == null) {
            synchronized (map) {
                if (results == null) {
                    results = new ConcurrentHashMap<>();
                    map.put(actionClass, results);
                }
            }
        }

        results.put(xid, v);
    }

    public static String getResult(Class actionClass, String xid) {
        Map results = map.get(actionClass);
        if (results != null) {
            return results.get(xid);
        }

        return null;
    }

    public static void removeResult(Class actionClass, String xid) {
        Map results = map.get(actionClass);
        if (results != null) {
            results.remove(xid);
        }
    }
}

添加 TCC 接口,在接口中添加以下方法:

  • Try - prepareDecreaseAccount()
  • Confirm - commit()
  • Cancel - rollback()
package cn.tedu.account.tcc;

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

import java.math.BigDecimal;

@LocalTCC
public interface AccountTccAction {

    @TwoPhaseBusinessAction(name = "accountTccAction", commitMethod = "commit", rollbackMethod = "rollback")
    boolean prepareDecreaseAccount(BusinessActionContext businessActionContext,
                                   @BusinessActionContextParameter(paramName = "userId") Long userId,
                                   @BusinessActionContextParameter(paramName = "money") BigDecimal money);

    boolean commit(BusinessActionContext businessActionContext);

    boolean rollback(BusinessActionContext businessActionContext);

}

实现类:

package cn.tedu.account.tcc;

import cn.tedu.account.entity.Account;
import cn.tedu.account.mapper.AccountMapper;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Component
@Slf4j
public class AccountTccActionImpl implements AccountTccAction {
    @Autowired
    private AccountMapper accountMapper;

    @Transactional
    @Override
    public boolean prepareDecreaseAccount(BusinessActionContext businessActionContext, Long userId, BigDecimal money) {
        log.info("减少账户金额,第一阶段锁定金额,userId="+userId+", money="+money);

        Account account = accountMapper.selectById(userId);
        if (account.getResidue().compareTo(money) < 0) {
            throw new RuntimeException("账户金额不足");
        }

        /*
        余额-money
        冻结+money
         */
        accountMapper.updateFrozen(userId, account.getResidue().subtract(money), account.getFrozen().add(money));

        //保存标识
        ResultHolder.setResult(getClass(), businessActionContext.getXid(), "p");
        return true;
    }

    @Transactional
    @Override
    public boolean commit(BusinessActionContext businessActionContext) {

        long userId = Long.parseLong(businessActionContext.getActionContext("userId").toString());
        BigDecimal money =  new BigDecimal(businessActionContext.getActionContext("money").toString());
        log.info("减少账户金额,第二阶段,提交,userId="+userId+", money="+money);

        //防止重复提交
        if (ResultHolder.getResult(getClass(), businessActionContext.getXid()) == null) {
            return true;
        }

        accountMapper.updateFrozenToUsed(userId, money);

        //删除标识
        ResultHolder.removeResult(getClass(), businessActionContext.getXid());
        return true;
    }

    @Transactional
    @Override
    public boolean rollback(BusinessActionContext businessActionContext) {
        long userId = Long.parseLong(businessActionContext.getActionContext("userId").toString());
        BigDecimal money =  new BigDecimal(businessActionContext.getActionContext("money").toString());

        //防止重复回滚
        if (ResultHolder.getResult(getClass(), businessActionContext.getXid()) == null) {
            return true;
        }

        log.info("减少账户金额,第二阶段,回滚,userId="+userId+", money="+money);

        accountMapper.updateFrozenToResidue(userId, money);

        //删除标识
        ResultHolder.removeResult(getClass(), businessActionContext.getXid());
        return true;
    }
}

在业务代码中调用 Try 阶段方法

业务代码中调用 TCC 第一阶段方法prepareDecreaseAccount(),并添加全局事务注解 @GlobalTransactional

package cn.tedu.account.service;

import cn.tedu.account.mapper.AccountMapper;
import cn.tedu.account.tcc.AccountTccAction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
@Service
public class AccountServiceImpl implements AccountService {
    // @Autowired
    // private AccountMapper accountMapper;

    @Autowired
    private AccountTccAction accountTccAction;

    @Override
    public void decrease(Long userId, BigDecimal money) {
        // accountMapper.decrease(userId,money);
        accountTccAction.prepareDecreaseAccount(null, userId, money);
    }
}

启动 account 进行测试

按顺序启动服务:

  1. Eureka
  2. Seata Server
  3. Easy Id Generator
  4. Storage
  5. Account
  6. Order

调用保存订单,地址:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100

观察 account 的控制台日志:
分布式事务(三)--Seata TCC模式事务_第22张图片

查看数据库表中的账户数据:

分布式事务(三)--Seata TCC模式事务_第23张图片

全局事务回滚测试

下面来测试全局事务回滚的情况。

订单和库存第一阶段成功,而账户第一阶段失败了,这时会触发全局事务的回滚,如下图所示:

分布式事务(三)--Seata TCC模式事务_第24张图片
首先在 account 的第一阶段代码中添加模拟异常:

AccountTccActionImplprepareDecreaseAccount 方法

@Transactional
    @Override
    public boolean prepareDecreaseAccount(BusinessActionContext businessActionContext, Long userId, BigDecimal money) {
        log.info("减少账户金额,第一阶段锁定金额,userId="+userId+", money="+money);

        Account account = accountMapper.selectById(userId);
        if (account.getResidue().compareTo(money) < 0) {
            throw new RuntimeException("账户金额不足");
        }

        /*
        余额-money
        冻结+money
         */
        accountMapper.updateFrozen(userId, account.getResidue().subtract(money), account.getFrozen().add(money));

        if (Math.random() < 0.5) {
            throw new RuntimeException("模拟异常");
        }

        //保存标识
        ResultHolder.setResult(getClass(), businessActionContext.getXid(), "p");
        return true;
    }

重启 account 后,访问订单:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100

查看控制台,可以看到 storage 和 order 的回滚日志,order 的回滚日志如下:

分布式事务(三)--Seata TCC模式事务_第25张图片

你可能感兴趣的:(分布式事务seata)