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

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事务模式的理论、使用有了深入的了解,今天继续对SeataTCC事务模式进行理论学习;并区别与官网,我们利用openfeign进行生产级示例搭建,降低入门难度。

理论部分来自Seata官网:http://seata.io/zh-cn/docs/dev/mode/tcc-mode.html

二、整体机制

回顾前面章节的学习:一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:

  • 一阶段 prepare 行为
  • 二阶段 commitrollback 行为

在这里插入图片描述
上面这个流程中,一共涉及到了三个方法,preparecommit 以及 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 注解所在的方法,第二阶段则分为两种情况,提交或者回滚,分别对应了两个不同的方法,commitMethodrollbackMethod 就指明了相应的方法。
  • 一阶段的 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

一、前言 通过以下系列章节&#xff1a; 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开发—远程控制 一&#xff0c;准备二&#xff0c;安装版本控制工具1&#xff0c;安装gitforwindows2&#xff0c;安装乌龟git1&#xff0c;安装乌龟git应用2&#xff0c;安装乌龟git对应的语言包 3&#xff0c;设置Visual Studio的git插件4&#xff0c;创建git项目 三&#x…...

【Python基础】Python数据容器(集合)

文章目录 数据容器&#xff1a;set&#xff08;集合&#xff09;集合的定义集合的常用操作-修改(1)添加新元素(2)移除元素(3)从集合中随机取出元素(4)清空集合(5)取出 两个集合的差集(6)消除 两个集合的差集(7)两个集合 合并(8)统计集合元素数量len()(9)集合的遍历 集合的特点 …...

高通 Camera HAL3:集成camxoverridesettings.txt到整机版本

camxoverridesettings.txt 是高通提供给开发者临时进行CAMX、CHI-CDK功能调试的一种方式&#xff0c;通过配置各种变量值然后写入到该文件&#xff0c;能控制Log打印、参数配置、数据dump等多种功能 这个文件需要集成在设备目录的vendor/etc/camera/里 因为camxoverridesetti…...

PHP面试题大全

一 、PHP基础部分 1、PHP语言的一大优势是跨平台&#xff0c;什么是跨平台&#xff1f; PHP的运行环境最优搭配为ApacheMySQLPHP&#xff0c;此运行环境可以在不同操作系统&#xff08;例如windows、Linux等&#xff09;上配置&#xff0c;不受操作系统的限制&#xff0c;所以…...

Linux发送接收邮件

目录 一、实验 1.linux用户发送给linux中的其它用户 2.linux用户发送给外网用户 一、实验 1.linux用户发送给linux中的其它用户 &#xff08;1&#xff09;使用命令 yum install -y sendmail 安装sendmail软件 &#xff08;2&#xff09;使用yum install -y mailx 安装 mail…...

SpringBoot-【回顾】

第一个SpringBoot程序 自动装配原理 Springboot的自动装配实际上就是为了从Spring.factories文件中获取到对应的需要进行自动装配的类&#xff0c;并生成相应的Bean对象&#xff0c;然后将它们交给Spring容器来帮我们进行管理 启动器&#xff1a;以starter为标记 EnableAuto…...

Python模拟试卷2023(1)

模拟试卷(1) 一、简答题 &#xff08;共8题&#xff0c;100分&#xff09; 1、已知有列表lst[54,36,75,28,50]&#xff0c;请完成一下操作&#xff1a; 1、在列表尾部插入元素42 2、在元素28前面插入66 3、删除并输出28 4、将列表按降序排序 5、清空整个列表 lst[54,3…...

常量接口 vs 常量类 vs 枚举区别

把常量定义在接口里与类里都能通过编译&#xff0c;那2者到底有什么区别呢&#xff1f; 那个更合理&#xff1f; 常量接口 public interface ConstInterfaceA {public static final String CONST_A "aa";public static final String CONST_C "cc"; } 存在…...

第二章 模态命题:必然、可能

第二章 模态命题&#xff1a;必然、可能 第一节 模态命题-句式转换-逻辑转换 题-模态命题-句式转换-逻辑转换&#xff1a;①不一定不可能&#xff1b;②不一定可能不未必。 1.唐代韩愈在《师说》中指出&#xff1a;“孔子曰&#xff1a;三人行&#xff0c;则必有我师。是故…...

Selenium 必了解—如何测试REST API

目录 前言&#xff1a; Web UI测试存在的问题&#xff1a; REST API测试&#xff1a; 依赖包 程序示例&#xff1a; 1-获取联系人 2-GET Request&#xff1a; 3-POST Request: 4- 编辑请求 5- 删除请求 前言&#xff1a; Selenium WebDriver 可以用于测试 Web 应用的…...

