五阶段--分布式事务--Seata TCC模式-TCC模式/Spring Cloud微服务添加 TCC 分布式事务

目录

一 TCC 基本原理

 二 TCC 事务

1 创建工程,导入项目

2 添加TCC事务

2.1 订单order工程

2.2 库存storage工程

2.3 账户Account 工程


分布式事务(六)Seata TCC模式-TCC模式介绍

一 TCC 基本原理

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

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

a

 二 TCC 事务

1 创建工程,导入项目

第一步: 创建 empty project: seata-tcc  ,独立的project工程,要与seata-at工程区分开

在这里插入图片描述

第二步: "seata-tcc/无事务版本.zip",解压后,只能一个个导入,project Structure-->modules-->import module-->挨个找到七个工程导入

 或者通过右侧的Maven中的加号找到七个工程的pom文件导入

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

a

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

a

2 添加TCC事务

2.1 订单order工程

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

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

a

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

a

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

a

第一步: order-parent1父工程添加seata 依赖

<!--分布式事务seata依赖-->
        <dependency>
          <groupId>com.alibaba.cloud</groupId>
          <artifactId>spring-cloud-alibaba-seata</artifactId>
          <version>${spring-cloud-alibaba-seata.version}</version>
          <exclusions>
            <exclusion>
              <artifactId>seata-all</artifactId>
              <groupId>io.seata</groupId>
            </exclusion>
          </exclusions>
        </dependency>
        <dependency>
          <groupId>io.seata</groupId>
          <artifactId>seata-all</artifactId>
          <version>${seata.version}</version>
        </dependency>

第二步:三个配置文件

  • application.yml -- 设置全局事务组的组名
spring:
  application:
    name: order

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql:///seata_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    username: root
    password: root
    # 事务组设置
  cloud:
    alibaba:
      seata:
        tx-service-group: order_tx_group
server:
  port: 8083

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true
#    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}

mybatis-plus:
  type-aliases-package: cn.tedu.order.entity
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true

logging:
  level:
    cn.tedu.order.mapper: DEBUG

  • 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
  }
}

第三步: 修改Mapper , 添加 TCC 三个数据库操作

package cn.tedu.order.mapper;

import cn.tedu.order.entity.Order;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;


