【3万字详解】项目结构完成了,如何给项目添加分布式事务【Seata AT 模式】与【Seata TCC 模式】,幂等性?

目录

准备工作

解压文件

修改文件registry.conf

修改文件file.conf

修改bin下面的seata-server.bat,更改内存占用(85行)

修改项目-Seata AT 模式

添加seata依赖

添加配置文件

yml文件添加配置

修改order订单服务

创建数据源自动配置类

远程调用服务添加事务

修改account账户服务

修改storage库存服务

测试

修改项目-Seata TCC 模式

TCC 模式介绍

依赖与配置文件的添加

修改Order订单服务

修改Mapper

定义接口与实现类

更改业务代码 OrderServiceImpl

修改storage服务

修改mapper层代码

定义接口与实现类

更改业务代码 StorageServiceImpl

修改account服务

修改mapper层代码

定义接口与实现类

更改业务代码 AccountServiceImpl

测试


准备工作

解压文件

先准备好压缩文件,然后进行解压。

修改文件registry.conf

右键打开registry.conf文件,type参数改为eureka,并将名字改为seata-server。

 

修改文件file.conf

右键打开file.conf文件。

模式mode改为db,包改为cj包,添加时区serverTimezone,用户名密码改为root。

修改bin下面的seata-server.bat,更改内存占用(85行)

找到第85行,修改参数配置。 

有的启动参数不支持,可以根据报错信息删除文件里面对应的参数

双击启动,在eureka注册中心查看服务

修改项目-Seata AT 模式

整体项目的基本流程是order订单服务需要远程调用account账户服务和storage库存服务,以完成创建订单之后,要修改账户的余额和商品库存等信息。

添加seata依赖

在order-parent 父项目下面添加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>

添加配置文件

需向每个服务添加两个配置文件,分别是registry.conf和file.conf,文件的内容分别如下:

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

yml文件添加配置

原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
#    jdbcUrl: ${spring.datasource.url}
#  cloud:
#    alibaba:
#      seata:
#        # 三个模块 order,storage,account 都要在这同一个事务组中
#        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/*Mapper.xml
  configuration:
    map-underscore-to-camel-case: true

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

现yml配置文件内容就是将上面注释的部分放开。

注意下面三个服务都需要添加两个事务的配置文件,并且每个服务对应的yml文件也需要添加上面注释的部分。

可以看一下关键项目结构如下:

修改order订单服务

创建数据源自动配置类

注意spring默认存在数据源自动配置类,在这里我们使用自定义的,所以需要排除掉spring默认的,需要在启动类的注解上面进行相关配置,如下所示,其它服务也是一样的。

启动类修改:

package cn.tedu.order;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients
@MapperScan("cn.tedu.order.mapper")
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//排除掉spring默认的数据源自动配置类
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

}

自定义数据源自动配置类:

package cn.tedu.order;

import com.alibaba.druid.pool.DruidDataSource;
import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

/**
 * @Author 作者:小龙猿
 * @Project 项目:seata-at
 * @Time 时间:2021/9/23 9:43
 */
/*
自定义的数据源自动配置类,与spring中默认的数据源自动类配置类冲突,
需要在启动类中排除spring默认的自动配置
 */
@Configuration
public class DSAutoConfiguration {
    // 创建原始数据源对象
    // hikari使用的数据库地址参数不是URL,而是jdbcUrl,所以需在yml文件中添加此配置
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource dataSource(){
        return new HikariDataSource();
        //return new DruidDataSource();
    }
    // 创建数据源代理对象
    @Primary // 首选对象,即当使用autowired注解注入DataSource对象时,如果不能确定注入哪一个,则首选注入这个
    @Bean
    public DataSource dataSourceProxy(DataSource ds){
        return new DataSourceProxy(ds);
    }
}

远程调用服务添加事务

因为在order订单服务里调用了account服务和storage服务,需在order订单服务开启全局事务,注意全局事务只需在此服务添加即可,并且每个服务还需要添加控制本地事务注解。

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

import java.util.Random;

/**
 * @Author 作者:小龙猿
 * @Project 项目:seata-at
 * @Time 时间:2021/9/22 11:47
 */
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    EasyIdClient easyIdClient;
    @Autowired
    private AccountClient accountClient;
    @Autowired
    private StorageClient storageClient;

    @GlobalTransactional //开启全局事务,只在第一个模块添加
    @Transactional //控制本地事务
    @Override
    public void create(Order order) {
        //远程调用发号器,获取订单id
        Long orderId = Long.valueOf(easyIdClient.nextId("order_business"));
        //Long orderId = new Random().nextLong();
        order.setId(orderId);
        orderMapper.create(order);
        //远程调用库存,减少商品库存
        storageClient.decrease(order.getProductId(), order.getCount());
        //远程调用账户,扣减账户金额
        accountClient.decrease(order.getUserId(), order.getMoney());
    }
}

修改account账户服务

account账户服务的修改操作与order服务类似,区别在于账户服务只需添加控制本地事务注解。

修改storage库存服务

storage账户服务的修改操作与order服务类似,区别在于账户服务只需添加控制本地事务注解。

