目录
修改bin下面的seata-server.bat,更改内存占用(85行)
准备工作
解压文件
先准备好压缩文件,然后进行解压。
修改文件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 模式介绍
T
ry - 第一阶,冻结数据阶段,向订单表直接插入订单,订单状态设置为0(冻结状态)。
C
onfirm - 第二阶段,提交事务,将订单状态修改成1(正常状态)。
C
ancel - 第二阶段,回滚事务,删除订单。
依赖与配置文件的添加
操作与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;
}
}
注意:需要在另外两个服务里面都加上这个工具类,并在对应实现类进行相同操作。