public interface OrderMapper extends BaseMapper<Order> {
    void create(Order order);
    void insertFrozen(Order order); //插入冻结状态订单
    void updateStatus(Long orderId, Integer status); //修改状态
    // 删除方法 deleteById(orderId) 从通用Mapper继承
}
  • 修改resource包下的mapper包的OrderMapper.xml文件,添加上面三个方法的sql语句
  <insert id="insertFrozen">
        INSERT INTO `order` (`id`,`user_id`,`product_id`,`count`,`money`,`status`)
        VALUES(#{id}, #{userId}, #{productId}, #{count}, #{money},#{status});
    </insert>

    <update id="updateStatus">
        update `order` set status=#{status} where id=#{orderId}
    </update>
    <delete id="deleteById">
        delete from `order` where id=#{orderId}
    </delete>

第四步:  第一阶段Try成功后,第二阶段为了处理幂等性问题这里首先添加一个工具类 ResultHolder

  • 这个工具也可以在第二阶段 Confirm 或 Cancel 阶段对第一阶段的成功与否进行判断,在第一阶段成功时需要保存一个标识。
  • ResultHolder可以为每一个全局事务保存一个标识:复制粘贴使用即可
package cn.tedu.order.tcc;

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

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

    public static void setResult(Class<?> actionClass, String xid, String v) {
        Map<String, String> 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<String, String> results = map.get(actionClass);
        if (results != null) {
            return results.get(xid);
        }

        return null;
    }

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

 第五步:按照 seata tcc 的规则,定义 TccAcction 接口和实现类

order工程创建tcc包:

package cn.tedu.order.tcc;

import cn.tedu.order.entity.Order;
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;

/**
 * 按照 Seata 规则,实现 TccAction 接口
 * 1. 添加 @LocalTcc 注解
 * 2. 添加三个方法-- TCC 方法
 * 3. 第一个方法上添加"两阶段业务操作"注解,
 *    并指定后面两个方法的方法名
 * 4. 三个方法上添加 BusinessActionContext 上下文对象作为参数,
 *    用来从第一阶段向第二阶段传递参数
 * 5. 传递的参数数据,用 @BusinessActionContextParameter 注解放入上下文对象
 * */
@LocalTCC
public interface OrderTccAction {
    /**
     * 避开seata的bug,订单数据一个一个单独传递,而不用封装的Order 对象
     * */
    //表示这些是第一阶段方法,默认方法名,就不用配置提交方法和回滚方法
    @TwoPhaseBusinessAction(name =" OrderTccAction")
    boolean prepare(BusinessActionContext ctx,
                    @BusinessActionContextParameter(paramName = "orderId") Long orderId,
                     Long userId,
                     Long productId,
                     Integer count,
                     BigDecimal money);
    boolean commit(BusinessActionContext ctx);
    boolean rollback(BusinessActionContext ctx);

}

实现类:

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 org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Component
public class OrderTccActionImpl implements OrderTccAction {
    @Autowired
    private OrderMapper orderMapper;

    /*一阶段*/
    @Transactional
    @Override
    public boolean prepare(BusinessActionContext ctx, Long orderId, Long userId, Long productId, Integer count, BigDecimal money) {
        // 冻结订单
        orderMapper.insertFrozen(new Order(orderId,userId,productId,count,money,0));
        // 保存一阶段的成功标记--两个键一个标记
        ResultHolder.setResult(OrderTccAction.class,ctx.getXid(),"p");
        return true;
    }
    /*二阶段*/
    @Transactional
    @Override
    //加同步锁,等待删除成功后才能继续执行其他的
    public synchronized boolean commit(BusinessActionContext ctx) {
        // 判断标记p是否存在--标记不存在,表示二阶段要么执行过,要么一阶段失败,二阶段不再执行
        if (ResultHolder.getResult(OrderTccAction.class,ctx.getXid()) == null){
            return true;//没有标记到此结束
        }
        Long orderId = Long.valueOf(ctx.getActionContext("orderId").toString());
        orderMapper.updateStatus(orderId, 1);
        // 二阶段执行完成,删除键标记
        ResultHolder.removeResult(OrderTccAction.class,ctx.getXid());
        return true;
    }
    /*二阶段*/
    @Transactional
    @Override
    //加同步锁,等待删除成功后才能继续执行其他的
    public synchronized boolean rollback(BusinessActionContext ctx) {
        // 判断标记p是否存在--标记不存在,表示二阶段要么执行过,要么一阶段失败,二阶段不再执行
        if (ResultHolder.getResult(OrderTccAction.class,ctx.getXid()) == null){
            return true;//没有标记到此结束
        }
        Long orderId = Long.valueOf(ctx.getActionContext("orderId").toString());
        orderMapper.deleteById(orderId);
        // 二阶段执行完成,删除键标记
        ResultHolder.removeResult(OrderTccAction.class,ctx.getXid());
        return true;
    }
}

第六步:修改业务方法,调用 TccAcction 的第一阶段方法(Try--预留数据),在第一个模块上添加 @GlobalTransactional 启动全局事务

package cn.tedu.order.service;

import cn.tedu.order.entity.Order;
import cn.tedu.order.feign.AccountClient;
import cn.tedu.order.feign.EasyIdClient;
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.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Random;

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private EasyIdClient easyIdClient;
    @Autowired
    private StorageClient storageClient;
    @Autowired
    private AccountClient accountClient;

    @Autowired
    private OrderTccAction tcc;

    @GlobalTransactional //启动全局事务
    @Override
    public void create(Order order) {
        // 远程调用发号器,获取订单id
        String s = easyIdClient.nextId("order_business");
        Long orderId = Long.valueOf(s);

        order.setId(orderId);
        // orderMapper.create(order);
        /*第一个参数上下文对象,在tcc的动态代理对象中,通过 AOP 添加了前置通知,
        * 在前置代码中回创建上下文对象
        * */
        //不直接正常的创建订单,而是调用TccAction一阶段方法,冻结订单
        tcc.prepare(null,
                order.getId(),
                order.getUserId(),
                order.getProductId(),
                order.getCount(),
                order.getMoney());

        // 远程调用storage库存,减少库存
        //storageClient.decrease(order.getProductId(),order.getCount());
        // 远程调用account账户,减少账户余额
        //accountClient.decrease(order.getUserId(),order.getMoney());
    }
}

