Seata TCC 模式理论学习、生产级使用示例搭建及注意事项 | Spring Cloud55
一、前言
通过以下系列章节:
docker-compose 实现Seata Server高可用部署 | Spring Cloud 51
Seata AT 模式理论学习、事务隔离及部分源码解析 | Spring Cloud 52
Spring Boot集成Seata利用AT模式分布式事务示例 | Spring Cloud 53
Seata XA 模式理论学习、使用及注意事项 | Spring Cloud54
我们对Seata及其AT事务模式、XA事务模式的理论、使用有了深入的了解,今天继续对Seata的TCC事务模式进行理论学习;并区别与官网,我们利用openfeign进行生产级示例搭建,降低入门难度。
理论部分来自
Seata官网:http://seata.io/zh-cn/docs/dev/mode/tcc-mode.html
二、整体机制
回顾前面章节的学习:一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
- 一阶段
prepare行为 - 二阶段
commit或rollback行为

上面这个流程中,一共涉及到了三个方法,prepare、commit 以及 rollback,这三个方法都完全是用户自定义的方法,都是需要我们自己来实现的。相较于 AT 事务模式 TCC 这种模式其实是不依赖于底层数据库的事务支持的。
三、示例说明
这是一个商品下单的案例,一共有四个服务和一个公共模块:
account-tcc:账户服务,可以查询/修改用户的账户信息order-tcc:订单服务,可以下订单。storage-tcc:仓储服务,可以查询/修改商品的库存数量。bussiness-tcc:业务服务,用户下单操作将在这里完成。common-tcc:公共模块,包含:实体类、openfeign接口、统一异常处理等。
具体业务吊牌逻辑如下:

四、数据库设计
4.1 账户表
account-tcc 账户服务对应账户表:t_account:
-- ----------------------------
-- Table structure for t_account
-- ----------------------------
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (`id` bigint NOT NULL AUTO_INCREMENT,`user_id` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '用户ID',`money` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '账户余额',`freeze_money` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '冻结金额',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of t_account
-- ----------------------------
INSERT INTO `t_account` VALUES (1, 'user1', 500.00, 0.00);SET FOREIGN_KEY_CHECKS = 1;
4.2 仓储表
storage-tcc 仓储服务对应账户表:t_storage:
-- ----------------------------
-- Table structure for t_storage
-- ----------------------------
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (`id` bigint NOT NULL AUTO_INCREMENT,`commodity_code` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,`count` int NULL DEFAULT 0,`freeze_count` int NULL DEFAULT 0,PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `commodity_code`(`commodity_code` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of t_storage
-- ----------------------------
INSERT INTO `t_storage` VALUES (1, 'iphone', 6, 0);SET FOREIGN_KEY_CHECKS = 1;
4.3 订单表
order-tcc 订单服务对应账户表:t_order:
-- ----------------------------
-- Table structure for t_order
-- ----------------------------
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (`id` bigint NOT NULL AUTO_INCREMENT,`user_id` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,`commodity_code` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,`count` int NULL DEFAULT 0,`money` decimal(10, 2) NULL DEFAULT 0.00,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;
五、示例搭建
5.1 项目总体结构



5.2 common-tcc 搭建
5.2.1 实体类
com/gm/seata/openfeign/entity/Account.java:
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;@Data
@TableName("t_account")
public class Account {@TableId(type = IdType.ASSIGN_ID)private long id;private String userId;private BigDecimal money;private BigDecimal freezeMoney;
}
com/gm/seata/openfeign/entity/Order.java:
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;@Data
@TableName("t_order")
public class Order {@TableId(type = IdType.ASSIGN_ID)private long id;private String userId;private String commodityCode;private int count;private BigDecimal money;
}
com/gm/seata/openfeign/entity/Storage.java:
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;@Data
@TableName("t_order")
public class Order {@TableId(type = IdType.ASSIGN_ID)private long id;private String userId;private String commodityCode;private int count;private BigDecimal money;
}
5.2.2 feign接口
com/gm/seata/openfeign/feign/AccountServiceApi.java:
import com.gm.seata.openfeign.entity.Account;
import com.gm.seata.openfeign.util.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;@FeignClient(value = "account-tcc")
public interface AccountServiceApi {/*** 扣除账户余额* @param userId* @param money* @return*/@RequestMapping(value = "deduct", method = RequestMethod.GET)R<Account> deduct(@RequestParam("userId") String userId, @RequestParam("money") BigDecimal money);
}
com/gm/seata/openfeign/feign/OrderServiceApi.java:
import com.gm.seata.openfeign.util.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;@FeignClient(value = "order-tcc")
public interface OrderServiceApi {/*** 创建订单* @param userId* @param commodityCode* @param count* @return*/@RequestMapping(value = "createOrder", method = RequestMethod.GET)R<Boolean> createOrder(@RequestParam("userId") String userId, @RequestParam("commodityCode") String commodityCode, @RequestParam("count") Integer count);
}
com/gm/seata/openfeign/feign/StorageServiceApi.java:
import com.gm.seata.openfeign.entity.Storage;
import com.gm.seata.openfeign.util.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;@FeignClient(value = "storage-tcc")
public interface StorageServiceApi {/*** 扣除库存* @param commodityCode* @param count* @return*/@RequestMapping(value = "deduct", method = RequestMethod.GET)R<Storage> deduct(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Integer count);
}
5.2.3 feign 微服务调用异码处理
com/gm/seata/openfeign/handle/FeignErrorDecoder.java:
import com.alibaba.fastjson.JSONObject;
import com.gm.seata.openfeign.util.ErrorEnum;
import feign.Response;
import feign.RetryableException;
import feign.Util;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;import java.nio.charset.Charset;@Slf4j
@Configuration
public class FeignErrorDecoder extends ErrorDecoder.Default {@Overridepublic Exception decode(String methodKey, Response response) {try {// 可以自定义一些逻辑String message = Util.toString(response.body().asReader(Charset.forName("utf8")));JSONObject jsonObject = JSONObject.parseObject(message);int code = jsonObject.getInteger("code");ErrorEnum errorEnum = ErrorEnum.getEnumByCode(code);// 包装成自己自定义的异常return new RuntimeException(String.valueOf(errorEnum.getCode()));} catch (Exception e) {log.error("非已知异常", e.getMessage(), e);}Exception exception = super.decode(methodKey, response);// 如果是RetryableException,则返回继续重试if (exception instanceof RetryableException) {return exception;}return new RuntimeException(String.valueOf(ErrorEnum.UNKNOWN_EXCEPTION.getCode()));}
}
5.2.4 Controller 统一异常处理
com/gm/seata/openfeign/handle/GlobalBizExceptionHandler.java:
import com.gm.seata.openfeign.util.ErrorEnum;
import com.gm.seata.openfeign.util.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;/*** 全局异常处理器*/
@Slf4j
@Order(10000)
@RestControllerAdvice
public class GlobalBizExceptionHandler {/*** 全局异常.** @param e the e* @return R*/@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public R handleGlobalException(Exception e) {log.error("全局异常信息 ex={}", e.getMessage(), e);R r = null;// 根据异常信息与已知异常进行匹配try {int code = Integer.parseInt(e.getLocalizedMessage());ErrorEnum errorEnum = ErrorEnum.getEnumByCode(code);if (errorEnum != null) {r = R.restResult(null, errorEnum.getCode(), errorEnum.getTitle());}} finally {if (e instanceof feign.FeignException) {ErrorEnum errorEnum = ErrorEnum.UNKNOWN_EXCEPTION;r = R.restResult(null, errorEnum.getCode(), errorEnum.getTitle());}if (r == null) {r = R.failed(e.getLocalizedMessage());}}return r;}
}
5.2.5 已知异常枚举类
com/gm/seata/openfeign/util/ErrorEnum.java:
import lombok.AllArgsConstructor;
import lombok.Getter;@Getter
@AllArgsConstructor
public enum ErrorEnum {NO_SUCH_COMMODITY(3000, "无此商品"),STORAGE_LOW_PREPARE(3001, "库存不足,预扣库存失败"),STORAGE_LOW_COMMIT(3002, "库存不足,扣库存失败"),NO_SUCH_ACCOUNT(4000, "无此账户"),ACCOUNT_LOW_PREPARE(4001, "余额不足,预扣款失败"),ACCOUNT_LOW_COMMIT(4002, "余额不足,扣款失败"),UNKNOWN_EXCEPTION(9999, "远程方法调用异常");private final Integer code;private final String title;public static ErrorEnum getEnumByCode(int code) {for (ErrorEnum error : ErrorEnum.values()) {if (error.getCode().equals(code)) {return error;}}return null;}
}
5.2.6 响应信息结构体
com/gm/seata/openfeign/util/R.java:
import lombok.*;
import lombok.experimental.Accessors;
import java.io.Serializable;/*** 响应信息主体**/
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class R<T> implements Serializable {private static final long serialVersionUID = 1L;/*** 成功标记*/private static final Integer SUCCESS = 0;/*** 失败标记*/private static final Integer FAIL = 1;@Getter@Setterprivate int code;@Getter@Setterprivate String msg;@Getter@Setterprivate T data;public static <T> R<T> ok() {return restResult(null, SUCCESS, null);}public static <T> R<T> ok(T data) {return restResult(data, SUCCESS, null);}public static <T> R<T> ok(T data, String msg) {return restResult(data, SUCCESS, msg);}public static <T> R<T> failed() {return restResult(null, FAIL, null);}public static <T> R<T> failed(String msg) {return restResult(null, FAIL, msg);}public static <T> R<T> failed(T data) {return restResult(data, FAIL, null);}public static <T> R<T> failed(T data, String msg) {return restResult(data, FAIL, msg);}public static <T> R<T> restResult(T data, int code, String msg) {R<T> apiResult = new R<>();apiResult.setCode(code);apiResult.setData(data);apiResult.setMsg(msg);return apiResult;}
}
5.2.7 自动配置实现
在src/main/resources/META-INF/spring路径下新建文件org.springframework.boot.autoconfigure.AutoConfiguration.imports内容如下:
com.gm.seata.openfeign.handle.GlobalBizExceptionHandler
com.gm.seata.openfeign.handle.FeignErrorDecoder
新建文件org.springframework.cloud.openfeign.FeignClient.imports内容如下:
com.gm.seata.openfeign.feign.AccountServiceApi
com.gm.seata.openfeign.feign.OrderServiceApi
com.gm.seata.openfeign.feign.StorageServiceApi
通过上述方式实现自动配置。
5.3 account-tcc 搭建
5.3.1 完整依赖
seata/openfeign-tcc/account-tcc/pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>openfeign-tcc</artifactId><groupId>com.gm</groupId><version>0.0.1-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>account-tcc</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>com.gm</groupId><artifactId>common-tcc</artifactId><version>0.0.1-SNAPSHOT</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!-- 注意一定要引入对版本,要引入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><!-- 上面排除掉了springcloud默认色seata版本,此处引入和seata-server版本对应的seata包--><dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId><version>1.6.1</version></dependency><!--<dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId></dependency>--><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
5.3.2 配置文件
src/main/resources/bootstrap.yml:
server:port: 3011spring:application:name: @artifactId@cloud:nacos:username: @nacos.username@password: @nacos.password@discovery:server-addr: ${NACOS_HOST:nacos1.kc}:${NACOS_PORT:8848}datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.0.46:3306/seata-tcc?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghaiusername: rootpassword: '1qaz@WSX'
seata:# 是否开启spring-boot自动装配,seata-spring-boot-starter 专有配置,默认trueenabled: true# 是否开启数据源自动代理,seata-spring-boot-starter专有配置,默认会开启数据源自动代理,可通过该配置项关闭enable-auto-data-source-proxy: false# 配置自定义事务组名称,需与下方server.vgroupMapping配置一致,程序会通过用户配置的配置中心去寻找service.vgroupMappingtx-service-group: mygroupconfig: # 从nacos配置中心获取client端配置type: nacosnacos:server-addr: ${NACOS_HOST:nacos1.kc}:${NACOS_PORT:8848}group : DEFAULT_GROUPnamespace: a4c150aa-fd09-4595-9afe-c87084b22105dataId: seataServer.propertiesusername: @nacos.username@password: @nacos.username@registry: # 通过服务中心通过服务发现获取seata-server服务地址type: nacosnacos:# 注:客户端注册中心配置的serverAddr和namespace与Server端一致,clusterName与Server端cluster一致application: seata-server # 此处与seata-server的application一致,才能通过服务发现获取服务地址group : DEFAULT_GROUPserver-addr: ${NACOS_HOST:nacos1.kc}:${NACOS_PORT:8848}userName: @nacos.username@password: @nacos.username@namespace: a4c150aa-fd09-4595-9afe-c87084b22105service:# 应用程序(客户端)会通过用户配置的配置中心去寻找service.vgroupMapping.[事务分组配置项]vgroup-mapping:# 事务分组配置项[mygroup]对应的值为TC集群名[default],与Seata-Server中的seata.registry.nacos.cluster配置一致mygroup : default# 全局事务开关,默认false。false为开启,true为关闭disable-global-transaction: falseclient:rm:report-success-enable: true
management:endpoints:web:exposure:include: '*'logging:level:io.seata: debug# mybatis-plus配置控制台打印完整带参数SQL语句
mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
5.2.3 功能搭建
5.3.3.1 启动类
com/gm/seata/openfeign/AccountTCCApplication.java:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication
@EnableDiscoveryClient
public class AccountTCCApplication {public static void main(String[] args) {SpringApplication.run(AccountTCCApplication.class, args);}
}
5.3.3.2 Mapper类
com/gm/seata/openfeign/mapper/AccountMapper.java:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gm.seata.openfeign.entity.Account;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;@Mapper
public interface AccountMapper extends BaseMapper<Account> {@Select("SELECT * FROM t_account WHERE user_id = #{userId} limit 1")Account getAccountByUserId(@Param("userId") String userId);
}
5.3.3.3 Service类
业务重点来了,敲黑板!!!
com/gm/seata/openfeign/service/AccountService.java:
import com.gm.seata.openfeign.entity.Account;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
import java.math.BigDecimal;@LocalTCC
public interface AccountService {/*** 执行资源检查及预业务操作*/// @BusinessActionContextParameter 注解就是将对应的参数放入到 BusinessActionContext 中,将来可以从 BusinessActionContext 中取出对应的参数。@TwoPhaseBusinessAction(name = "accountService", commitMethod = "commit", rollbackMethod = "rollback")Account prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "userId") String userId, @BusinessActionContextParameter(paramName = "money") BigDecimal money);/*** 全局事物进行提交*/boolean commit(BusinessActionContext actionContext);/*** 全局事务进行回滚*/boolean rollback(BusinessActionContext actionContext);
}
- 首先接口的定义上,需要加一个注解
@LocalTCC,这个表示开启Seata中的TCC模式。 - 然后就是
@TwoPhaseBusinessAction注解,两阶段提交的注解,这个注解有三个属性,第一个name就是处理两阶段提交的bean的名字,其实就是当前bean的名字,当前类名首字母小写。两阶段第一阶段就是prepare阶段,也就是预处理阶段 。@TwoPhaseBusinessAction注解所在的方法,第二阶段则分为两种情况,提交或者回滚,分别对应了两个不同的方法,commitMethod和rollbackMethod就指明了相应的方法。 - 一阶段的
prepare需要开发者手动调用,二阶段的commit或者rollback则是系统自动调用。prepare中的方法是由开发者来传递的,而在二阶段的方法中,相关的参数我们需要从BusinessActionContext中获取,@BusinessActionContextParameter注解就是将对应的参数放入到BusinessActionContext中(注意需要给每一个参数取一个名字),将来可以从BusinessActionContext中取出对应的参数。 - 另外需要注意,接口的返回值设计成
boolean,用以表示相应的操作执行成功还是失败,返回false表示执行失败,默认会有重试机制进行重试。
com/gm/seata/openfeign/service/impl/AccountServiceImpl.java:
package com.gm.seata.openfeign.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.gm.seata.openfeign.entity.Account;
import com.gm.seata.openfeign.mapper.AccountMapper;
import com.gm.seata.openfeign.service.AccountService;
import com.gm.seata.openfeign.util.ErrorEnum;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal;@Slf4j
@Service
public class AccountServiceImpl implements AccountService {@AutowiredAccountMapper accountMapper;/*** 预扣款阶段,检查账户余额** @param userId* @param money* @return*/@Transactional(rollbackFor = Exception.class)public Account prepare(BusinessActionContext actionContext, String userId, BigDecimal money) {Account account = accountMapper.getAccountByUserId(userId);if (account == null) {//throw new RuntimeException("账户不存在");throw new RuntimeException(String.valueOf(ErrorEnum.NO_SUCH_ACCOUNT.getCode()));}// 账户余额 与 本次消费金额进行 比较if (account.getMoney().compareTo(money) < 0) {//throw new RuntimeException("余额不足,预扣款失败");throw new RuntimeException(String.valueOf(ErrorEnum.ACCOUNT_LOW_PREPARE.getCode()));}account.setFreezeMoney(account.getFreezeMoney().add(money));account.setMoney(account.getMoney().subtract(money));QueryWrapper query = new QueryWrapper();query.eq("user_id", userId);Integer i = accountMapper.update(account, query);log.info("{} 账户预扣款 {} 元", userId, money);return account;}/*** 实际扣款阶段** @param actionContext* @return*/@Transactional(rollbackFor = Exception.class)public boolean commit(BusinessActionContext actionContext) {String userId = (String) actionContext.getActionContext("userId");BigDecimal money = new BigDecimal(actionContext.getActionContext("money").toString());Account account = accountMapper.getAccountByUserId(userId);// 账户冻结金额 与 本次消费金额进行 比较if (account.getFreezeMoney().compareTo(money) < 0) {// 抛出指定异常throw new RuntimeException(String.valueOf(ErrorEnum.ACCOUNT_LOW_COMMIT.getCode()));}account.setFreezeMoney(account.getFreezeMoney().subtract(money));QueryWrapper query = new QueryWrapper();query.eq("user_id", userId);Integer i = accountMapper.update(account, query);log.info("{} 账户扣款 {} 元", userId, money);return i == 1;}/*** 账户回滚阶段** @param actionContext* @return*/@Transactional(rollbackFor = Exception.class)public boolean rollback(BusinessActionContext actionContext) {String userId = (String) actionContext.getActionContext("userId");BigDecimal money = new BigDecimal(actionContext.getActionContext("money").toString());Account account = accountMapper.getAccountByUserId(userId);if (account.getFreezeMoney().compareTo(money) >= 0) {account.setFreezeMoney(account.getFreezeMoney().subtract(money));account.setMoney(account.getMoney().add(money));QueryWrapper query = new QueryWrapper();query.eq("user_id", userId);Integer i = accountMapper.update(account, query);log.info("{} 账户释放冻结金额 {} 元", userId, money);return i == 1;}log.info("{} 账户资金已释放", userId);// 说明prepare中抛出异常,未冻结资金return true;}}
5.3.3.4 Controller类
com/gm/seata/openfeign/controller/AccountController.java:
import com.gm.seata.openfeign.service.AccountService;
import com.gm.seata.openfeign.util.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;@RestController
public class AccountController {@AutowiredAccountService accountService;/*** 扣除账户余额** @param userId* @param money* @return*/@RequestMapping(value = "deduct", method = RequestMethod.GET)public R<Account> deduct(@RequestParam("userId") String userId, @RequestParam("money") BigDecimal money) {return R.ok(accountService.prepare(null, userId, money));}
}
5.4 storage-tcc 搭建
5.4.1 完整依赖
减少重复内容,请参考 5.3.1 部分,自动修改
5.4.2 配置文件
减少重复内容,请参考 5.3.2 部分,自动修改
5.4.3 功能搭建
5.4.3.1 启动类
com/gm/seata/openfeign/StorageTCCApplication.java:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication
@EnableDiscoveryClient
public class StorageTCCApplication {public static void main(String[] args) {SpringApplication.run(StorageTCCApplication.class, args);}
}
5.4.3.2 Mapper类
com/gm/seata/openfeign/mapper/StorageMapper.java:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gm.seata.openfeign.entity.Storage;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;@Mapper
public interface StorageMapper extends BaseMapper<Storage> {@Select("SELECT * FROM t_storage WHERE commodity_code = #{commodityCode} limit 1")Storage getStorageByCommodityCode(@Param("commodityCode") String commodityCode);
}
5.4.3.3 Service类
com/gm/seata/openfeign/service/StorageService.java:
import com.gm.seata.openfeign.entity.Storage;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;@LocalTCC
public interface StorageService {/*** 执行资源检查及预业务操作*/// @BusinessActionContextParameter 注解就是将对应的参数放入到 BusinessActionContext 中,将来可以从 BusinessActionContext 中取出对应的参数。@TwoPhaseBusinessAction(name = "storageService", commitMethod = "commit", rollbackMethod = "rollback")Storage prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "commodityCode") String commodityCode, @BusinessActionContextParameter(paramName = "count") Integer count);/*** 全局事物进行提交*/boolean commit(BusinessActionContext actionContext);/*** 全局事务进行回滚*/boolean rollback(BusinessActionContext actionContext);
}
减少重复内容,请参考 5.3.3 部分说明
com/gm/seata/openfeign/service/impl/StorageServiceImpl.java:
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.gm.seata.openfeign.entity.Storage;
import com.gm.seata.openfeign.mapper.StorageMapper;
import com.gm.seata.openfeign.service.StorageService;
import com.gm.seata.openfeign.util.ErrorEnum;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Slf4j
@Service
public class StorageServiceImpl implements StorageService {@AutowiredStorageMapper storageMapper;/*** 扣除商品库存预处理阶段,进行商品库存冻结** @param commodityCode* @param count* @return*/@Transactional(rollbackFor = Exception.class)public Storage prepare(BusinessActionContext actionContext, String commodityCode, Integer count) {Storage storage = storageMapper.getStorageByCommodityCode(commodityCode);if (storage == null) {//throw new RuntimeException("商品不存在");throw new RuntimeException(String.valueOf(ErrorEnum.NO_SUCH_COMMODITY.getCode()));}if (storage.getCount() < count) {//throw new RuntimeException("库存不足,预扣库存失败");throw new RuntimeException(String.valueOf(ErrorEnum.STORAGE_LOW_PREPARE.getCode()));}storage.setFreezeCount(storage.getFreezeCount() + count);storage.setCount(storage.getCount() - count);QueryWrapper query = new QueryWrapper();query.eq("commodity_code", commodityCode);Integer i = storageMapper.update(storage, query);log.info("{} 商品库存冻结 {} 个", commodityCode, count);return storage;}/*** 扣除商品库存提交阶段,进行商品库存扣除** @param actionContext* @return*/@Transactional(rollbackFor = Exception.class)public boolean commit(BusinessActionContext actionContext) {String commodityCode = (String) actionContext.getActionContext("commodityCode");Integer count = (Integer) actionContext.getActionContext("count");Storage storage = storageMapper.getStorageByCommodityCode(commodityCode);if (storage.getFreezeCount() < count) {//throw new RuntimeException("库存不足,扣库存失败");throw new RuntimeException(String.valueOf(ErrorEnum.STORAGE_LOW_COMMIT.getCode()));}storage.setFreezeCount(storage.getFreezeCount() - count);QueryWrapper query = new QueryWrapper();query.eq("commodity_code", commodityCode);int i = storageMapper.update(storage, query);log.info("{} 商品库存扣除 {} 个", commodityCode, count);return i == 1;}/*** 扣除商品库存回滚阶段** @param actionContext* @return*/@Transactional(rollbackFor = Exception.class)public boolean rollback(BusinessActionContext actionContext) {String commodityCode = (String) actionContext.getActionContext("commodityCode");Integer count = (Integer) actionContext.getActionContext("count");Storage storage = storageMapper.getStorageByCommodityCode(commodityCode);if (storage.getFreezeCount() >= count) {storage.setFreezeCount(storage.getFreezeCount() - count);storage.setCount(storage.getCount() + count);QueryWrapper query = new QueryWrapper();query.eq("commodity_code", commodityCode);int i = storageMapper.update(storage, query);log.info("{} 商品释放库存 {} 个", commodityCode, count);return i == 1;}// 说明 prepare 阶段就没有冻结return true;}
}
5.4.3.4 Controller类
com/gm/seata/openfeign/controller/StorageController.java:
import com.gm.seata.openfeign.entity.Storage;
import com.gm.seata.openfeign.service.StorageService;
import com.gm.seata.openfeign.util.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@RestController
public class StorageController {@AutowiredStorageService storageService;/*** 扣除商品库存** @param commodityCode* @param count* @return*/@RequestMapping(value = "deduct", method = RequestMethod.GET)public R<Storage> deduct(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Integer count) {return R.ok(storageService.prepare(null, commodityCode, count));}
}
5.5 order-tcc 搭建
5.5.1 完整依赖
减少重复内容,请参考 5.3.1 部分,自动修改
5.5.2 配置文件
减少重复内容,请参考 5.3.2 部分,自动修改
5.5.3 功能搭建
5.5.3.1 启动类
com/gm/seata/openfeign/OrderTCCApplication.java:
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
@EnableFeignClients("com.gm.seata.openfeign.feign")
public class OrderTCCApplication {public static void main(String[] args) {SpringApplication.run(OrderTCCApplication.class, args);}
}
5.5.3.2 Mapper类
com/gm/seata/openfeign/mapper/OrderMapper.java:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gm.seata.openfeign.entity.Order;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface OrderMapper extends BaseMapper<Order> {}
5.5.3.3 Service类
com/gm/seata/openfeign/service/OrderService.java:
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;@LocalTCC
public interface OrderService {/*** 执行资源检查及预业务操作*/// @BusinessActionContextParameter 注解就是将对应的参数放入到 BusinessActionContext 中,将来可以从 BusinessActionContext 中取出对应的参数。@TwoPhaseBusinessAction(name = "storageService", commitMethod = "commit", rollbackMethod = "rollback")boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "userId") String userId, @BusinessActionContextParameter(paramName = "commodityCode") String commodityCode, @BusinessActionContextParameter(paramName = "count") Integer count);/*** 全局事物进行提交*/boolean commit(BusinessActionContext actionContext);/*** 全局事务进行回滚*/boolean rollback(BusinessActionContext actionContext);
}
减少重复内容,请参考 5.3.3 部分说明
com/gm/seata/openfeign/service/impl/OrderServiceImpl.java:
package com.gm.seata.openfeign.service.impl;import com.gm.seata.openfeign.entity.Account;
import com.gm.seata.openfeign.entity.Order;
import com.gm.seata.openfeign.feign.AccountServiceApi;
import com.gm.seata.openfeign.mapper.OrderMapper;
import com.gm.seata.openfeign.service.OrderService;
import com.gm.seata.openfeign.util.ErrorEnum;
import com.gm.seata.openfeign.util.R;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal;@Slf4j
@Service
public class OrderServiceImpl implements OrderService {@AutowiredOrderMapper orderMapper;@AutowiredAccountServiceApi accountServiceApi;/*** 创建商品订单预处理阶段,扣除账户余额** @param commodityCode* @param count* @return*/@Transactional(rollbackFor = Exception.class)public boolean prepare(BusinessActionContext actionContext, String userId, String commodityCode, Integer count) {//先去扣款,假设每个产品100块钱R<Account> accountResult = null;try {accountResult = accountServiceApi.deduct(userId, new BigDecimal(count * 100.0));} catch (Exception e) {e.printStackTrace();// 远程方法调用失败throw new RuntimeException(e.getMessage());}log.info("{} 用户账户信息 {}", userId, accountResult.getData());log.info("{} 用户购买的 {} 商品共计 {} 件,预下单成功", userId, commodityCode, count);return true;}/*** 创建商品订单提交阶段,创建订单记录** @param actionContext* @return*/@Transactional(rollbackFor = Exception.class)public boolean commit(BusinessActionContext actionContext) {String userId = (String) actionContext.getActionContext("userId");String commodityCode = (String) actionContext.getActionContext("commodityCode");Integer count = (Integer) actionContext.getActionContext("count");Order order = new Order();order.setCount(count);order.setCommodityCode(commodityCode);order.setUserId(userId);order.setMoney(new BigDecimal(count * 100.0));int i = orderMapper.insert(order);log.info("{} 用户购买的 {} 商品共计 {} 件,下单成功", userId, commodityCode, count);return i == 1;}/*** 创建商品订单回滚阶段,暂无业务操作** @param actionContext* @return*/@Transactional(rollbackFor = Exception.class)public boolean rollback(BusinessActionContext actionContext) {String userId = (String) actionContext.getActionContext("userId");String commodityCode = (String) actionContext.getActionContext("commodityCode");Integer count = (Integer) actionContext.getActionContext("count");log.info("{} 用户购买的 {} 商品共计 {} 件,订单回滚成功", userId, commodityCode, count);return true;}}
5.5.3.4 Controller类
com/gm/seata/openfeign/controller/OrderController.java:
import com.gm.seata.openfeign.service.OrderService;
import com.gm.seata.openfeign.util.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@RestController
public class OrderController {@AutowiredOrderService orderService;/*** 创建订单** @param userId* @param commodityCode* @param count* @return*/@RequestMapping(value = "createOrder", method = RequestMethod.GET)public R<Boolean> createOrder(@RequestParam("userId") String userId, @RequestParam("commodityCode") String commodityCode, @RequestParam("count") Integer count) {return R.ok(orderService.prepare(null, userId, commodityCode, count));}
}
5.6 business-tcc 搭建
5.6.1 完整依赖
减少重复内容,请参考 5.3.1 部分,自动修改
5.6.2 配置文件
减少重复内容,请参考 5.3.2 部分,自动修改
5.6.3 功能搭建
5.6.3.1 启动类
com/gm/seata/openfeign/BusinessTCCApplication.java:
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
@EnableFeignClients("com.gm.seata.openfeign.feign")
public class BusinessTCCApplication {public static void main(String[] args) {SpringApplication.run(BusinessTCCApplication.class, args);}
}
5.6.3.2 Service类
com/gm/seata/openfeign/service/BusinessService.java:
public interface BusinessService {void buy(String userId, String commodityCode, Integer count);
}
com/gm/seata/openfeign/service/impl/BusinessServiceImpl.java:
import com.gm.seata.openfeign.feign.OrderServiceApi;
import com.gm.seata.openfeign.feign.StorageServiceApi;
import com.gm.seata.openfeign.service.BusinessService;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Slf4j
@Service
public class BusinessServiceImpl implements BusinessService {@AutowiredStorageServiceApi storageServiceApi;@AutowiredOrderServiceApi orderServiceApi;/*** 下单购买,先扣除库存再创建订单** @param userId* @param commodityCode* @param count*/@GlobalTransactionalpublic void buy(String userId, String commodityCode, Integer count) {String xid = RootContext.getXID();log.info("xid={}", xid);/*** 扣除库存*/// 只有抛出异常,才能触发GlobalTransactional 的回滚逻辑处理try {storageServiceApi.deduct(commodityCode, count);} catch (Exception e) {throw new RuntimeException(e.getMessage());}/*** 创建订单*/try {orderServiceApi.createOrder(userId, commodityCode, count);} catch (Exception e) {// 远程方法调用失败throw new RuntimeException(e.getMessage());}}
}
5.6.3.4 Controller类
com/gm/seata/openfeign/controller/OrderController.java:
import com.gm.seata.openfeign.service.BusinessService;
import com.gm.seata.openfeign.util.ErrorEnum;
import com.gm.seata.openfeign.util.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@RestController
public class BusinessController {@AutowiredBusinessService businessService;/*** 商品下单购买** @param userId* @param commodityCode* @param count* @return*/@RequestMapping(value = "buy", method = RequestMethod.GET)public R<String> buy(@RequestParam("userId") String userId, @RequestParam("commodityCode") String commodityCode, @RequestParam("count") Integer count) {try {businessService.buy(userId, commodityCode, count);return R.ok("下单成功", "");} catch (Exception e) {e.printStackTrace();int code = Integer.parseInt(e.getMessage());return R.restResult("下单失败", code, ErrorEnum.getEnumByCode(code).getTitle());}}
}
六、示例说明
由第四章节可知:账户余额500,库存6,每件商品单价100元。
请求地址:http://127.0.0.1:4000/buy?userId=user1&count=2&commodityCode=iphone
每请求一次,扣除余额200元,扣除库存2个,已知可正常下单2次

第三次请求因余额不足,进行全局事务回滚

以下为仓储服务回滚信息,释放已冻结的2个库存

相关文章:
Seata TCC 模式理论学习、生产级使用示例搭建及注意事项 | Spring Cloud55
一、前言 通过以下系列章节: docker-compose 实现Seata Server高可用部署 | Spring Cloud 51 Seata AT 模式理论学习、事务隔离及部分源码解析 | Spring Cloud 52 Spring Boot集成Seata利用AT模式分布式事务示例 | Spring Cloud 53 Seata XA 模式理论学习、使用…...
一文详解:Vue3中使用Vue Router
目录 安装和配置Vue Router安装Vue Router配置Vue Router Vue Router的基本概念Vue Router 的配置项介绍routes中的配置项介绍 路由跳转使用 router-link组件使用router.push函数 路由传参动态路由嵌套路由命名路由路由守卫全局路由守卫路由独享守卫 路由懒加载使用import()方式…...
C++开发—远程控制
C开发—远程控制 一,准备二,安装版本控制工具1,安装gitforwindows2,安装乌龟git1,安装乌龟git应用2,安装乌龟git对应的语言包 3,设置Visual Studio的git插件4,创建git项目 三&#x…...
【Python基础】Python数据容器(集合)
文章目录 数据容器:set(集合)集合的定义集合的常用操作-修改(1)添加新元素(2)移除元素(3)从集合中随机取出元素(4)清空集合(5)取出 两个集合的差集(6)消除 两个集合的差集(7)两个集合 合并(8)统计集合元素数量len()(9)集合的遍历 集合的特点 …...
高通 Camera HAL3:集成camxoverridesettings.txt到整机版本
camxoverridesettings.txt 是高通提供给开发者临时进行CAMX、CHI-CDK功能调试的一种方式,通过配置各种变量值然后写入到该文件,能控制Log打印、参数配置、数据dump等多种功能 这个文件需要集成在设备目录的vendor/etc/camera/里 因为camxoverridesetti…...
PHP面试题大全
一 、PHP基础部分 1、PHP语言的一大优势是跨平台,什么是跨平台? PHP的运行环境最优搭配为ApacheMySQLPHP,此运行环境可以在不同操作系统(例如windows、Linux等)上配置,不受操作系统的限制,所以…...
Linux发送接收邮件
目录 一、实验 1.linux用户发送给linux中的其它用户 2.linux用户发送给外网用户 一、实验 1.linux用户发送给linux中的其它用户 (1)使用命令 yum install -y sendmail 安装sendmail软件 (2)使用yum install -y mailx 安装 mail…...
SpringBoot-【回顾】
第一个SpringBoot程序 自动装配原理 Springboot的自动装配实际上就是为了从Spring.factories文件中获取到对应的需要进行自动装配的类,并生成相应的Bean对象,然后将它们交给Spring容器来帮我们进行管理 启动器:以starter为标记 EnableAuto…...
Python模拟试卷2023(1)
模拟试卷(1) 一、简答题 (共8题,100分) 1、已知有列表lst[54,36,75,28,50],请完成一下操作: 1、在列表尾部插入元素42 2、在元素28前面插入66 3、删除并输出28 4、将列表按降序排序 5、清空整个列表 lst[54,3…...
常量接口 vs 常量类 vs 枚举区别
把常量定义在接口里与类里都能通过编译,那2者到底有什么区别呢? 那个更合理? 常量接口 public interface ConstInterfaceA {public static final String CONST_A "aa";public static final String CONST_C "cc"; } 存在…...
第二章 模态命题:必然、可能
第二章 模态命题:必然、可能 第一节 模态命题-句式转换-逻辑转换 题-模态命题-句式转换-逻辑转换:①不一定不可能;②不一定可能不未必。 1.唐代韩愈在《师说》中指出:“孔子曰:三人行,则必有我师。是故…...
Selenium 必了解—如何测试REST API
目录 前言: Web UI测试存在的问题: REST API测试: 依赖包 程序示例: 1-获取联系人 2-GET Request: 3-POST Request: 4- 编辑请求 5- 删除请求 前言: Selenium WebDriver 可以用于测试 Web 应用的…...
pytorch安装老版本
比如1.7.1, cuda 10.1 pip install torch1.7.1cu101 -f https://download.pytorch.org/whl/torch_stable.html官网查看有哪些可以装的: https://download.pytorch.org/whl/torch_stable.html...
怎么自学电脑编程
首要之首:不要急于选择一种语言 新手们有一个常见的错误就是犹豫于判断哪种编程语言是做好的、最该先学的。 我们有很多的选择,但你不能说那种语言最好。 我们应该理解:说到底,什么语言并不重要。 重要的是理解数据结构、控制逻辑…...
【华为OD统一考试B卷 | 100分】斗地主之顺子(C++ Java JavaScript Python)
文章目录 题目描述输入描述输出描述用例C++JavajavaScriptpython题目描述 在斗地主扑克牌游戏中, 扑克牌由小到大的顺序为:3,4,5,6,7,8,9,10,J,Q,K,A,2,玩家可以出的扑克牌阵型有:单张、对子、顺子、飞机、炸弹等。 其中顺子的出牌规则为:由至少5张由小到大连续递增的扑…...
案例39:基于Java办公自动化管理系统开题报告设计
博主介绍:✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取源码联系🍅 👇🏻 精彩专…...
基于山景BP10128音频处理器高通滤波器算法设计
+ hezkz17进数字音频答疑 山景BP10128音频处理器是一款高性能的数字信号处理器,专门用于音频信号的处理和增强。它采用先进的数字信号处理技术和算法,能够对音频信号进行实时处理,并且具有高效、稳定、可靠等特点。 该处理器具有以下主要功能: 均衡器:支持低音、中音、…...
docker搭建本地私有仓库
一、搭建本地私有仓库 有时候使用Docker Hub这样的公共仓库可能不方便,这种情况下用户可以使用registry创建一个本地仓库供私人使用,这点跟Maven的管理类似。 使用私有仓库有许多优点: 1)节省网络带宽,针对于每个镜像…...
Asp.net某店POS积分管理系统-清除履历表、日志表、月购买额(源代码+论文)
大型百货店作为日常生活中不可缺少的一部分,给人们的生活提供了很大的方便。而为这样一个庞大而复杂的购物平台,提供一套完备的管理系统支持是很必要的。在现代销售行业中,会员制、积分管理、代金消费的概念已经越来越普及。为了吸引更多消费者,加大销售企业的竞争力。就需…...
Baumer工业相机堡盟工业相机如何使用BGAPISDK的相机图像时间戳计算运行时间以及时间差(C#)
Baumer工业相机堡盟工业相机如何使用BGAPISDK的相机图像时间戳计算运行时间以及时间差(C#) Baumer工业相机Baumer工业相机BGAPI SDK和图像时间戳的技术背景Baumer工业相机使用BGAPISDK控制相机数据流的方式1.引用合适的类文件2.使用BGAPISDK获取时间戳的…...
【杂谈】-递归进化:人工智能的自我改进与监管挑战
递归进化:人工智能的自我改进与监管挑战 文章目录 递归进化:人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管?3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...
CMake基础:构建流程详解
目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...
【Go语言基础【13】】函数、闭包、方法
文章目录 零、概述一、函数基础1、函数基础概念2、参数传递机制3、返回值特性3.1. 多返回值3.2. 命名返回值3.3. 错误处理 二、函数类型与高阶函数1. 函数类型定义2. 高阶函数(函数作为参数、返回值) 三、匿名函数与闭包1. 匿名函数(Lambda函…...
springboot整合VUE之在线教育管理系统简介
可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生,小白用户,想学习知识的 有点基础,想要通过项…...
基于SpringBoot在线拍卖系统的设计和实现
摘 要 随着社会的发展,社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 在线拍卖系统,主要的模块包括管理员;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单…...
MFC 抛体运动模拟:常见问题解决与界面美化
在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...
LRU 缓存机制详解与实现(Java版) + 力扣解决
📌 LRU 缓存机制详解与实现(Java版) 一、📖 问题背景 在日常开发中,我们经常会使用 缓存(Cache) 来提升性能。但由于内存有限,缓存不可能无限增长,于是需要策略决定&am…...
比较数据迁移后MySQL数据库和OceanBase数据仓库中的表
设计一个MySQL数据库和OceanBase数据仓库的表数据比较的详细程序流程,两张表是相同的结构,都有整型主键id字段,需要每次从数据库分批取得2000条数据,用于比较,比较操作的同时可以再取2000条数据,等上一次比较完成之后,开始比较,直到比较完所有的数据。比较操作需要比较…...
libfmt: 现代C++的格式化工具库介绍与酷炫功能
libfmt: 现代C的格式化工具库介绍与酷炫功能 libfmt 是一个开源的C格式化库,提供了高效、安全的文本格式化功能,是C20中引入的std::format的基础实现。它比传统的printf和iostream更安全、更灵活、性能更好。 基本介绍 主要特点 类型安全:…...
在RK3588上搭建ROS1环境:创建节点与数据可视化实战指南
在RK3588上搭建ROS1环境:创建节点与数据可视化实战指南 背景介绍完整操作步骤1. 创建Docker容器环境2. 验证GUI显示功能3. 安装ROS Noetic4. 配置环境变量5. 创建ROS节点(小球运动模拟)6. 配置RVIZ默认视图7. 创建启动脚本8. 运行可视化系统效果展示与交互技术解析ROS节点通…...