pytorch安装老版本

比如1.7.1&#xff0c; cuda 10.1 pip install torch1.7.1cu101 -f https://download.pytorch.org/whl/torch_stable.html官网查看有哪些可以装的&#xff1a; https://download.pytorch.org/whl/torch_stable.html...

怎么自学电脑编程

首要之首&#xff1a;不要急于选择一种语言 新手们有一个常见的错误就是犹豫于判断哪种编程语言是做好的、最该先学的。 我们有很多的选择&#xff0c;但你不能说那种语言最好。 我们应该理解&#xff1a;说到底&#xff0c;什么语言并不重要。 重要的是理解数据结构、控制逻辑…...

【华为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办公自动化管理系统开题报告设计

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…...

基于山景BP10128音频处理器高通滤波器算法设计

+ hezkz17进数字音频答疑 山景BP10128音频处理器是一款高性能的数字信号处理器,专门用于音频信号的处理和增强。它采用先进的数字信号处理技术和算法,能够对音频信号进行实时处理,并且具有高效、稳定、可靠等特点。 该处理器具有以下主要功能: 均衡器:支持低音、中音、…...

docker搭建本地私有仓库

一、搭建本地私有仓库 有时候使用Docker Hub这样的公共仓库可能不方便&#xff0c;这种情况下用户可以使用registry创建一个本地仓库供私人使用&#xff0c;这点跟Maven的管理类似。 使用私有仓库有许多优点&#xff1a; 1&#xff09;节省网络带宽&#xff0c;针对于每个镜像…...

Asp.net某店POS积分管理系统-清除履历表、日志表、月购买额(源代码+论文)

大型百货店作为日常生活中不可缺少的一部分,给人们的生活提供了很大的方便。而为这样一个庞大而复杂的购物平台,提供一套完备的管理系统支持是很必要的。在现代销售行业中,会员制、积分管理、代金消费的概念已经越来越普及。为了吸引更多消费者,加大销售企业的竞争力。就需…...

