赞
踩
目录
TCC 与 Seata AT 事务一样都是两阶段事务,它与 AT 事务的主要区别为:
第一步: 创建 empty project: seata-tcc ,独立的project工程,要与seata-at工程区分开
第二步: "seata-tcc/无事务版本.zip",解压后,只能一个个导入,project Structure-->modules-->import module-->挨个找到七个工程导入
或者通过右侧的Maven中的加号找到七个工程的pom文件导入
在 idea 中按两下 shift
键,搜索 add maven projects
,打开 maven 工具:
然后选择 seata-tcc
工程目录下的 7 个项目的 pom.xml
导入:
我们要添加以下 TCC 事务操作的代码:
第一步: 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>
第二步:三个配置文件
- 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 {
- # 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"
- }
- }
- 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继承
- }
- <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
。
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工程控制台日志:
查看数据库表中的订单数据:
我们要添加以下 TCC 事务操作的代码:
T
ry - 第一阶,冻结数据阶段,将要减少的库存量先冻结:C
onfirm - 第二阶段,提交事务,使用冻结的库存完成业务数据处理:C
ancel - 第二阶段,回滚事务,冻结的库存解冻,恢复以前的库存量:第一步:有三个文件需要配置:
- 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
-
这三个文件的设置与上面 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
第一步: 添加seata依赖,此处因为order-parent父工程添加了,此处可以不再添加
第二步:三个配置,可以从上面两个工程中复制粘贴
第三步: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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。