项目的完整代码已上传至码云,需要的话可以仔细查看。 ferryzl/seata-samplesnewhttps://gitee.com/ferryzl/seata-samplesnew

测试

依次启动eureka、seata、DbInit、EasyIdGenerator、account、storage、order服务。

查看eureka注册中心:

访问网址 http://localhost:8083/create?userId=1&productId=1&count=10&money=400

查看控制台:

说明事务添加成功了。

再刷新两下网址:

后端控制台发现报错,因为我们给账户的初始余额是1000元,每次刷新会扣减400,所以刷新3次之后余额按理会变为-200元,但我们设置了 如果扣减金额大于当前余额的话就会抛出异常。

package cn.tedu.account.service;
import cn.tedu.account.entity.Account;
import cn.tedu.account.mapper.AccountMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

/**
 * @Author 作者:小龙猿
 * @Project 项目:seata-at
 * @Time 时间:2021/9/22 10:22
 */
@Service
public class AccountServiceImpl implements AccountService{
    @Autowired
    private AccountMapper accountMapper;
    @Transactional // 控制本地事务
    @Override
    public void decrease(Long userId, BigDecimal money){
        Account a = accountMapper.selectByUserId(userId);
        /*
        BigDecimal a  BigDecimal b
        a.compareTo(b)
            返回值
                正数,表示a大
                负数,表示a小
                0,表示相同
         */
        if (a.getResidue().compareTo(money)<0){
            throw new RuntimeException("可用金额不足");
        }
        accountMapper.decrease(userId, money);
    }
}

设置了事务之后,数据库不会继续扣减金额,即事务发生了回滚,所以当前余额还是200元(两次访问网址后的金额)。

 

修改项目-Seata TCC 模式

上面已经完成了seata at 模式的分布式事务处理,下面介绍一下seata tcc 模式的使用步骤。

seata at 模式使用的是自定义的数据源自动配置类,seata tcc 使用的就是spring默认的数据源配置类。

先还原项目(未添加seata at模式事务的阶段)。

TCC 模式介绍

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

a

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

a

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

a

依赖与配置文件的添加

操作与seata at模式一样,都是添加相同的依赖于配置文件,只是application.yml里面不再需要jdbcUrl参数配置了,因为我们就是使用的是spring默认的数据源配置类。

修改Order订单服务

修改Mapper

第一阶段需要插入冻结数据(并不是直接插入订单数据),成功时执行第二阶段的修改状态方法,失败时执行第二阶段的删除冻结数据的方法

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继承 //失败时执行删除操作
}

对应mapper.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},1);
    </insert>

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

定义接口与实现类

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;
/**
 * @Author 作者:小龙猿
 * @Project 项目:seata-tcc
 * @Time 时间:2021/9/23 15:20
 */
/*
按照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;

/**
 * @Author 作者:小龙猿
 * @Project 项目:seata-tcc
 * @Time 时间:2021/9/23 15:49
 */
@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));//插入第一阶段冻结数据
        return true;
    }

    @Transactional
    @Override
    public boolean commit(BusinessActionContext ctx) {
        //从上下文对象获取 orderId 订单id
        Long orderId = Long.valueOf(ctx.getActionContext("orderId").toString());
        orderMapper.updateStatus(orderId, 1);//提交的时候,修改状态为1
        return true;
    }

    @Transactional
    @Override
    public boolean rollback(BusinessActionContext ctx) {
        Long orderId = Long.valueOf(ctx.getActionContext("orderId").toString());
        orderMapper.deleteById(orderId);//如果出现错误,直接删除冻结数据
        return true;
    }
}

更改业务代码 OrderServiceImpl

业务代码中不再直接保存订单数据,而是调用 TCC 第一阶段方法prepareCreateOrder(),并添加全局事务注解 @GlobalTransactional,只需手动调用第一阶段方法,不用自己调用第二阶段的两个方法,会由RM资源管理器自动调用:

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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Random;

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    //private OrderMapper orderMapper;
    private OrderTccAction tcc;
    @Autowired
    private EasyIdClient easyIdClient;
    @Autowired
    private StorageClient storageClient;
    @Autowired
    private AccountClient accountClient;

    @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添加了前置通知,
            在前置代码中会创建上下文对象
         */
        //调用tcc第一阶段方法,不直接正常的创建订单,而是调用TccAction第一阶段方法来冻结订单
        tcc.prepare(null,
                order.getId(),
                order.getUserId(),
                order.getProductId(),
                order.getCount(),
                order.getMoney());//只需手动调用第一阶段方法,不用自己调用第二阶段的两个方法,会由RM资源管理器自动调用

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

修改storage服务

这里的依赖添加与配置文件添加操作与seata at 模式一样。只是application.yml里面不再需要jdbcUrl参数配置了,因为我们就是使用的是spring默认的数据源配置类。

修改mapper层代码

同order服务一样,都不是直接向数据库提交数据。

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);//冻结转可用(失败)

}

