当前位置: 首页 > article >正文

SpringCloud整合seata,XA、AT、TCC、SAGA模式

参考资料:

SpringCloud-Alibaba搭建

SpringCloud-nacos整合

Seata部署

参考demo(及学习资料)

seata官网

参考视频​​​​​c(AT模式的UNDO_LOG讲的可能有点问题,但是很通俗易懂)

 参考视频2(不太通俗易懂)

沽泡付费视频(就是对着官网念) 

上述三个视频的参考资料


准备环境:

        该教程默认已经有如下环境,如果没有可以参考上述教程:

  • 部署了Nacos
  • 部署了Seata
  • 搭建了SpringCloud 
  • 并且版本已经做到了统一(只要保证JVM里面使用的jar包版本和部署的版本一致即可
  • 同一系列版本见Wiki

 


模拟事件-搭建demo:

        因为seata是分布式事务,所以这个例子需要多个微服务,多个库进行联动

创建数据库

        首先创建两个数据库

CREATE DATABASE /*!32312 IF NOT EXISTS*/`seata_db1` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */;USE `seata_db1`;/*Table structure for table `money` */DROP TABLE IF EXISTS `money`;CREATE TABLE `money` (`id` varchar(32) NOT NULL COMMENT '主键',`user_name` varchar(32) DEFAULT NULL COMMENT '名字',`bank_money` double DEFAULT NULL COMMENT '银行余额',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;/*Data for the table `money` */insert  into `money`(`id`,`user_name`,`bank_money`) values 
('084d862fa66c4c82886a4b0bb9214ab1','张三',100);

 

CREATE DATABASE /*!32312 IF NOT EXISTS*/`seata_db2` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */;USE `seata_db2`;/*Table structure for table `order_details` */DROP TABLE IF EXISTS `order_details`;CREATE TABLE `order_details` (`id` varchar(32) NOT NULL COMMENT '主键',`goods_name` varchar(32) DEFAULT NULL COMMENT '商品名',`count` int(11) DEFAULT NULL COMMENT '仓库余额',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;/*Data for the table `order_details` */insert  into `order_details`(`id`,`goods_name`,`count`) values 
('7072809e86834335843bc918c33074ec','desk',50);

 

 创建示例微服务

         用搭建好的SpringClud框架,默认已经整合了nacos,并且已经部署好了seata

        关于微服务的创建,这里就不再赘述了,可以参考该文章

          参考上述文章,创建两个微服务   seata-demo1seata-demo2,然后进行相关依赖的导入

        这里是将所有的依赖加入到了公共服务里面,请根据个人情况使用,在common-service服务中的pom.xml文件,

Lombok依赖

        <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version></dependency>

 控制层依赖

        <!-- springboot starter web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>

 mybatis-plus及数据库相关

        <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.1.tmp</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><!--引入mysql--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.11</version></dependency>

fegin组件

        <!--fegin组件--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>

 nacos相关(使用时默认的,如果nacos不是上述系列的则需要指定版本)

        <!--引入nacos client的依赖--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--引入nacos config 依赖--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency>

还有一些bootstrap.yml启动的辅助依赖

        <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-loadbalancer</artifactId></dependency>

最后添加的依赖如下:

然后将公共依赖引入创建的两个微服务,参考

        <dependency><groupId>org.example</groupId><artifactId>common-service</artifactId><version>1.0-SNAPSHOT</version></dependency>

将相关配置交给nacos管理,可以参考文章,这里只是贴代码,不赘述

  • seata-demo1
bootstrap.yml
spring:profiles:include:uat

 bootstrap-uat.yml

spring:config:use-legacy-processing: trueprofiles.active: uatapplication:name: seata-demo1cloud:nacos:config:server-addr: localhost:8848group: DEFAULT_GROUPusername: nacospassword: nacosfile-extension: yamlrefresh-enabled: true

然后将相关配置放入到nacos中

server:port: 8081
spring:nacos:discovery:server-addr: localhost:8848username: nacospassword: nacosdatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_db1?characterEncoding=UTF-8username: rootpassword: 123888

  •  seata-demo2
bootstrap.yml
spring:profiles:include:uat

bootstrap-uat.yml
spring:config:use-legacy-processing: trueprofiles.active: uatapplication:name: seata-demo2cloud:nacos:config:server-addr: localhost:8848group: DEFAULT_GROUPusername: nacospassword: nacosfile-extension: yamlrefresh-enabled: true

 然后将相关配置放入到nacos中

server:port: 8080
spring:nacos:discovery:server-addr: localhost:8848username: nacospassword: nacosdatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_db2?characterEncoding=UTF-8username: rootpassword: 123888

这样的话,就可以连接到数据库了,然后编写 mybatis-plus 的三层,参考文章,这里将不再赘述,仅仅是贴代码

  • seata-demo1
package org.example.entity;import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;@Data
@TableName("money")
public class Money {@TableIdprivate String id;@TableField("user_name")private String userName;@TableField("bank_money")private Double bankMoney;
}
package org.example.dao;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.example.entity.Money;public interface MoneyMapper extends  BaseMapper<Money> {
}
package org.example.service;import com.baomidou.mybatisplus.extension.service.IService;
import org.example.entity.Money;public interface MoneyService extends IService<Money> {}
package org.example.service;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.example.dao.MoneyMapper;
import org.example.entity.Money;
import org.springframework.transaction.annotation.Transactional;@Service
public class MoneyServiceImpl extends ServiceImpl<MoneyMapper, Money> implements MoneyService{}

  • seata-demo2
@Data
@TableName("order_details")
public class OrderDetails {@TableIdprivate String id;@TableField("goods_name")private String goodsName;@TableField("count")private int count;
}
package org.example.dao;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.example.entity.OrderDetails;public interface OrderDetailsMapper extends BaseMapper<OrderDetails> {
}
package org.example.service;import com.baomidou.mybatisplus.extension.service.IService;
import org.example.entity.OrderDetails;public interface OrderDetailsService extends IService<OrderDetails> {}

 然后编写业务代码,分布式事务的调用,模拟扣款和扣库存

  • seata-demo1

feign调用

package org.example.client;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;@FeignClient(value = "seata-demo2-service")
public interface Demo2Client {@RequestMapping("order/addCount")ResponseEntity<String> addCount(@RequestParam("count") int count);
}
MoneyService
package org.example.service;import com.baomidou.mybatisplus.extension.service.IService;
import org.example.entity.Money;public interface MoneyService extends IService<Money> {void changeMoney(Money money,Double num);
}
MoneyServiceImpl
package org.example.service;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.example.dao.MoneyMapper;
import org.example.entity.Money;@Service
public class MoneyServiceImpl extends ServiceImpl<MoneyMapper, Money> implements MoneyService{public void changeMoney(Money money, Double num) {Double bankMoney = money.getBankMoney();if (bankMoney >= num) {money.setBankMoney(bankMoney - num);this.updateById(money);}else{throw new RuntimeException("余额不足");}}
}
TransService
package org.example.service;public interface TransService {void globalRollBackDemo(int orderCount);
}
TransServiceImpl
package org.example.service;import lombok.extern.slf4j.Slf4j;
import org.example.client.Demo2Client;
import org.example.entity.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;@Service
@Slf4j
public class TransServiceImpl implements TransService {@Autowiredprivate MoneyService moneyService;@Autowiredprivate Demo2Client demo2Client;/*** 分布式事务   回滚例子*/public void globalRollBackDemo(int orderCount) {log.info("=================开始扣钱========================");List<Money> list = moneyService.list();Money money = list.get(0);Double bankMoney = money.getBankMoney();log.info("=================查询到银行余额为:"+bankMoney+",开始扣款:20=================");moneyService.changeMoney(money,20D);log.info("=================扣款成功,开始扣库存,扣减库存为:"+orderCount+"========================");demo2Client.addCount(orderCount);}
}
MoneyController
package org.example.controller;import org.example.client.Demo2Client;
import org.example.service.TransService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;@Controller
@RequestMapping("money")
public class MoneyController {@Autowiredprivate TransService transService;@RequestMapping("test/globalTransactional/fail")public ResponseEntity<String> testGTRollBack() {transService.globalRollBackDemo(60);return new ResponseEntity<String>("sucess",HttpStatus.OK);}@RequestMapping("test/globalTransactional/sucess")public ResponseEntity<String> testGTSucess() {transService.globalRollBackDemo(40);return new ResponseEntity<String>("sucess",HttpStatus.OK);}}

SeataDemo1Application
package org.example;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("org.example.dao")
@EnableFeignClients
public class SeataDemo1Application
{public static void main( String[] args ){SpringApplication.run(SeataDemo1Application.class,args);}
}

  • seata-demo2
OrderDetailsController
package org.example.controller;import org.example.entity.OrderDetails;
import org.example.service.OrderDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;@Controller
@RequestMapping("order")
public class OrderDetailsController {@Autowiredprivate OrderDetailsService orderService;@RequestMapping("addCount")public ResponseEntity<String> addCount(@RequestParam("count") Integer count) {orderService.updateOrderDetails(count);return new ResponseEntity<String>("success",HttpStatus.OK);}}
OrderDetailsService
package org.example.service;import com.baomidou.mybatisplus.extension.service.IService;
import org.example.entity.OrderDetails;public interface OrderDetailsService extends IService<OrderDetails> {//  修改库存剩余void updateOrderDetails(Integer count);}
OrderDetailsServiceImpl
package org.example.service;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.example.dao.OrderDetailsMapper;
import org.example.entity.OrderDetails;
import org.springframework.stereotype.Service;import java.util.List;@Service
@Slf4j
public class OrderDetailsServiceImpl extends ServiceImpl<OrderDetailsMapper, OrderDetails> implements OrderDetailsService {@Overridepublic void updateOrderDetails(Integer count) {List<OrderDetails> list = this.list();// 取第一个进行OrderDetails orderDetails = list.get(0);if (orderDetails.getCount()>=count) {int sum = orderDetails.getCount() - count;orderDetails.setCount(sum);this.updateById(orderDetails);log.info("库存扣减成功");}else{throw new RuntimeException("库存为:"+list.get(0).getCount()+"不足,开始回滚");}}
}
SeataDemo2Application
package org.example;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("org.example.dao")
@EnableFeignClients
public class SeataDemo2Application
{public static void main( String[] args ){SpringApplication.run(SeataDemo2Application.class,args);}
}


XA模式:

导入依赖

  •  可以在公共的服务,或者私有的服务中导入seata的jar包
  • jar包的版本如果和Spring-Cloud是同一个系列的(见版本选择),可以不指定,如果不是一个系列的,则需要指定版本(和部署的版本一致
  • 本教程是将jar包放入到公共服务中,并且指定版本号
<!-- 注意一定要引入对版本,要引入spring-cloud版本seata,而不是springboot版本的seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><!-- 排除掉springcloud默认的seata版本,以免版本不一致出现问题--><exclusions><exclusion><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId></exclusion><exclusion><groupId>io.seata</groupId><artifactId>seata-all</artifactId></exclusion></exclusions></dependency><dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId><version>1.7.0</version></dependency>

添加seata的相关配置

 在nacos管理的配置中,找到seata-demo1和seata-demo2的配置,添加如下配置:

#seata客户端配置
seata:enabled: trueapplication-id: seata_txtx-service-group: seata_tx_groupservice:vgroup-mapping:seata_tx_group: defaultregistry:type: nacosnacos:application: seata-serverserver-addr: 127.0.0.1:8848namespace:group: SEATA_GROUPdata-source-proxy-mode: XA

开启分布式事务---XA模式

  • 在需要开启分布式事务的地方使用   @GlobalTransactional 修饰即可
  • 本地事务,建议加上  @Transactional
  • 例如我们要对,上面的扣钱和扣库存业务,添加分布式事务,只需要在seata-demo1的
    TransServiceImpl添加@GlobalTransactional

  • 然后在本地事务使用@Transactional修饰
  • 如:seata-demo1中的 MoneyServiceImpl 以及seata-demo2中的 OrderDetailsServiceImpl

这样,分布式事务XA模式就实现了 


AT模式:

        AT模式和XA模式在实现上基本没有区别,只需要将YML文件中的XA改为AT即可


TCC模式:

        关于TCC模式的原理,业务悬挂,空回滚等原理,以及TCC专用的注解这些就不说了,详情可以看上面的视频,这里仅仅是讲怎么实现。

        基于上面搭出的框架,按照下面的步骤来编写TCC模式

        TCC模式是在AT模式上进行改造的,所以YML的写法和AT模式一样

修改YML配置

seata-demo1
server:port: 8081
spring:nacos:discovery:server-addr: localhost:8848username: nacospassword: nacosdatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_db1?characterEncoding=UTF-8username: rootpassword: 123888
#seata客户端配置
seata:enabled: trueapplication-id: seata_txtx-service-group: seata_tx_groupservice:vgroup-mapping:seata_tx_group: defaultregistry:type: nacosnacos:application: seata-serverserver-addr: 127.0.0.1:8848namespace:group: SEATA_GROUPdata-source-proxy-mode: AT

 seata-demo2
server:port: 8080
spring:nacos:discovery:server-addr: localhost:8848username: nacospassword: nacosdatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_db2?characterEncoding=UTF-8username: rootpassword: 123888
#seata客户端配置
seata:enabled: trueapplication-id: seata_txtx-service-group: seata_tx_groupservice:vgroup-mapping:seata_tx_group: defaultregistry:type: nacosnacos:application: seata-serverserver-addr: 127.0.0.1:8848namespace:group: SEATA_GROUPdata-source-proxy-mode: AT

创建数据库

         可以在上述seata_db1数据库中创建两个数据库:t_account和t_account_tx

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for t_account
-- ----------------------------
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account`  (`id` INT NOT NULL AUTO_INCREMENT,`user_id` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`money` INT NULL DEFAULT 0,PRIMARY KEY (`id`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;-- ----------------------------
-- Records of t_account
-- ----------------------------
INSERT INTO `t_account` VALUES (1, 'U100000', 900);-- ----------------------------
-- Table structure for t_account_tx
-- ----------------------------
DROP TABLE IF EXISTS `t_account_tx`;
CREATE TABLE `t_account_tx`  (`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',`tx_id` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '事务id',`freeze_money` INT NULL DEFAULT NULL COMMENT '冻结金额',`state` INT NULL DEFAULT NULL COMMENT '状态 0try 1confirm 2cancel',PRIMARY KEY (`id`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;

        可以在上述seata_db2数据库中创建两个数据库:t_order和t_order_tx

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for t_order
-- ----------------------------
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order`  (`id` INT NOT NULL AUTO_INCREMENT,`user_id` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`commodity_code` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`count` INT NULL DEFAULT 0,`money` INT NULL DEFAULT 0,PRIMARY KEY (`id`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 25 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;-- ----------------------------
-- Records of t_order
-- ------------------------------ ----------------------------
-- Table structure for t_order_tx
-- ----------------------------
DROP TABLE IF EXISTS `t_order_tx`;
CREATE TABLE `t_order_tx`  (`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',`tx_id` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '事务id',`state` INT NULL DEFAULT NULL COMMENT '状态 0try 1confirm 2cancel',PRIMARY KEY (`id`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;

编写Mybatis-plus的三层及业务层

 seata-demo1

实体
package org.example.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;@Data
@TableName("t_account")
public class Account {@TableId(type = IdType.AUTO)private Integer id;private String userId;private int money;
}

package org.example.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;@Data
@TableName("t_account_tx")
public class AccountTX {public static final int STATE_TRY = 0;public static final int STATE_CONFIRM = 1;public static final int STATE_CANCEL = 2;@TableId(type = IdType.AUTO)private Integer id;private String txId;private int freezeMoney;private int state = STATE_TRY;
}
dao层
package org.example.dao;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.example.entity.Account;public interface AccountMapper  extends BaseMapper<Account> {
}

package org.example.dao;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.example.entity.AccountTX;public interface AccountTXMapper extends BaseMapper<AccountTX> {
}
service层
interface接口
package org.example.service;import com.baomidou.mybatisplus.extension.service.IService;
import org.example.entity.Account;public interface IAccountService  extends IService<Account> {/*** 账户扣款* @param userId* @param money* @return*/void reduce(String userId, int money);
}
package org.example.service;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;/*** TCC 二阶段提交业务接口*/
@LocalTCC
public interface IAccountTCCService {/*** try-预扣款*/@TwoPhaseBusinessAction(name="tryReduce", commitMethod = "confirm", rollbackMethod = "cancel")void tryReduce(@BusinessActionContextParameter(paramName = "userId") String userId,@BusinessActionContextParameter(paramName = "money") int money);/*** confirm-提交* @param ctx* @return*/boolean confirm(BusinessActionContext ctx);/*** cancel-回滚* @param ctx* @return*/boolean cancel(BusinessActionContext ctx);
}
实现层
package org.example.service;import org.example.dao.AccountMapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.example.entity.Account;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Service
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements IAccountService {@Override@Transactionalpublic void reduce(String userId, int money) {Account one = lambdaQuery().eq(Account::getUserId, userId).one();if(one != null && one.getMoney() < money){throw new RuntimeException("Not Enough Money ...");}lambdaUpdate().setSql("money = money - " + money).eq(Account::getUserId, userId).update();}
}
package org.example.service;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.example.dao.AccountMapper;
import org.example.dao.AccountTXMapper;
import org.example.entity.Account;
import org.example.entity.AccountTX;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class AccountTCCServiceImpl  implements IAccountTCCService {@Autowiredprivate AccountMapper accountMapper;@Autowiredprivate AccountTXMapper accountTXMapper;@Overridepublic void tryReduce(String userId, int money) {System.err.println("-----------tryReduce-------------" + RootContext.getXID());//业务悬挂AccountTX accountTX = accountTXMapper.selectOne(new LambdaQueryWrapper<AccountTX>().eq(AccountTX::getTxId, RootContext.getXID()));if (accountTX != null){//存在,说明已经canel执行过类,拒绝服务return;}Account one = accountMapper.selectOne(new LambdaQueryWrapper<Account>().eq(Account::getUserId, userId));if(one != null && one.getMoney() < money){throw new RuntimeException("Not Enough Money ...");}LambdaUpdateWrapper<Account> wrapper = new LambdaUpdateWrapper<>();wrapper.setSql("money = money - " + money);wrapper.eq(Account::getUserId, userId);accountMapper.update(null, wrapper);AccountTX tx = new AccountTX();tx.setFreezeMoney(money);tx.setTxId(RootContext.getXID());tx.setState(AccountTX.STATE_TRY);accountTXMapper.insert(tx);}@Overridepublic boolean confirm(BusinessActionContext ctx) {System.err.println("-----------confirm-------------");//删除记录int ret = accountTXMapper.delete(new LambdaQueryWrapper<AccountTX>().eq(AccountTX::getTxId, ctx.getXid()));return ret == 1;}@Overridepublic boolean cancel(BusinessActionContext ctx) {System.err.println("-----------cancel-------------");String userId = ctx.getActionContext("userId").toString();String money = ctx.getActionContext("money").toString();AccountTX accountTX = accountTXMapper.selectOne(new LambdaQueryWrapper<AccountTX>().eq(AccountTX::getTxId, ctx.getXid()));if (accountTX == null){//为空, 空回滚accountTX = new AccountTX();accountTX.setTxId(ctx.getXid());accountTX.setState(AccountTX.STATE_CANCEL);if(money != null){accountTX.setFreezeMoney(Integer.parseInt(money));}accountTXMapper.insert(accountTX);return true;}//幂等处理if(accountTX.getState() == AccountTX.STATE_CANCEL){return true;}//恢复余额accountMapper.update(null, new LambdaUpdateWrapper<Account>().setSql("money = money + " + money).eq(Account::getUserId, userId));accountTX.setFreezeMoney(0);accountTX.setState(AccountTX.STATE_CANCEL);int ret = accountTXMapper.updateById(accountTX);return ret == 1;}
}
controller层
package org.example.controller;import org.example.service.IAccountTCCService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("accounts")
public class AccountController {//@Autowired//private IAccountService accountService;@Autowiredprivate IAccountTCCService accountTCCService;@GetMapping(value = "/reduce")public String reduce(String userId, int money) {try {accountTCCService.tryReduce(userId, money);} catch (Exception exx) {exx.printStackTrace();return "FAIL";}return "SUCCESS";}}
启动类
package org.example;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("org.example.dao")
@EnableFeignClients
public class SeataDemo1Application
{public static void main( String[] args ){SpringApplication.run(SeataDemo1Application.class,args);}
}

seata-demo2
实体层
package org.example.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;@Data
@TableName("t_order")
public class Order {@TableId(type = IdType.AUTO)private Integer id;private String userId;private String commodityCode;private Integer count;private Integer money;
}

package org.example.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;@Data
@TableName("t_order_tx")
public class OrderTX {public static final int STATE_TRY = 0;public static final int STATE_CONFIRM = 1;public static final int STATE_CANCEL = 2;@TableId(type = IdType.AUTO)private Integer id;private String txId;private int state = STATE_TRY;
}
dao层
package org.example.dao;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.example.entity.Order;public interface OrderMapper  extends BaseMapper<Order> {
}

package org.example.dao;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.example.entity.OrderTX;public interface OrderTXMapper extends BaseMapper<OrderTX> {
}
service层
interface层
package org.example.service;import com.baomidou.mybatisplus.extension.service.IService;
import org.example.entity.Order;public interface IOrderService  extends IService<Order> {/*** 创建订单*/void create(String userId, String commodityCode, int orderCount);
}
package org.example.service;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;/*** TCC 二阶段提交业务接口*/
@LocalTCC
public interface IOrderTCCService {/*** try-预扣款*/@TwoPhaseBusinessAction(name="tryCreate", commitMethod = "confirm", rollbackMethod = "cancel")void tryCreate(@BusinessActionContextParameter(paramName = "userId") String userId,@BusinessActionContextParameter(paramName = "commodityCode") String commodityCode,@BusinessActionContextParameter(paramName = "orderCount") int orderCount);/*** confirm-提交* @param ctx* @return*/boolean confirm(BusinessActionContext ctx);/*** cancel-回滚* @param ctx* @return*/boolean cancel(BusinessActionContext ctx);}
实现层
package org.example.service;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.example.entity.Order;
import org.example.feign.AccountFeignClient;
import org.example.dao.OrderMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Service
@Transactional
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {@Autowiredprivate AccountFeignClient accountFeignClient;@Override@Transactionalpublic void create(String userId, String commodityCode, int count) {// 定单总价 = 订购数量(count) * 商品单价(100)int orderMoney = count * 100;// 生成订单Order order = new Order();order.setCount(count);order.setCommodityCode(commodityCode);order.setUserId(userId);order.setMoney(orderMoney);super.save(order);// 调用账户余额扣减String result = accountFeignClient.reduce(userId, orderMoney);if (!"SUCCESS".equals(result)) {throw new RuntimeException("Failed to call Account Service. ");}}
}
package org.example.service;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.example.dao.OrderMapper;
import org.example.dao.OrderTXMapper;
import org.example.entity.Order;
import org.example.entity.OrderTX;
import org.example.feign.AccountFeignClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class OrderTCCServiceImpl  implements IOrderTCCService {@Autowiredprivate AccountFeignClient accountFeignClient;@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate OrderTXMapper orderTXMapper;@Overridepublic void tryCreate(String userId, String commodityCode, int count) {System.err.println("---------tryCreate-----------");//业务悬挂OrderTX orderTX = orderTXMapper.selectOne(new LambdaQueryWrapper<OrderTX>().eq(OrderTX::getTxId, RootContext.getXID()));if (orderTX != null){//存在,说明已经canel执行过类,拒绝服务return;}// 定单总价 = 订购数量(count) * 商品单价(100)int orderMoney = count * 100;// 生成订单Order order = new Order();order.setCount(count);order.setCommodityCode(commodityCode);order.setUserId(userId);order.setMoney(orderMoney);orderMapper.insert(order);OrderTX tx = new OrderTX();tx.setTxId(RootContext.getXID());tx.setState(OrderTX.STATE_TRY);orderTXMapper.insert(tx);// 调用账户余额扣减String result = accountFeignClient.reduce(userId, orderMoney);if (!"SUCCESS".equals(result)) {throw new RuntimeException("Failed to call Account Service. ");}}@Overridepublic boolean confirm(BusinessActionContext ctx) {System.err.println("---------confirm-----------");//删除记录int ret = orderTXMapper.delete(new LambdaQueryWrapper<OrderTX>().eq(OrderTX::getTxId, ctx.getXid()));return ret == 1;}@Overridepublic boolean cancel(BusinessActionContext ctx) {System.err.println("---------cancel-----------" );String userId = ctx.getActionContext("userId").toString();String commodityCode = ctx.getActionContext("commodityCode").toString();OrderTX orderTX = orderTXMapper.selectOne(new LambdaQueryWrapper<OrderTX>().eq(OrderTX::getTxId, ctx.getXid()));if (orderTX == null){//为空, 空回滚orderTX = new OrderTX();orderTX.setTxId(ctx.getXid());orderTX.setState(OrderTX.STATE_CANCEL);orderTXMapper.insert(orderTX);return true;}//幂等处理if(orderTX.getState() == OrderTX.STATE_CANCEL){return true;}//恢复余额orderMapper.delete(new LambdaQueryWrapper<Order>().eq(Order::getUserId, userId).eq(Order::getCommodityCode, commodityCode));orderTX.setState(OrderTX.STATE_CANCEL);int ret = orderTXMapper.updateById(orderTX);return ret == 1;}
}
controller层
package org.example.controller;import org.example.service.IOrderTCCService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("orders")
public class OrderController {@Autowiredprivate IOrderTCCService orderTCCService;@GetMapping(value = "/create")public String create(String userId, String commodityCode, int orderCount) {try {orderTCCService.tryCreate(userId, commodityCode, orderCount);} catch (Exception exx) {exx.printStackTrace();return "FAIL";}return "SUCCESS";}
}
feign层
package org.example.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;@FeignClient(name = "seata-demo2-service")
public interface AccountFeignClient {@GetMapping("/accounts/reduce")String reduce(@RequestParam("userId") String userId, @RequestParam("money") int money);}
启动类
package org.example;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("org.example.dao")
@EnableFeignClients
public class SeataDemo2Application
{public static void main( String[] args ){SpringApplication.run(SeataDemo2Application.class,args);}
}

测试

正常:http://localhost:8088/businesses/purchase?rollback=false&count=2

超库存:http://localhost:8088/businesses/purchase?rollback=false&count=12

超余额:http://localhost:8088/businesses/purchase?rollback=false&count=8


SAGA模式:

        saga模式适用于长事务,如银行API调用,等繁杂的事务处理

相关文章:

SpringCloud整合seata,XA、AT、TCC、SAGA模式

参考资料&#xff1a; SpringCloud-Alibaba搭建 SpringCloud-nacos整合 Seata部署 参考demo&#xff08;及学习资料&#xff09; seata官网 参考视频​​​​​c&#xff08;AT模式的UNDO_LOG讲的可能有点问题&#xff0c;但是很通俗易懂&#xff09; 参考视频2&#xff…...

centos8.0 docker ngnix

问题1&#xff1a;镜像拉取不下来&#xff0c;用DAO云加速器 问题2&#xff1a;ngnix镜像不能运行&#xff0c; 无法检索OCI运行时错误 &#xff0c;更新包yum update libseccomp 问题3&#xff1a;docker run -v 目录有ngninx.conf 或conf.d 等 .特殊字符&#xff0c;报无效格…...

案例-06.部门管理-根据ID查询

一.根据ID查询-接口文档 二.根据ID查询-Controller层 package com.gjw.controller;/*** 部门管理Controller*/import com.gjw.anno.Log; import com.gjw.pojo.Dept; import com.gjw.pojo.Result; import com.gjw.service.DeptService; import com.gjw.service.impl.DeptServi…...

moveable 一个可实现前端海报编辑器的 js 库

目录 缘由-胡扯本文实验环境通用流程1.基础移动1.1 基础代码1.1.1 data-* 解释 1.2 操作元素创建1.3 css 修饰1.4 cdn 引入1.5 js 实现元素可移动1.6 图片拖拽2.缩放3.旋转4.裁剪 懒得改文案了&#xff0c;海报编辑器换方案了&#xff0c;如果后面用别的再更。 缘由-胡扯 导火…...

【愚公系列】《Python网络爬虫从入门到精通》012-字符串处理

标题详情作者简介愚公搬代码头衔华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,CSDN商业化专家,阿里云专家博主,阿里云签约作者,腾讯云优秀博主,腾讯云内容共创官,掘金优秀博主,亚马逊技领云博主,51CTO博客专家等。近期荣誉2022年度…...

shell脚本备份MySQL数据库和库下表

目录 注意&#xff1a; 一.脚本内容 二.执行效果 三.创建定时任务 注意&#xff1a; 以下为对MySQL5.7.42版本数据库备份shell脚本参考运行备份的机器请确认mysqldump版本>5.7&#xff0c;否则备份参数--set-gtid-purgedOFF无效&#xff0c;考虑到一般数据库节点和备份…...

java处理pgsql的text[]类型数据问题

背景 公司要求使用磐维数据库&#xff0c;于是去了解了这个是基于PostgreSQL构建的&#xff0c;在使用时有场景一条图片数据中可以投放到不同的页面&#xff0c;由于简化设计就放在数组中&#xff0c;于是使用了text[]类型存储&#xff1b;表结构 #这是一个简化版表结构&…...

MongoDB 架构设计:深入解析核心组件与工作原理

MongoDB 架构设计&#xff1a;深入解析核心组件与工作原理 MongoDB 作为一个高性能、易扩展的 NoSQL 数据库&#xff0c;其优秀的架构设计是其成功的关键。本文将深入解析 MongoDB 的架构设计&#xff0c;详细讲解其核心组件和工作原理&#xff0c;帮助您更好地理解和使用 Mon…...

【PostgreSQL】PG在windows下的安装

一、准备 通过官网下载安装文件&#xff0c;官方下载路径如下&#xff1a; https://www.postgresql.org/download/windows/ 二、安装 双击postgresql-17.3-1-windows-x64.exe文件&#xff0c;启动安装&#xff0c;进入安装步骤&#xff0c;点击Next 选择PG安装路径&#xff…...

掌握SQL多表连接查询_轻松处理复杂数据关系

1. 引言 1.1 数据库中的多表关系概述 在实际应用中&#xff0c;数据库通常由多个表组成&#xff0c;每个表存储不同类型的数据。例如&#xff0c;在一个电子商务系统中&#xff0c;可能会有用户表、订单表、产品表等。这些表之间存在关联关系&#xff0c;通过多表连接查询可以…...

MVC模式和MVVM模式

目录 一、MVC模式和MVVM模式 1. MVC模式 2. MVVM 模式 3.在Qt中的应用示例 4.总结 二、MVC与MVVM模式的共同点和区别 1.共同点 2.区别 3.交互流程 4.总结 MVC&#xff08;Model-View-Controller&#xff09;和MVVM&#xff08;Model-View-ViewModel&#xff09;是两种…...

Macos机器hosts文件便捷修改工具——SwitchHosts

文章目录 SwitchHosts软件下载地址操作添加方案切换方案管理方案快捷键 检测 SwitchHosts SwitchHosts 是一款 Mac 平台上的免费软件&#xff0c;它可以方便地管理和切换 hosts 文件&#xff0c;支持多种 hosts 文件格式。 软件下载地址 SwitchHosts 操作 添加方案 添加 …...

mysqld_exporter的搭建

1、创建/data/apps目录&#xff0c;并且下载mysql_exporte mkdir -p /data/apps ​ cd /data/apps ​ wget https://githubfast.com/prometheus/mysqld_exporter/releases/download/v0.12.1/mysqld_exporter-0.12.1.linux-amd64.tar.gz 或者 wget https://github.com/promethe…...

CentOS上安装WordPress

在CentOS上安装WordPress是一个相对直接的过程&#xff0c;可以通过多种方法完成&#xff0c;包括使用LAMP&#xff08;Linux, Apache, MySQL, PHP&#xff09;栈或使用更现代的LEMP&#xff08;Linux, Nginx, MySQL, PHP&#xff09;栈。 我选择的是&#xff08;Linux, Nginx…...

【数据结构】 栈和队列

在计算机科学的世界里&#xff0c;数据结构是构建高效算法的基础。栈&#xff08;Stack&#xff09;和队列&#xff08;Queue&#xff09;作为两种基本且重要的数据结构&#xff0c;在软件开发、算法设计等众多领域都有着广泛的应用。今天&#xff0c;我们就来深入探讨一下栈和…...

微服务限流策略与性能优化全解析

一、服务瓶颈评估实例 1.1 背景介绍 本文我用我工作中实际的一个电商营销中台系统的订单服务来阐述。此微服务数据库采用 MySQL&#xff0c;配置为 8 核 32G。订单服务部署于一组服务器集群&#xff0c;考虑到高可用性&#xff0c;至少配置 3 个节点&#xff0c;每个节点服务…...

Windows环境搭建ES集群

搭建步骤 下载安装包 下载链接&#xff1a;https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.17.27-windows-x86_64.zip 解压 解压并复制出3份 es-node1配置 config/elasticsearch.yml cluster.name: xixi-es-win node.name: node-1 path.data: D:\\wor…...

qt中实现QListWidget列表

FR:徐海涛&#xff08;hunkxu)...

大模型参数规模解析:32B中的“B“代表什么?如何影响AI性能?

以下是优化后的技术笔记整理&#xff0c;包含关键知识点解析和行业应用案例&#xff1a; 大模型参数规模解析&#xff1a;32B中的"B"代表什么&#xff1f;如何影响AI性能&#xff1f; 一、参数单位解读 B Billion&#xff08;十亿&#xff09;&#xff1a;在AI模…...

Docker+Ollama+WebUI+AnythingLLM,构建企业本地AI大模型

文章目录 概要Ollama部署WebUI部署AnythingLLM部署Docker-Compose部署管理所有容器小结参考文章 概要 Ollama 是一个强大的大模型提供者&#xff0c;它通过开源的方式&#xff0c;为开发者和企业提供了先进的大型语言模型&#xff08;LLM&#xff09;。这些模型拥有处理和生成…...

【大模型】DeepSeek 高级提示词技巧使用详解

目录 一、前言 二、DeepSeek 通用提示词技巧 2.1 DeepSeek 通用提示词技巧总结 三、DeepSeek 进阶使用技巧 3.1 DeepSeek一个特定角色的人设 3.1.1 为DeepSeek设置角色操作案例一 3.1.2 为DeepSeek设置角色操作案例二 3.2 DeepSeek开放人设升级 3.2.1 特殊的人设&#…...

【玩转全栈】----Django基本配置和介绍

目录 Django基本介绍&#xff1a; Django基本配置&#xff1a; 安装Django 创建项目 创建app 注册app Django配置路由URL Django创建视图 启动项目 Django基本介绍&#xff1a; Django是一个开源的、基于Python的高级Web框架&#xff0c;旨在以快速、简洁的方式构建高质量的We…...

[Unity角色控制专题] (借助ai)详细解析官方第三人称控制器

首先模板链接在这里&#xff0c;你可以直接下载并导入unity即可查看官方为开发者写好一套控制器 本文的ai工具用到了豆包&#xff0c;其灵活程度很高&#xff0c;总结能力也强过我太多 因此大量使用&#xff0c;不喜勿喷 Starter Assets - ThirdPerson | Updates in new Charac…...

安装 Docker Desktop 修改默认安装目录到指定目录

Docker Desktop安装目录设置 Docker Desktop 默认安装位置 &#xff08;C:\Program Files\Docker\Docker) 是这个 &#xff0c;导致系统盘占用过大&#xff0c;大概2G ; 那么如何安装到其他磁盘呢&#xff1f; 根据docker desktop 官网 Docker Desktop install 我们可以看到&a…...

渗透测试--文件包含漏洞

文件包含漏洞 前言 《Web安全实战》系列集合了WEB类常见的各种漏洞&#xff0c;笔者根据自己在Web安全领域中学习和工作的经验&#xff0c;对漏洞原理和漏洞利用面进行了总结分析&#xff0c;致力于漏洞准确性、丰富性&#xff0c;希望对WEB安全工作者、WEB安全学习者能有所帮助…...

【ISO 14229-1:2023 UDS诊断全量测试用例清单系列:第十一节】

ISO 14229-1:2023 UDS诊断服务测试用例全解析&#xff08;RequestTransferExit0x37服务&#xff09; 作者&#xff1a;车端域控测试工程师 更新日期&#xff1a;2025年02月13日 关键词&#xff1a;UDS协议、0x37服务、传输终止、NRC验证、ISO 14229-1:2023 一、服务功能概述 0…...

虚拟环境测试部署应用

一、作用 虚拟环境(env)在计算机领域,特别是在软件开发和测试中扮演着重要角色。它主要用于创建一个隔离的环境,使得开发者可以在不影响系统其余部分的情况下安装、配置和运行软件项目。以下是虚拟环境的一些主要作用: 1、依赖管理 不同的项目可能需要不同版本的库或框…...

【线性代数】2矩阵

1.矩阵的运算 1.1.定义 矩阵行列式数表数行数和列数可以不相等行数和列数必须相等1.2.加法与数乘 矩阵的数乘:所有元素都乘这个数 矩阵的加法:对应位置处元素相加 🦊已知,求 1.3.乘法 矩阵乘法三步法 ①能不能乘:内定乘 ②乘完是何类型:外定型 ③中的元素是什么:左…...

前端为什么要使用new Promise包裹一个函数

在前端开发中&#xff0c;使用 new Promise 包裹一个函数主要是为了将原本不支持 Promise 规范的操作转化为支持 Promise 规范的操作&#xff0c;从而可以更好地处理异步操作&#xff0c;提升代码的可读性和可维护性。下面详细介绍这么做的常见原因和应用场景&#xff1a; 1. …...

深度学习在天文观测中的应用:解锁宇宙的奥秘

深度学习在天文观测中的应用:解锁宇宙的奥秘 引言 宇宙是无尽的,天文学家通过观测天体来揭示宇宙的奥秘。随着现代天文设备技术的进步,我们现在可以通过 射电望远镜、空间望远镜 和 地面望远镜 获取大量的天文数据。然而,这些数据的规模和复杂性让传统的手工分析方法变得…...