第七步:启动 order 进行测试

按顺序启动服务:

Eureka
Seata Server
Easy Id Generator
Order
调用保存订单,地址:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100

观察order工程控制台日志:
a

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

a

2.2 库存storage工程

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

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

a

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

a

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

a

第一步:有三个文件需要配置:

  • application.yml
spring:
  application:
    name: order

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql:///seata_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    username: root
    password: root
    # 事务组设置
  cloud:
    alibaba:
      seata:
        tx-service-group: order_tx_group
server:
  port: 8083

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true
#    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}

mybatis-plus:
  type-aliases-package: cn.tedu.order.entity
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true

logging:
  level:
    cn.tedu.order.mapper: DEBUG

  • registry.conf
  • file.conf

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

第二步: 工具类 ResultHolder

package cn.tedu.storage.tcc;

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

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

    public static void setResult(Class<?> actionClass, String xid, String v) {
        Map<String, String> 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<String, String> results = map.get(actionClass);
        if (results != null) {
            return results.get(xid);
        }

        return null;
    }

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

第三步:StorageMapper接口 添加冻结库存相关方法 以及实现类的方法重写

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

  • 冻结库存
  • 冻结库存量修改为已售出量
  • 解冻库存
package cn.tedu.storage.mapper;

import cn.tedu.storage.entity.Storage;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

public interface StorageMapper extends BaseMapper<Storage> {
    void decrease(Long productId,Integer count);

    Storage selectByProductId(Long productId);
    void updateResidueToFrozen(Long productId,Integer count);//可用-->冻结
    void updateFrozenToUsed(Long productId,Integer count);//冻结-->已使用
    void updateFrozenToResidue(Long productId,Integer count);//冻结-->可用
}
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class StorageTccActionImpl implements StorageTccAction {
    @Autowired
    private StorageMapper storageMapper;

    /*一阶段*/
    @Transactional
    @Override
    public boolean prepare(BusinessActionContext ctx, Long productId, Integer count) {
        Storage storage =
                storageMapper.selectByProductId(productId);
        if (storage.getResidue() < count) {
            throw new RuntimeException("库存不足");
        }
        storageMapper.updateResidueToFrozen(productId,count);
        // 保存一阶段的成功标记--两个键一个标记
        ResultHolder.setResult(StorageTccAction.class,ctx.getXid(),"p");
        return true;
    }

    /*二阶段*/
    @Transactional
    @Override
    //加同步锁,等待删除成功后才能继续执行其他的
    public synchronized boolean commit(BusinessActionContext ctx) {
        // 判断标记p是否存在--标记不存在,表示二阶段要么执行过,要么一阶段失败,二阶段不再执行
        if (ResultHolder.getResult(StorageTccAction.class,ctx.getXid()) == null){
            return true;//没有标记到此结束
        }
        Long productId = Long.valueOf(ctx.getActionContext("productId").toString());
        Integer count = Integer.valueOf(ctx.getActionContext("count").toString());
        storageMapper.updateFrozenToUsed(productId,count);
        // 二阶段执行完成,删除键标记
        ResultHolder.removeResult(StorageTccAction.class,ctx.getXid());
        return true;
    }
    /*二阶段*/
    @Transactional
    @Override
    //加同步锁,等待删除成功后才能继续执行其他的
    public synchronized boolean rollback(BusinessActionContext ctx) {
        // 判断标记p是否存在--标记不存在,表示二阶段要么执行过,要么一阶段失败,二阶段不再执行
        if (ResultHolder.getResult(StorageTccAction.class,ctx.getXid()) == null){
            return true;//没有标记到此结束
        }
        Long productId = Long.valueOf(ctx.getActionContext("productId").toString());
        Integer count = Integer.valueOf(ctx.getActionContext("count").toString());
        storageMapper.updateFrozenToResidue(productId,count);
        // 二阶段执行完成,删除键标记
        ResultHolder.removeResult(StorageTccAction.class,ctx.getXid());
        return true;
    }
}

