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获取时间戳的…...

Python实现prophet 理论及参数优化
文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候,写过一篇简单实现,后期随着对该模型的深入研究,本次记录涉及到prophet 的公式以及参数调优,从公式可以更直观…...
使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装
以下是基于 vant-ui(适配 Vue2 版本 )实现截图中照片上传预览、删除功能,并封装成可复用组件的完整代码,包含样式和逻辑实现,可直接在 Vue2 项目中使用: 1. 封装的图片上传组件 ImageUploader.vue <te…...

如何将联系人从 iPhone 转移到 Android
从 iPhone 换到 Android 手机时,你可能需要保留重要的数据,例如通讯录。好在,将通讯录从 iPhone 转移到 Android 手机非常简单,你可以从本文中学习 6 种可靠的方法,确保随时保持连接,不错过任何信息。 第 1…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
一、开发环境准备 工具安装: 下载安装DevEco Studio 4.0(支持HarmonyOS 5)配置HarmonyOS SDK 5.0确保Node.js版本≥14 项目初始化: ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...

IoT/HCIP实验-3/LiteOS操作系统内核实验(任务、内存、信号量、CMSIS..)
文章目录 概述HelloWorld 工程C/C配置编译器主配置Makefile脚本烧录器主配置运行结果程序调用栈 任务管理实验实验结果osal 系统适配层osal_task_create 其他实验实验源码内存管理实验互斥锁实验信号量实验 CMISIS接口实验还是得JlINKCMSIS 简介LiteOS->CMSIS任务间消息交互…...
今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存
文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...
鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南
1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发,使用DevEco Studio作为开发工具,采用Java语言实现,包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...
代理篇12|深入理解 Vite中的Proxy接口代理配置
在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...

安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)
船舶制造装配管理现状:装配工作依赖人工经验,装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书,但在实际执行中,工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...
C++.OpenGL (14/64)多光源(Multiple Lights)
多光源(Multiple Lights) 多光源渲染技术概览 #mermaid-svg-3L5e5gGn76TNh7Lq {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-3L5e5gGn76TNh7Lq .error-icon{fill:#552222;}#mermaid-svg-3L5e5gGn76TNh7Lq .erro…...