SpringCloud事务管理TCC模式案例三

SpringCloud分布式事务TCC模式案例三

1新建项目

新建seata-tcc工程,新建Empty-Project工程

image-20210703142020369

image-20210703142101028

image-20210703142131852

关闭idea再打开就可以看到项目

image-20210703142236995

2导入无事务项目

选择各个项目的pom文件,选择add as Maven Project,即可作为maven项目

image-20210703142624023

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

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

a

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

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

  • a

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

a

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

a

3order-parent 添加 seata 依赖

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

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.tedu</groupId>
    <artifactId>order-parent</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>order-parent</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <mybatis-plus.version>3.3.2</mybatis-plus.version>
        <druid-spring-boot-starter.version>1.1.23</druid-spring-boot-starter.version>
        <seata.version>1.3.0</seata.version>
        <spring-cloud-alibaba-seata.version>2.0.0.RELEASE</spring-cloud-alibaba-seata.version>
        <spring-cloud.version>Hoxton.SR6</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid-spring-boot-starter.version}</version>
        </dependency>

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

4order项目配置

在order项目的application.yml配置全局事务组的组名

cloud:
    alibaba:
      seata:
        tx-service-group: order_tx_group

image-20210703143638376

在order模块添加连接seat的配置文件registry.conf和file.conf

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

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

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

image-20210703144950779

orderMapper添加更新订单状态,删除订单

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

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

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

package cn.tedu.order.mapper;

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

public interface OrderMapper extends BaseMapper<Order> {

    //冻结的订单,status=0
    void create(Order order);

    //订单从冻结状态改为正常状态,status=1
    void updateStatus(Long orderId,int status);

    //使用继承的 deleteById() 方法删除订单
}

在resources中的mapper中添加OrderMapper.xml文件

<?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.order.mapper.OrderMapper" >
    <resultMap id="BaseResultMap" type="cn.tedu.order.entity.Order" >
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="user_id" property="userId" jdbcType="BIGINT" />
        <result column="product_id" property="productId" jdbcType="BIGINT" />
        <result column="count" property="count" jdbcType="INTEGER" />
        <result column="money" property="money" jdbcType="DECIMAL" />
        <result column="status" property="status" jdbcType="INTEGER" />
    </resultMap>

    <insert id="create">
        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>

</mapper>

5Seata 实现订单的 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<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 操作需要定义一个接口,我们在接口中添加以下方法:

  • 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 {
    //try
    //第二阶段方法名,如果不是默认名,需要添加属性配置
    /**
     * name="orderTccAction",
     *             commitMethod = "commit",
     *             rollbackMethod = "rollback"
     * @return
     */
    //为了避开seata的一个bug,这里不传递封装的order对象
    //而是一个一个的传递订单参数
    @TwoPhaseBusinessAction(name="orderTccAction")
    boolean prepare(BusinessActionContext ctx,
                    @BusinessActionContextParameter(paramName = "orderId") Long orderId,
                    Long userId,
                    Long productId,
                    Integer count,
                    BigDecimal money);

    //confirm
    boolean commit(BusinessActionContext ctx);

    //Cancel
    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) {
        //插入状态是0的订单,表示是一个冻结的订单
        orderMapper.create(new Order(orderId,userId,productId,count,money,0));

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

        return true;
    }

    @Transactional
    @Override
    public boolean commit(BusinessActionContext ctx) {
        //如果没有标记,二阶段不执行
        //--第一阶段没有成功
        //--第二阶段已经执行过,标记已删除,不再重复
        if(ResultHolder.getResult(OrderTccAction.class,ctx.getXid())==null){
            return true;
        }

        //从上下问对象取出orderid
        Long orderId =Long.valueOf(ctx.getActionContext("orderId").toString());

        //订单的状态为1,表示正常
        orderMapper.updateStatus(orderId,1);

        //第二阶段成功,删除标记
        ResultHolder.removeResult(OrderTccAction.class,ctx.getXid());

        return true;
    }

    @Transactional
    @Override
    public boolean rollback(BusinessActionContext ctx) {
        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;
    }
}

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

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