Baumer工业相机堡盟工业相机如何使用BGAPISDK的相机图像时间戳计算运行时间以及时间差(C#)

Baumer工业相机堡盟工业相机如何使用BGAPISDK的相机图像时间戳计算运行时间以及时间差&#xff08;C#&#xff09; Baumer工业相机Baumer工业相机BGAPI SDK和图像时间戳的技术背景Baumer工业相机使用BGAPISDK控制相机数据流的方式1.引用合适的类文件2.使用BGAPISDK获取时间戳的…...

python:消除已安装库在import导入时出现红线问题

问题 在pycharm中&#xff0c;对于已经安装的库文件&#xff0c;在进行import导入时出现红线&#xff0c;不影响运行&#xff0c; 简单有效的消除红线的方法。 解决办法 在工程目录中的程序可以采用Mark directory - Source Root方法。 对于安装的第三方库文件环境不在本工程…...

关闭nginx容器之后,再次启动,原来宿主机映射的端口失效的问题解决

最近用containerd在部署nginx的时候&#xff0c;发生了一个比较诡异的问题&#xff0c;当笔者通过nerdctl stop把原来的nginx容器关闭&#xff0c;然后再通过nerdctl run启动一个新的nginx容器的时候&#xff0c;把原来的宿主机端口映射到这个新容器上&#xff0c;但新启动的容…...

【小沐学Python】Python实现在线电子书(MkDocs + readthedocs + github + Markdown)

文章目录 1、简介2、安装3、创建新项目4、添加页面5、编辑导航页6、设置主题7、更改图标图标8、构建网站9、部署9.1 准备github项目9.2 注册登录Read the Docs9.3 导入github项目到 Read the Docs 10、Markdown语法10.1 横线10.2 标题10.3 段落10.4 文字高亮10.5 换行10.6 斜体…...

Python 中的短路评估

文章目录 Python 中的逻辑运算符or (或)运算符AND 运算符 什么是短路在 Python 中使用 AND 运算符进行短路在 Python 中使用 OR 运算符进行短路 本文是关于使用逻辑运算符在 Python 中显示短路行为。 Python 中的逻辑运算符 or (或)运算符 OR&#xff1a;两个操作数均使用 Py…...

LVGL源码分析(1):lv_ll链表的实现

在LVGL中难免需要用到链表&#xff1a;group中的对象需要用链表来存储&#xff0c;这样可以切换对象的焦点&#xff1b;再比如LVGL内部的定时器&#xff0c;多个定时器也是用链表进行存储的。这篇文章就来分析一下LVGL中链表的源码。 文章目录 1 链表结构体2 插入元素源码分析…...

js判断数据类型的几种方法及其局限性(typeof, instanceof, Object.prototype.toString.call())

​ js中判断了类型的方法有很多, 这篇文章主要来说一下常用的几种判断类型的方法,以及使用: 每个方法都各有优缺点,在日常使用的时候请结合这些优缺点进行斟酌: 1. 使用typeof判断数据类型 javaScript中typeof可以判断以下类型: undefined: 未定义的变量或者值 boolean: 布…...

【MySQL】一文带你掌握聚合查询和联合查询

文章目录 1. 聚合函数1.1 COUNT1.2 SUM1.3 AVG1.4 MAX&#xff0c;MIN 2. GROUP BY3. HAVING4. 联合查询4.1 内连接4.2 外连接4.3 自连接4.4 子连接 5.合并查询5.1 UNION5.2 UNION ALL 1. 聚合函数 概念&#xff1a; 聚合函数是一种用于处理数据集合的函数&#xff0c;它将多个…...

初步了解JVM

JVM 整体组成部分 类加载器 类加载过程 加载&#xff1a;使用IO读取字节码文件&#xff0c;转换并存储&#xff0c;为每个类创建一个Class对象&#xff0c;存储在方法区中 链接&#xff08;验证&#xff0c;准备&#xff0c;解析&#xff09; ​ 验证:对字节码文件格式进…...

嘀嗒陪诊小程序v1.0.8+小程序前端

嘀嗒陪诊小程序功能相对简单&#xff0c;后台也简捷&#xff0c;如果只是做个陪诊服务的小程序也基本能满足了&#xff0c;整体测试了下海参崴发现BUG&#xff0c;小程序端也能正常为使用&#xff0c;唯一用户授权接口是老的。 应用背景&#xff1a;人口老龄化少子化&#xff…...

Java中线程的生命周期

Java中线程的生命周期 Java中线程的声明周期与os中线程的生命周期不太一样&#xff0c;java中线程有6个状态&#xff0c;见下&#xff1a; NEW: 初始状态&#xff0c;线程被创建出来但没有被调用 start() 。RUNNABLE: 运行状态&#xff0c;线程被调用了 start()等待运行的状态…...

做货源的网站/谷歌搜索排名

第一步先下载源码&#xff0c;解压后 ./dist/configure --enable-cxx编译&#xff0c;然后make, make install--enable-cxx To build the Berkeley DB C API, enter --enable-cxx as an argument to configure. 默认的安装路径是&#xff1a; /usr/local/BerkeleyDB.6.1/ 代码如…...

做网站需要注意的问题/免费独立站自建站网站

Combiners的作用&#xff1a;每一个map可能会产生大量的输出&#xff0c;combiner的作用就是在map端对输出先做一次合并&#xff0c;以减少传输到reducer的数据量&#xff0c;1&#xff09;combiner最基本是实现本地key的聚合&#xff0c;对map输出的key排序&#xff0c;value进…...

2018政府网站建设 会议/站长之家alexa排名

CSS3 Shadows浏览器支持情况 text-shadow 和 box-shadow 这两个属性在主流现代浏览器上得到了很好的支持( > Chrome 4.0, > Firefox 3.5, > Safari 4.0, > Opera 9.6, > IE10)。 text-shadow 和 box-shadow 的不同之处&#xff1a; box-shadow语法&#xff1a; …...

电商网站建设的核心是什么/市场营销策划方案模板

爬虫的构建流程 HTTP Error 418: 这个错误是对方发现你是爬虫。 主要是分为四步&#xff1a;准备工作、获取数据、解析内容、保存数据 一、准备工作&#xff1a; 一般python程序第一行须加入 #-*_coding:utf-8-*-或者#codingutf-8 这样可以在代码种包含中文在python中&…...

重庆做网站开发的集中/seo排名培训学校

参考林海峰老师的配置教程 http://blog.51cto.com/9161406/1839667转载于:https://www.cnblogs.com/luhuajun/p/7379938.html...

育儿网网站开发/营销推广方法有哪些

cp命令用来复制文件或者目录&#xff0c;为linux常用命令之一。一般情况下&#xff0c;shell会设置一个别名&#xff0c;在命令行下复制文件时&#xff0c;如果目标文件已经存在&#xff0c;就会询问是否覆盖&#xff0c;不管是否使用-i参数。在shell脚本中执行cp时&#xff0c…...