添加本地事务管理:

第四步:在StorageMapper对应的xml核心配置文件中编辑这三个方法的SQL语句

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.tedu.storage.mapper.StorageMapper" >
    <resultMap id="BaseResultMap" type="cn.tedu.storage.entity.Storage" >
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="product_id" property="productId" jdbcType="BIGINT" />
        <result column="total" property="total" jdbcType="INTEGER" />
        <result column="used" property="used" jdbcType="INTEGER" />
        <result column="residue" property="residue" jdbcType="INTEGER" />
    </resultMap>
    <update id="decrease">
        UPDATE storage SET used = used+#{count},residue=residue-#{count} WHERE product_id = #{productId}
    </update>

    <select id="selectByProductId" resultMap="BaseResultMap">
        select * from storage where product_id = #{productId}
    </select>

    <update id="updateResidueToFrozen">
        update  storage
        set Residue=Residue-#{count},Frozen=Frozen+#{count}
        where product_id = #{productId}
    </update>

    <update id="updateFrozenToUsed">
        update  storage
        set Frozen=Frozen - #{count},Used = Used + #{count}
        where product_id = #{productId}
    </update>

    <update id="updateFrozenToResidue">
        update  storage
        set Frozen=Frozen - #{count},Residue=Residue + #{count}
        where product_id = #{productId}
    </update>
</mapper>

第五步: 库存接口实现类调用 Try 阶段方法,业务代码中调用 TCC 第一阶段方法prepareDecreaseStorage(),并添加全局事务注解 @GlobalTransactional

package cn.tedu.storage.service;

import cn.tedu.storage.mapper.StorageMapper;
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 StorageTccAction tcc;

    //@GlobalTransactional  //此时可以不用添加
    @Override
    public void decrease(Long productId, Integer count) {
        tcc.prepare(null,productId,count);
    }
}
第六步:order工程服务层实现类打开注释掉的远程调用库存的方法--OrderServiceimpl
第七步:启动storage 测试
按顺序启动服务:Eureka-->Seata Server-->Easy Id Generator-->Storage-->Order
查看localhost:8761  eureka服务,查看服务是否注册成功


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

  网页返回创建订单成功,库存没有时返回500

  • 观察 storage 的控制台日志:

a

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

a

  • 反复的提交订单,库存同时减少,知道库存不足可看到下面效果:

  •  注意:

2.3 账户Account 工程

第一步: 添加seata依赖,此处因为order-parent父工程添加了,此处可以不再添加

第二步:三个配置,可以从上面两个工程中复制粘贴

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

第三步:mapper层接口添加调用方法

package cn.tedu.account.mapper;

import cn.tedu.account.entity.Account;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import java.math.BigDecimal;

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

    Account selectByUserId(Long userId);
    void updateResidueToFrozen(Long userId,BigDecimal money);//可用-->冻结
    void updateFrozenToUsed(Long userId,BigDecimal money);//冻结-->已使用
    void updateFrozenToResidue(Long userId,BigDecimal money);//冻结-->可用
}