对应mapper.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="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 Used = Used + #{count},Frozen = Frozen - #{count}
        where product_id = #{productId};
    </update>
    <update id="updateFrozenToResidue">
         update storage
        set Residue = Residue + #{count},Frozen = Frozen - #{count}
        where product_id = #{productId};
    </update>


</mapper>

定义接口与实现类

逻辑与order服务一样。

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;
/**
 * @Author 作者:小龙猿
 * @Project 项目:seata-tcc
 * @Time 时间:2021/9/23 17:19
 */
@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;

/**
 * @Author 作者:小龙猿
 * @Project 项目:seata-tcc
 * @Time 时间:2021/9/23 17:26
 */
@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);//库存充足的话则冻结库存
        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());
        storageMapper.updateFrozenToUsed(productId, count);//成功,则更新已使用库存
        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());
        storageMapper.updateFrozenToResidue(productId, count);//失败,则恢复可用库存
        return true;
    }
}

更改业务代码 StorageServiceImpl

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;//不直接减库存了
    private StorageTccAction tcc;
    //方法上不需要加事务注解了(不用加本地控制事务和全局事务)
    @Override
    public void decrease(Long productId, Integer count) {
        //storageMapper.decrease(productId, count);
        tcc.prepare(null, productId, count);
    }
}

修改account服务

这里的依赖添加与配置文件添加操作与seata at 模式一样。只是application.yml里面不再需要jdbcUrl参数配置了,因为我们就是使用的是spring默认的数据源配置类。

修改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);

}

对应的mapper.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.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 frozen = frozen + #{money},residue = residue - #{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>

定义接口与实现类

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;
/**
 * @Author 作者:小龙猿
 * @Project 项目:seata-tcc
 * @Time 时间:2021/9/23 18:34
 */
@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;
/**
 * @Author 作者:小龙猿
 * @Project 项目:seata-tcc
 * @Time 时间:2021/9/23 18:37
 */
@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);
        return true;
    }

    @Transactional
    @Override
    public boolean commit(BusinessActionContext ctx) {
        Long userId = Long.valueOf(ctx.getActionContext("userId").toString());
        BigDecimal money = new BigDecimal(ctx.getActionContext("money").toString());
        accountMapper.updateFrozenToUsed(userId, money);
        return true;
    }

    @Transactional
    @Override
    public boolean rollback(BusinessActionContext ctx) {
        Long userId = Long.valueOf(ctx.getActionContext("userId").toString());
        BigDecimal money = new BigDecimal(ctx.getActionContext("money").toString());
        accountMapper.updateFrozenToResidue(userId, money);
        return true;
    }
}

更改业务代码 AccountServiceImpl

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;//不直接扣减金额
    private AccountTccAction tcc;
    @Override
    public void decrease(Long userId, BigDecimal money) {
        //accountMapper.decrease(userId, money);
        tcc.prepare(null, userId, money);
    }
}

测试

访问网址 http://localhost:8083/create?userId=1&productId=1&count=10&money=100 ,查看控制台内容:

添加幂等性

第二阶段为了处理幂等性问题(不进行重复操作)这里首先添加一个工具类 ResultHolder

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

package cn.tedu.order.tcc;
/**
 * @Author 作者:小龙猿
 * @Project 项目:seata-tcc
 * @Time 时间:2021/9/24 9:25
 */
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);
        }
    }
}

然后改进一下 OrderTccActionImpl 实现类:

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;

/**
 * @Author 作者:小龙猿
 * @Project 项目:seata-tcc
 * @Time 时间:2021/9/23 15:49
 */
@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(getClass(), ctx.getXid(), "p");
        return true;
    }

    @Transactional
    @Override
    public synchronized boolean commit(BusinessActionContext ctx) {
        //判断标记是否存在,不存在则直接返回(一阶段执行失败或者二阶段已经执行过了)
        if (ResultHolder.getResult(getClass(), ctx.getXid())==null){
            return true;
        }
        //从上下文对象获取 orderId 订单id
        Long orderId = Long.valueOf(ctx.getActionContext("orderId").toString());
        orderMapper.updateStatus(orderId, 1);//提交的时候,修改状态为1
        //提交成功时删除标记,为了防止阻塞带来重复删除,可以加一个synchronized锁
        ResultHolder.removeResult(getClass(), ctx.getXid());
        return true;
    }

    @Transactional
    @Override
    public synchronized boolean rollback(BusinessActionContext ctx) {
        //第一阶段没有完成的情况下,不必执行回滚
        //因为第一阶段有本地事务,事务失败时已经进行了回滚。
        //如果这里第一阶段成功,而其他全局事务参与者失败,这里会执行回滚
        //幂等性控制:如果重复执行回滚则直接返回
        if (ResultHolder.getResult(getClass(), ctx.getXid()) == null) {
            return true;
        }
        Long orderId = Long.valueOf(ctx.getActionContext("orderId").toString());
        orderMapper.deleteById(orderId);//如果出现错误,直接删除冻结数据
        //回滚结束时,删除标识
        ResultHolder.removeResult(getClass(), ctx.getXid());
        return true;
    }
}

注意:需要在另外两个服务里面都加上这个工具类,并在对应实现类进行相同操作。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值