package cn.tedu.order.servie;

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 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
    private EasyIdClient easyIdClient;
    @Autowired
    private StorageClient storageClient;
    @Autowired
    private AccountClient accountClient;

    @Autowired
    private OrderTccAction orderTccAction;

    @Override
    public void create(Order order) {
        //TODO: 远程调用发号器 生成订单id 生成订单id
        String s = easyIdClient.nextId("order_business");
        Long orderId = Long.valueOf(s);

        order.setId(orderId);

        //不在直击执行业务操作存储订单,
        //而实改成调用TCC第一阶段方法,冻结订单,预留订单
        //orderMapper.create(order);
        /**
         orderTccAction 是一个动态代理对象,用AOP添加了通知代码,
         在通知代码中,创建了上下文对象,并传递到被代理的目标方法中
         */
        orderTccAction.prepare(
                null,
                order.getId(),
                order.getUserId(),
                order.getProductId(),
                order.getCount(),
                order.getMoney());

        //TODO: 调用库存减少商品库存
        //storageClient.decrease(order.getProductId(),order.getCount());
        //ToDO: 远程调用账户扣减账户金额
        //accountClient.decrease(order.getUserId(),order.getMoney());
    }
}

7启动测试order

按顺序启动服务:

Eureka

Seata Server

Easy Id Generator

Order

调用保存订单,地址:

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

image-20210703151950433

观察控制台日志:

image-20210703152651034

image-20210703152746549

8异常测试

在业务类处理结束处抛出异常

throw new Exception("测试异常");

再次访问

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

image-20210703153239494

image-20210703153349618

image-20210703153417485

9添加storage模块

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

a

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

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

a

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

a

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

a

10storage项目配置

在application.yml上添加事务组配置

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

image-20210703154635570

将order模块中的resources下的file.conf和registry.conf文件复制到storage模块下resources文件夹下

image-20210703154912209

11StorageMapper 添加冻结库存相关方法

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

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

在 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);

    //查询库存,用来判断是否有足够库存
    //使用继承的 selectById()方法

    //可用-->冻结
    void updateResidueToFrozen(Long productId,Integer count);

    //冻结-->已售出
    void updateFrozenToUsed(Long productId,Integer count);

    //冻结-->可用
    void updateFrozenToResidue(Long productId,Integer count);
}

在resources的mapper文件夹下StorageMapper.xml添加实现方法

<?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="selectById" 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 residue = residue + #{count},
            frozen = frozen - #{count}
        WHERE product_id = #{productId}
    </update>
</mapper>

Seata 实现库存的 TCC 操作方法

工具类 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);
        }
    }
}

添加 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")
    boolean prepare(BusinessActionContext ctx,
                    @BusinessActionContextParameter(paramName = "productId") Long productId,
                    @BusinessActionContextParameter(paramName = "count") Integer count);

    boolean commit(BusinessActionContext ctx);

    boolean rollback(BusinessActionContext ctx);
}

实现类:

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.selectById(productId);
        if(storage.getResidue()<count){
            //当第一阶段操作失败时,二阶段回滚操作不能主席那个
            throw new RuntimeException("库存不足");
        }

        //可用-->冻结
        storageMapper.updateResidueToFrozen(productId,count);

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

    @Transactional
    @Override
    public boolean commit(BusinessActionContext ctx) {
        Long productId = Long.valueOf(
                ctx.getActionContext("productId").toString());

        Integer count = Integer.valueOf(
                ctx.getActionContext("count").toString());

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

        // 冻结 --> 已使用
        storageMapper.updateFrozenToUsed(productId, count);

        //删除标识
        ResultHolder.removeResult(getClass(), ctx.getXid());

        return true;
    }

    @Transactional
    @Override
    public boolean rollback(BusinessActionContext ctx) {
        Long productId = Long.valueOf(
                ctx.getActionContext("productId").toString());

        Integer count = Integer.valueOf(
                ctx.getActionContext("count").toString());

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

        // 冻结 --> 可用
        storageMapper.updateFrozenToResidue(productId, count);

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

12在业务代码中调用 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 StorageMapper storageMapper;

    @Autowired
    private StorageTccAction storageTccAction;

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

13启动 storage 进行测试

按顺序启动服务:

Eureka

Seata Server

Easy Id Generator

Storage

Order

调用保存订单,地址:

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

image-20210703161812321

观察 storage 的控制台日志:

image-20210703161919941

Order日志

image-20210703162021701

image-20210703162055459

image-20210703162137014

14异常测试

在order订单实现类,在调用库存减少后抛出异常

throw new RuntimeException("模拟异常");

image-20210703162527548

重启order服务

再次访问

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

image-20210703162624289

image-20210703163047517

image-20210703163150854

image-20210703162704402

image-20210703162734186

说明storage在系统发生异常时会回滚

15添加account模块

配置

在account模块的application.yml添加事务组

cloud:
    alibaba:
      seata:
        tx-service-group: order_tx_group

image-20210703163838141

将order模块中的resources下的file.conf和registry.conf文件复制到account模块下resources文件夹下

image-20210703163953514

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<Account> {

    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:

<?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="cn.tedu.account.entity.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="selectById" resultMap="BaseResultMap">
        SELECT * FROM account WHERE `user_id`=#{userId}
    </select>

    <update id="updateFrozen">
        UPDATE account SET `residue`=#{residue},`frozen`=#{frozen} 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>

16Seata 实现库存的 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);
        }
    }

}