第四步: resources包下mapper包的accountmapper.xml编辑SQL语句

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.tedu.account.mapper.AccountMapper" >
    <resultMap id="BaseResultMap" type="Account" >
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="user_id" property="userId" jdbcType="BIGINT" />
        <result column="total" property="total" jdbcType="DECIMAL" />
        <result column="used" property="used" jdbcType="DECIMAL" />
        <result column="residue" property="residue" jdbcType="DECIMAL"/>
        <result column="frozen" property="frozen" jdbcType="DECIMAL"/>
    </resultMap>
    <update id="decrease">
        UPDATE account
        SET residue = residue - #{money},used = used + #{money}
        where user_id = #{userId};
    </update>

    <select id="selectByUserId" resultMap="BaseResultMap">
        select * from account where user_id = #{userId};
    </select>

    <update id="updateResidueToFrozen">
        update account
        set Residue=Residue-#{money}, Frozen=Frozen+#{money}
        where user_id = #{userId};
    </update>
    <update id="updateFrozenToUsed">
        update account
        set Frozen=Frozen-#{money}, Used=Used+#{money}
        where user_id = #{userId};
    </update>
    <update id="updateFrozenToResidue">
        update account
        set Frozen=Frozen-#{money}, Residue=Residue+#{money}
        where user_id = #{userId};
    </update>

</mapper>

第五步: 创建tcc包,添加标识工具类ResultHolder

package cn.tedu.account.tcc;

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

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

    public static void setResult(Class<?> actionClass, String xid, String v) {
        Map<String, String> 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<String, String> results = map.get(actionClass);
        if (results != null) {
            return results.get(xid);
        }

        return null;
    }

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


第六步: 按照 seata tcc 的规则,定义 TccAcction 接口和实现类

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")
    boolean prepare(BusinessActionContext ctx ,
                    @BusinessActionContextParameter(paramName = "userId") Long userId,
                    @BusinessActionContextParameter(paramName = "money")BigDecimal money);
    boolean commit(BusinessActionContext ctx);
    boolean rollback(BusinessActionContext ctx);
}
  • 实现类:
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
@Component
public class AccountTccActionImpl implements AccountTccAction {
    @Autowired
    private AccountMapper accountMapper;

    /*一阶段*/
    @Transactional
    @Override
    public boolean prepare(BusinessActionContext ctx, Long userId, BigDecimal money) {
        Account account = accountMapper.selectByUserId(userId);
        //判断可用金额
        if (account.getResidue().compareTo(money) < 0) {
            throw new RuntimeException("可用金额已不足");
        }
        accountMapper.updateResidueToFrozen(userId,money);
        //标记
        ResultHolder.setResult(AccountTccAction.class,ctx.getXid(),"p");
        return true;
    }

    /*二阶段*/
    @Transactional
    @Override
    public synchronized boolean commit(BusinessActionContext ctx) {
        if (ResultHolder.getResult(AccountTccAction.class,ctx.getXid()) == null){
            return true;
        }
        Long userId = Long.valueOf(ctx.getActionContext("userId").toString());
        BigDecimal money = new BigDecimal(ctx.getActionContext("money").toString());
        accountMapper.updateFrozenToUsed(userId,money);
        ResultHolder.removeResult(AccountTccAction.class,ctx.getXid());
        return true;
    }

    /*二阶段*/
    @Transactional
    @Override
    public synchronized boolean rollback(BusinessActionContext ctx) {
        if (ResultHolder.getResult(AccountTccAction.class,ctx.getXid()) == null){
            return true;
        }
        Long userId = Long.valueOf(ctx.getActionContext("userId").toString());
        BigDecimal money = new BigDecimal(ctx.getActionContext("money").toString());
        accountMapper.updateFrozenToResidue(userId,money);
        ResultHolder.removeResult(AccountTccAction.class,ctx.getXid());
        return true;
    }
}

第七步:  账户接口实现类调用 Try 阶段方法,业务代码中调用 TCC 第一阶段方法prepareDecreaseStorage(),并添加全局事务注解 @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 AccountTccAction tcc;

    @Override
    public void decrease(Long userId, BigDecimal money) {

        tcc.prepare(null,userId,money);
    }
}

第八步:放开orderServiceImpl实现类的账户远程调用的方法

 第九步:启动 account 进行测试

  • 按顺序启动服务:

Eureka-->Seata Server-->Easy Id Generator-->Storage-->Account-->Order

  • 调用保存订单,地址:

http://localhost:8083/create?userId=1&productId=1&count=10&money=100

  • 观察 account 的控制台日志:

    a

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

a

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值