添加 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;
    }
}

17在业务代码中调用 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 AccoutServiceImpl implements AccoutService{

    @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);
    }
}

18启动account进行测试

在order服务,开启调用account服务

accountClient.decrease(order.getUserId(),order.getMoney());

image-20210703170419061

按顺序启动服务:

  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

image-20210703170659527

image-20210703172618073

image-20210703172718050

image-20210703172807504

image-20210703170748928

image-20210703170822544

image-20210703170846416

19异常测试

在order调用库存和账户后抛出异常

throw new RuntimeException("模拟异常");

image-20210703171311092

重启order服务

再次访问

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

image-20210703171417033

image-20210703171813096

image-20210703171921579

image-20210703172037226

image-20210703172104856

image-20210703172132159

image-20210703172205055

end

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
该项目是采用目前比较流行的SpringBoot/SpringCloud构建微服务电商项目,项目叫 《果然新鲜》,实现一套串联的微服务电商项目。完全符合一线城市微服务电商的需求,对学习微服务电商架构,有非常大的帮助,该项目涵盖从微服务电商需求讨论、数据库设计、技术选型、互联网安全架构、整合SpringCloud各自组件、分布式基础设施等实现一套完整的微服务解决方案。 项目使用分布式微服务框架,涉及后台管理员服务、地址服务、物流服务、广告服务、商品服务、商品类别服务、品牌服务、订单服务 、购物车服务、首页频道服务、公告服务、留言服务、搜索服务、会员服务等。  系统架构图   SpringBoot+SpringCloud+SSM构建微服务电商项目使用SpringCloud Eureka作为注册中心,实现服务治理使用Zuul网关框架管理服务请求入口使用Ribbon实现本地负载均衡器和Feign HTTP客户端调用工具使用Hystrix服务保护框架(服务降级、隔离、熔断、限流)使用消息总线Stream RabbitMQ和 Kafka微服务API接口安全控制和单点登录系统CAS+JWT+OAuth2.0分布式基础设施构建分布式任务调度平台XXL-JOB分布式日志采集系统ELK分布式事务解决方案LCN分布式锁解决方案Zookeeper、Redis分布式配置中心(携程Apollo)高并发分布式全局ID生成(雪花算法)分布式Session框架Spring-Session分布式服务追踪与调用链Zipkin项目运营与部署环境分布式设施环境,统一采用Docker安装使用jenkins+docker+k8s实现自动部署微服务API管理ApiSwagger使用GitLab代码管理(GitHub  GitEE)统一采用第方云数据库使用七牛云服务器对静态资源实现加速 开发环境要求JDK统一要求:JDK1.8Maven统一管理依赖 统一采用Docker环境部署编码统一采用UTF-8开发工具IDEA 或者 Eclipse 
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。
TCC-Transaction是一个开源的TCC补偿性分布式事务框架。TCC是Try、Confirm、Cancel的缩写,表示事务的尝试、确认和取消阶段。TCC能够对分布式事务中的各个资源进行分别锁定、提交和释放。它的优点是能够实现严格一致性并且具有较短的执行时间和高实时性要求。同时,TCC也具有一定的缺点,例如对应用的侵入性较强,需要实现每个分支的try、confirm和cancel操作,实现难度较大。 关于Spring Cloud分布式事务和TCC,可以使用TCC-Transaction框架来实现。TCC-Transaction可以作为可靠性事件投递的替代品,并作为Spring Cloud Stream或Spring Cloud Bus的基础组件。此外,TCC还需要在事务管理器(协调器)节点上以高可用集群方式部署,并使用多数派算法来避免集群发生脑裂问题。 在实际应用中,TCC适用于一些需要严格一致性、执行时间短和实时性要求高的场景,例如红包和收付款业务。 更多关于TCC-Transaction框架的详细信息可以在其GitHub地址(https://github.com/changmingxie/tcc-transaction)和项目指南地址(https://github.com/changmingxie/tcc-transaction/wiki/使用指南1.2.x)中找到。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [SpringCloud(6) 分布式事务【概念、常见框架选择 - tx-lcn】](https://blog.csdn.net/qq_38225558/article/details/86103229)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *2* [Spring Cloud综合实战 - 基于TCC补偿模式的分布式事务](https://blog.csdn.net/Solarison/article/details/68061157)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值