SpringCloudAlibabaSeate处理分布式事务

SpringCloudAlibabaSeate处理分布式事务

1、部分面试题

  • 微服务boot/cloud做的项目,你不可能只有一个数据库吧?那么多个数据库之间如何处理分布式事务的?

    一个场景:在订单支付成功后,交易中心会调用订单中心的服务把订单状态更新,并调用物流中心的服务通知商品发货,同时还要调用积分中心的服务为用户增加相应的积分。如何保障分布式事务一致性,成为了确保订单业务稳定运行的核心诉求之一。

    image-20240403082438070

  • 阿里巴巴的Seate-AT模式如何做到对业务的无侵入?

  • 对于分布式事务问题,你知道的解决方案有哪些?

    1. 2PC(两阶段提交);
    2. 3PC(三阶段提交);
    3. TCC方案,TCC(Try-Confirm-Cancel)又被称补偿事务,类似2PC的柔性分布式解决方案,2PC改良版;
    4. LocalMessage本地消息表;
    5. 独立消息微服务+RabbitMQ/KafKa组件,实现可靠消息最终一致性方案;
    6. 最大努力通知方案;

​ 以上的面试题都指向了一个问题,一次业务操作需要跨多个数据源或者需要跨多个系统进行远程调用,就会产生分布式业务问题,但是关系型数据库提供的能力是基于单机事务的,一旦遇到了分布式事务场景,就需要通过更多其他技术手段来解决。

  • 分布式事务之前,是单机单库没这个问题,但是表结构关系从1:1->1:N->N:N;

  • 分布式事务之后

    单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。

    此时每个服务自己内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。

    image-20240403084053265

2、Seata简介

2.1、基本简介

官网:Apache Seata

GitHub:GitHub

​ +++++++++Seata(Simple Extensible Autonomous Transaction Architecture)是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。简单可扩展自治事务框架。

image-20240403091441724

2.2、各事务模式

2.2.1、Seata AT 模式(重点使用这个)

​ AT 模式是 Seata 创新的一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层,我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等。

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

2.2.2、Seata TCC 模式

​ TCC 模式是 Seata 支持的一种由业务方细粒度控制的侵入式分布式事务解决方案,是继 AT 模式后第二种支持的事务模式,最早由蚂蚁金服贡献。其分布式事务模型直接作用于服务层,不依赖底层数据库,可以灵活选择业务资源的锁定粒度,减少资源锁持有时间,可扩展性好,可以说是为独立部署的 SOA 服务而设计的。

image-20240403135105953

2.2.3、Seata Saga 模式

​ Saga 模式是 SEATA 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

image-20240403135249263

2.2.4、Seata XA 模式

​ XA 模式是从 1.2 版本支持的事务模式。XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准。Seata XA 模式是利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式。

image-20240403135335174

3、Seata工作流程简介

​ 整个分布式事务的管理,就是全局事务ID的传递和变更,要让开发者无感知。

image-20240403105942223

Seata对分布式事务的协调和控制就是一个XID加3个概念(TC+TM+RM)

  • ​ XID是全局事务的唯一标识,它可以在服务的调用链路中传递,绑定到事物的上下文中;

  • TC (Transaction Coordinator)-事务协调者
    它就是意义上的Seata,维护全局和分支事务的状态,驱动全局事务提交或回滚。

  • TM (Transaction Manager)-事务管理器

    标注全局@GlobalTransactional启动入口动作的微服务模块(比如订单模块),它是事务的发起者,负责定义全局事务的范围,并根据TC维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议。

  • RM(Resource Manager)-资源管理器(就是mysql数据库本身,可以是多个RM)
    管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

小总结

三个组件相互协作,TC以Seata 服务器(Server)形式独立部署,TM和RM则是以Seata Client的形式集成在微服务中运行。

image-20240403111744540

  1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;

  2. XID 在微服务调用链路的上下文中传播;

  3. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;

  4. TM 向 TC 发起针对 XID 的全局提交或回滚决议;

  5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

4、Seata-Server2.0.0安装

官网2.0.0版本的下载地址:2.0.0.zip (github.com)

GitHub下载地址:seata-server-2.0.0.zip (github.com)

Seata参数配置详情:参数配置 | Apache Seata

image-20240403122131548

4.1、mysql8.0数据库建库建表

直接拿到官网的mysql.sql文件mysql.sql (github.com)

先创建数据库

CREATE DATABASE seata;
USE seata;

然后再执行sql创建表

CREATE TABLE IF NOT EXISTS `global_table`
(`xid`                       VARCHAR(128) NOT NULL,`transaction_id`            BIGINT,`status`                    TINYINT      NOT NULL,`application_id`            VARCHAR(32),`transaction_service_group` VARCHAR(32),`transaction_name`          VARCHAR(128),`timeout`                   INT,`begin_time`                BIGINT,`application_data`          VARCHAR(2000),`gmt_create`                DATETIME,`gmt_modified`              DATETIME,PRIMARY KEY (`xid`),KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4;-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(`branch_id`         BIGINT       NOT NULL,`xid`               VARCHAR(128) NOT NULL,`transaction_id`    BIGINT,`resource_group_id` VARCHAR(32),`resource_id`       VARCHAR(256),`branch_type`       VARCHAR(8),`status`            TINYINT,`client_id`         VARCHAR(64),`application_data`  VARCHAR(2000),`gmt_create`        DATETIME(6),`gmt_modified`      DATETIME(6),PRIMARY KEY (`branch_id`),KEY `idx_xid` (`xid`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4;-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(`row_key`        VARCHAR(128) NOT NULL,`xid`            VARCHAR(128),`transaction_id` BIGINT,`branch_id`      BIGINT       NOT NULL,`resource_id`    VARCHAR(256),`table_name`     VARCHAR(32),`pk`             VARCHAR(36),`status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',`gmt_create`     DATETIME,`gmt_modified`   DATETIME,PRIMARY KEY (`row_key`),KEY `idx_status` (`status`),KEY `idx_branch_id` (`branch_id`),KEY `idx_xid` (`xid`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4;CREATE TABLE IF NOT EXISTS `distributed_lock`
(`lock_key`       CHAR(20) NOT NULL,`lock_value`     VARCHAR(20) NOT NULL,`expire`         BIGINT,primary key (`lock_key`)
) ENGINE = InnoDBDEFAULT CHARSET = utf8mb4;INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);

4.2、更改配置

修改seata-server-2.0.0\conf\application.yml配置文件,修改前要备份一下出厂的配置文件。

image-20240403124946568

我们先看一下自带的配置模板,看一下官方给的配置样例,我们对着改自己的就好。

server:port: 7091spring:application:name: seata-serverlogging:config: classpath:logback-spring.xmlfile:path: ${log.home:${user.home}/logs/seata}extend:logstash-appender:destination: 127.0.0.1:4560kafka-appender:bootstrap-servers: 127.0.0.1:9092topic: logback_to_logstashconsole:user:username: seatapassword: seata
seata:config:# support: nacos, consul, apollo, zk, etcd3#服务配置中心选择nacostype: nacos   nacos:server-addr: 127.0.0.1:8848namespace:#后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUPgroup: SEATA_GROUP username: nacospassword: nacosregistry:# support: nacos, eureka, redis, zk, consul, etcd3, sofa#服务注册中心选择nacostype: nacos  nacos:application: seata-serverserver-addr: 127.0.0.1:8848group: SEATA_GROUPnamespace:cluster: defaultusername: nacospassword: nacosstore:# support: file 、 db 、 redis 、 raft#这里就是我们参考官网的mysql.sql文件中要的条件db,使用数据库存储mode: db  db:datasource: druiddb-type: mysql#这里我们是8.0版本的驱动换一下driver-class-name: com.mysql.cj.jdbc.Driver  #这里说明一下,我的MySQL8.0的端口被我改成3307了url: jdbc:mysql://127.0.0.1:3307/seata?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true   user: rootpassword: 123456min-conn: 10max-conn: 100#这里分别对应我们创建的仨表global-table: global_table  branch-table: branch_tablelock-table: lock_tabledistributed-lock-table: distributed_lockquery-limit: 1000max-wait: 5000#  server:#    service-port: 8091 #If not configured, the default is '${server.port} + 1000'security:secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017tokenValidityInMilliseconds: 1800000ignore:urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**

​ 测试先启动Nacos

image-20240403131125595

​ 然后启动seata-server,找到seata的bin目录找到seata-server.bat启动

image-20240403132322699

​ 然后刷新一下nacos服务列表

image-20240403132358422

​ 访问seata首页:localhost:7091,账密都是seata

image-20240403132511295

image-20240403132548514

5、Seata案例实战数据库和表准备

我们要准备订单+库存+账户3个业务数据库准备好

业务说明:

  • 当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存;
  • 再通过远程调用账户服务来扣减用户账户里面的余额;
  • 最后在订单服务中修改订单状态为已完成;

该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。

image-20240403132929719

由于我们使用的是Seata的AT模式,所以在每一个库中都应创建一个undo_log回滚日志表以便在事务失败或者需要回滚时进行回滚。

​ undo_log回滚日志表在官网中也有给出

GitHub地址:incubator-seata/script/client/at/db/mysql.sql at master · apache/incubator-seata · GitHub

-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(`branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',`xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',`context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',`rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',`log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',`log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',`log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

5.1、seata_order:存储订单的数据库

执行SQL,建seata_order库+建t_order表+undo_log表

CREATE DATABASE seata_order;USE seata_order;CREATE TABLE t_order(`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',`product_id` BIGINT(11)DEFAULT NULL COMMENT '产品id',`count` INT(11) DEFAULT NULL COMMENT '数量',`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',`status` INT(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结')ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;SELECT * FROM t_order;-- for AT mode you must to init this sql for you business database. the seata server not need it.CREATE TABLE IF NOT EXISTS `undo_log`
(`branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',`xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',`context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',`rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',`log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',`log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',`log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

image-20240403140526886

5.2、seata_storage:存储库存的数据库

执行SQL,建seata_storage库+建t_storage表+undo_log表

#storageCREATE DATABASE seata_storage;USE seata_storage;CREATE TABLE t_storage(`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',`total` INT(11) DEFAULT NULL COMMENT '总库存',`used` INT(11) DEFAULT NULL COMMENT '已用库存',`residue` INT(11) DEFAULT NULL COMMENT '剩余库存')ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;INSERT INTO t_storage(`id`,`product_id`,`total`,`used`,`residue`)VALUES('1','1','100','0','100');SELECT * FROM t_storage;-- for AT mode you must to init this sql for you business database. the seata server not need it.CREATE TABLE IF NOT EXISTS `undo_log`(`branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',`xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',`context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',`rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',`log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',`log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',`log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

image-20240403140645307

5.3、seata_account:存诸账户信息的数据库

执行SQL,建seata_account库+建t_account表+undo_log表

#accountcreate database seata_account;use seata_account;CREATE TABLE t_account(`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额',`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度')ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;INSERT INTO t_account(`id`,`user_id`,`total`,`used`,`residue`)VALUES('1','1','1000','0','1000');SELECT * FROM t_account;-- for AT mode you must to init this sql for you business database. the seata server not need it.CREATE TABLE IF NOT EXISTS `undo_log`(`branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',`xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',`context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',`rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',`log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',`log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',`log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

image-20240403140813635

6、微服务实现

下订单–>减库存–>扣余额->改订单状态

6.1、使用MyBatis一键生成dao层

修改config.properties

# seata_order
jdbc.driverClass = com.mysql.cj.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3307/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
jdbc.user = root
jdbc.password =123456

修改generatorConfig.xml

<!--        <table tableName="t_pay" domainObjectName="Pay">-->
<!--            <generatedKey column="id" sqlStatement="JDBC"/>-->
<!--        </table>--><!--  seata_order --><table tableName="t_order" domainObjectName="Order"><generatedKey column="id" sqlStatement="JDBC"/></table>

刷新maven中mybatis插件

image-20240403145658913

cloud-api-commons新增库存和账户两个Feign接口

StorageFeignApi

@FeignClient(value = "seata-storage-service")  //seata-storage-service现在还没有一会建
public interface StorageFeignApi {//扣减库存@PostMapping(value = "/storage/decrease")ResultData decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);}

AccountFeignApi

@FeignClient(value = "seata-account-service")
public interface AccountFeignApi {//扣减账户余额@PostMapping("/account/decrease")ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}

6.2、新建订单Order微服务seata-order-service2001

导入相关依赖

 <dependencies><!-- nacos --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--alibaba-seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId></dependency><!--openfeign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--loadbalancer--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency><!--cloud-api-commons--><dependency><groupId>com.zm.cloud</groupId><artifactId>cloud-api-commons</artifactId><version>1.0-SNAPSHOT</version></dependency><!--web + actuator--><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><!--SpringBoot集成druid连接池--><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId></dependency><!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html --><dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId></dependency><!--mybatis和springboot整合--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId></dependency><!--Mysql数据库驱动8 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!--persistence--><dependency><groupId>javax.persistence</groupId><artifactId>persistence-api</artifactId></dependency><!--通用Mapper4--><dependency><groupId>tk.mybatis</groupId><artifactId>mapper</artifactId></dependency><!--hutool--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId></dependency><!-- fastjson2 --><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId></dependency><!--lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.28</version><scope>provided</scope></dependency><!--test--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>

配置application.yml

server:port: 2001spring:application:name: seata-order-servicecloud:nacos:discovery:server-addr: localhost:8848         #Nacos服务注册中心地址# ==========applicationName + druid-mysql8 driver===================datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3307/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=trueusername: rootpassword: 123456
# ========================mybatis===================
mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.zm.cloud.entitiesconfiguration:map-underscore-to-camel-case: true# ========================seata===================
seata:registry:type: nacosnacos:server-addr: 127.0.0.1:8848namespace: ""group: SEATA_GROUPapplication: seata-servertx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称service:vgroup-mapping: # 点击源码分析default_tx_group: default # 事务组与TC服务集群的映射关系data-source-proxy-mode: AT       #不写也是ATlogging:level:io:seata: info

对应说明

image-20240403152736627

主启动类

@SpringBootApplication
@MapperScan("com.zm.cloud.mapper")
@EnableDiscoveryClient
@EnableFeignClients
public class Main2001 {public static void main(String[] args) {SpringApplication.run(Main2001.class,args);}
}

我们把mybatis_generator中自动生成的订单模块dao层的实体类和mapper文件复制过来。

image-20240404105329805

在实体类上打上@ToString注解并实现序列化接口。

@Table(name = "t_order")
@ToString
public class Order implements Serializable

创建OrderService

public interface OrderService {//创建订单void createOrder(Order order);
}

创建OrderServiceImp实现类

@Slf4j
@Service
public class OrderServiceImp implements OrderService {@Resourceprivate OrderMapper orderMapper;@Resourceprivate StorageFeignApi storageFeignApi;//订单微服务通过openfeign调用库存微服务@Resourceprivate AccountFeignApi accountFeignApi;//订单微服务通过openfeign调用账户微服务@Overridepublic void createOrder(Order order) {//XID全局事务的检查通过seata的RootContext获取String xid = RootContext.getXID();//创建订单log.info("==================>开始新建订单"+"\t"+"xid_order:" +xid+"\n");//创建之前有订单状态,0是创建中,1代表创建完成order.setStatus(0);//再开始创建订单,拿到返回值进行判断,大于0代表插入一条记录成功int i = orderMapper.insert(order);//插入成功之后再获得MySQL的实体对象Order orderFromDB = null;if (i>0) {orderFromDB=orderMapper.selectOne(order);log.info("-------> 新建订单成功,orderFromDB info: "+orderFromDB+"\n");//新订单创建成功后开始调用storageFeignApi减少一个库存log.info("-------> 订单微服务开始调用storageFeignApi减少一个库存"+"\n");storageFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getCount());log.info("-------> 订单微服务开始调用storageFeignApi减少一个库存操作完成!!!");//新订单创建成功后开始调用accountFeignApi扣用户的钱log.info("-------> 订单微服务开始调用accountFeignApi扣除账户余额"+"\n");accountFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getMoney());log.info("-------> 订单微服务开始调用accountFeignApi扣除账户余额操作完成!!!");System.out.println();//订单完成修改状态为1log.info("-------> 正在修改订单状态....");orderFromDB.setStatus(1);//构建查询条件Example example = new Example(Order.class);Example.Criteria criteria = example.createCriteria();criteria.andEqualTo("userId",orderFromDB.getUserId());criteria.andEqualTo("status",0);//使用上面创建的查询条件,orderMapper调用updateByExampleSelective来更新数据库中的订单状态。//这个方法会找到所有满足条件的订单,将这些订单的status字段更新为orderFromDB中的status值int  updateResult = orderMapper.updateByExampleSelective(orderFromDB, example);log.info("-------> 修改订单状态完成"+"\t"+updateResult);log.info("-------> orderFromDB info: "+orderFromDB);}System.out.println();log.info("==================>结束新建订单"+"\t"+"xid_order:" +xid);}
}

新建一个controller

@RestController
@Slf4j
public class OrderController {@Resourceprivate OrderService orderService;//创建订单@GetMapping("/order/create")public ResultData create(Order order){orderService.createOrder(order);return ResultData.success(order);}
}

6.3、新建库存Storage微服务

新建module,seata-storage-service2002

添加相关依赖

 <dependencies><!-- nacos --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--alibaba-seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId></dependency><!--openfeign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--loadbalancer--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency><!--cloud_commons_utils--><dependency><groupId>com.atguigu.cloud</groupId><artifactId>cloud-api-commons</artifactId><version>1.0-SNAPSHOT</version></dependency><!--web + actuator--><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><!--SpringBoot集成druid连接池--><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId></dependency><!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html --><dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId></dependency><!--mybatis和springboot整合--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId></dependency><!--Mysql数据库驱动8 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!--persistence--><dependency><groupId>javax.persistence</groupId><artifactId>persistence-api</artifactId></dependency><!--通用Mapper4--><dependency><groupId>tk.mybatis</groupId><artifactId>mapper</artifactId></dependency><!--hutool--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId></dependency><!-- fastjson2 --><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId></dependency><!--lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.28</version><scope>provided</scope></dependency><!--test--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>

配置文件application.yml

server:port: 2002spring:application:name: seata-storage-servicecloud:nacos:discovery:server-addr: localhost:8848         #Nacos服务注册中心地址# ==========applicationName + druid-mysql8 driver===================datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=trueusername: rootpassword: 123456
# ========================mybatis===================
mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.atguigu.cloud.entitiesconfiguration:map-underscore-to-camel-case: true
# ========================seata===================
seata:registry:type: nacosnacos:server-addr: 127.0.0.1:8848namespace: ""group: SEATA_GROUPapplication: seata-servertx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称service:vgroup-mapping:default_tx_group: default # 事务组与TC服务集群的映射关系data-source-proxy-mode: ATlogging:level:io:seata: info

主启动类

@MapperScan("com.zm.cloud.mapper")
@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class Main2002 {public static void main(String[] args) {SpringApplication.run(Main2002.class,args);}
}

修改mybatis_generator中的自动生成配置,生成出库存的实体类和mapper文件

修改config.properties切换成库存的数据库seata_storage

# seata_storage
jdbc.driverClass = com.mysql.cj.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3307/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
jdbc.user = root
jdbc.password =123456

修改generatorConfig.xml切换t_storage

<table tableName="t_storage" domainObjectName="Storage"><generatedKey column="id" sqlStatement="JDBC"/>
</table>

修改完成之后刷新插件

image-20240404162703036

将生成的实体类和mapper文件拷贝到库存微服务中,实体类中打上ToString注解并实现序列化接口。

@Table(name = "t_storage")
@ToString
public class Storage implements Serializable 

StorageMapper中添加自己的扣减库存的方法

public interface StorageMapper extends Mapper<Storage> {//扣减库存void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}

对应着StorageMapper.xml增加方法的实现

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zm.cloud.mapper.StorageMapper"><resultMap id="BaseResultMap" type="com.zm.cloud.entities.Storage"><!--WARNING - @mbg.generated--><id column="id" jdbcType="BIGINT" property="id" /><result column="product_id" jdbcType="BIGINT" property="productId" /><result column="total" jdbcType="INTEGER" property="total" /><result column="used" jdbcType="INTEGER" property="used" /><result column="residue" jdbcType="INTEGER" property="residue" /></resultMap><update id="decrease">update t_storagesetused = used + #{count},residue = residue - #{count}whereproduct_id = #{productId}</update>
</mapper>

新建StorageService

public interface StorageService {//扣库存void decrease(Long productId, Integer count);
}

实现类StorageServiceImpl

@Service
@Slf4j
public class StorageServiceImpl implements StorageService {@Resourceprivate StorageMapper storageMapper;@Overridepublic void decrease(Long productId, Integer count) {log.info("------->storage-service中扣减库存开始");storageMapper.decrease(productId,count);log.info("------->storage-service中扣减库存结束");}
}

库存微服务的controller

@RestController
public class StorageController {@Resourceprivate StorageService storageService;//扣库存@RequestMapping("/storage/decrease")public ResultData decrease(Long productId, Integer count) {storageService.decrease(productId, count);return ResultData.success("扣减库存成功!");}
}

6.4、新建账户Account微服务

新建模块seata-account-service2003

依赖和上面的库存微服务一样的就不重复了,配置文件开始,application.yml

server:port: 2003spring:application:name: seata-account-servicecloud:nacos:discovery:server-addr: localhost:8848         #Nacos服务注册中心地址# ==========applicationName + druid-mysql8 driver===================datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3307/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=trueusername: rootpassword: 123456
# ========================mybatis===================
mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.zm.cloud.entitiesconfiguration:map-underscore-to-camel-case: true
# ========================seata===================
seata:registry:type: nacosnacos:server-addr: 127.0.0.1:8848namespace: ""group: SEATA_GROUPapplication: seata-servertx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称service:vgroup-mapping:default_tx_group: default # 事务组与TC服务集群的映射关系data-source-proxy-mode: ATlogging:level:io:seata: info

主启动类Main2003

@MapperScan("com.zm.cloud.mapper")
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class Main2003 {public static void main(String[] args) {SpringApplication.run(Main2003.class,args); }
}

修改mybatis_generator中的自动生成配置文件,生成出账户的实体类和mapper文件

修改config.properties切换成库存的数据库seata_account

# seata_account
jdbc.driverClass = com.mysql.cj.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3307/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
jdbc.user = root
jdbc.password =123456

修改generatorConfig.xml切换t_account

<!--seata_account-->
<table tableName="t_account" domainObjectName="Account"><generatedKey column="id" sqlStatement="JDBC"/>
</table>

修改完成,更新一下自动生成,然后将生成的实体类和mapper文件拷贝到Account微服务中。

实体类中打上ToString注解并实现序列化接口。

@Table(name = "t_account")
@ToString
public class Account implements Serializable 

AccountMapper中添加扣减余额的方法

public interface AccountMapper extends Mapper<Account> {//扣减账户余额void  decrease(@Param("userId") Long userId,@Param("money") Long money);
}

在AccountMapper中添加实现

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zm.cloud.mapper.AccountMapper"><resultMap id="BaseResultMap" type="com.zm.cloud.entities.Account"><!--WARNING - @mbg.generated--><id column="id" jdbcType="BIGINT" property="id" /><result column="user_id" jdbcType="BIGINT" property="userId" /><result column="total" jdbcType="DECIMAL" property="total" /><result column="used" jdbcType="DECIMAL" property="used" /><result column="residue" jdbcType="DECIMAL" property="residue" /></resultMap><update id="decrease">update t_accountset  residue = residue - #{money},used = used +  #{money}where user_id = #{userId}</update>
</mapper>

AccountService

public interface AccountService {//扣减余额void  decrease(@Param("userId") Long userId, @Param("money") Long money);
}

接口实现类AccountServiceImpl

@Service
@Slf4j
public class AccountServiceImpl implements AccountService {@Resourceprivate AccountMapper accountMapper;@Overridepublic void decrease(Long userId, Long money) {log.info("------->account-service中扣减账户余额开始");accountMapper.decrease(userId,money);//myTimeOut();//int age = 10/0;log.info("------->account-service中扣减账户余额结束");}//我们要模拟事务处理失败要回滚的场景private static void myTimeOut(){try { TimeUnit.SECONDS.sleep(65); } catch (InterruptedException e) { e.printStackTrace(); }}
}

AccountController


@RestController
public class AccountController {@Resourceprivate AccountService accountService;//扣减账户余额@RequestMapping("/account/decrease")ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money){accountService.decrease(userId,money);return ResultData.success("扣减账户余额成功!");}
}

6.5、测试

nacos和seata全部启动起来,然后把2001、2002、2003微服务启动起来。

image-20240404193936979

此时我们没有在订单模块添加@GlobalTransactional注解,浏览器请求一个下个订单,代表1号用户花了100块钱买了10个1号产品。

localhost:2001/order/create?userld=1&productld=1&count=10&money=100

然后你就会发现报错了~

image-20240404200128953

这个就跟我们的springboot+springcloud版本太高导致和阿里巴巴Seata不兼容有关了,去改一下boot+cloud的版本。

<!--仅为了整合openfeign + alibaba seata的案例,降低版本处理下-->
<spring.boot.version>3.1.7</spring.boot.version>
<spring.cloud.version>2022.0.4</spring.cloud.version>

我们到数据库中看一下它有订单的插入,但是库存和账户都没有变化。

image-20240404201023403

把这条数据删除后,再重启三个服务,然后再次访问一次

image-20240404204332985

新建成功,查看后台输出情况

image-20240404204448144

再看一下数据库的数据对不对帐。

image-20240404204724612

OK了,目前我们没有添加注解@GlobalTransactional,现在演示我们扣钱过程中如果出现报错,我们的事务会不会进行回滚。

超时异常报错

把之前的那个seata-account-service2003微服务中service实现类里的超时方法注释打开测试一下。

image-20240404205029329

重启2003微服务测试,开始转圈,等待65秒,其实这个时候看数据库订单状态为1

image-20240404205332274

后台报错超时,但是看到它扣款和减少库存是成功的

image-20240404205426589

到数据库中看,库存和金额都是被扣了

image-20240404210548853

by zero错误测试

把刚才的超时方法注掉,把那个除以0的打开,再来一次。

image-20240404210716710

直接报错,再看数据库的数据。

image-20240404210945074

库存减少了,钱也扣了,结果是个未完成订单,这事务压根不回滚。

6.6、添加@GlobalTransactional注解测试

我们保留超时的方法,然后在订单的service实现类中的createOrder方法上打上@GlobalTransactional并为这个全局事务起一个名字zm-transactional-order

@GlobalTransactional(name = "zm-transactional-order",rollbackFor = Exception.class)
@Override
public void createOrder(Order order)

重启服务,还是那个localhost:2001/order/create?userId=1&productId=1&count=10&money=100

然后开seata的控制台,全局事务的信息都有了,全局锁会保证当前的操作只有子事务被允许。

image-20240405104407804

这个时候到数据库看一下,有压根未完成的订单记录,库存也扣除了,账户也扣除了,还有undo_log表的记录。

image-20240405104620918

等到我们的超时时间一到,就会发现上面的所有事务全部都回滚了,订单都没有了,都是添加注解之前的数据。

image-20240405104759589

undo_log也是空的了

image-20240405104857796

查看控制台输出

image-20240405105411973

undo_log这个表无论数据是否正常插入,最后都会自动删除,它就起到一个溯源的作用,它记录上一步的内容,有错误的情况下根据这个内容进行回滚,然后自动删除。

既然错误的已经演示了,那么再来一个正常情况下的。

image-20240405142301533

查看后台输出

image-20240405142357100

6.7、AT模式如何做到对业务的无侵入

seata的整体机制:

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

    在一阶段,Seata会拦截“业务SQL”

    1. 解析SQL语义,找到“业务SQL”要更新的业务数据,在业务数据被更新之前,将其保存为“before image”;

    2. 执行“业务SQL”更新业务数据,在业务数据更新之后将其保存为“after image”,最后生成行锁;

      以上操作全部在一个数据库事务内完成,这样保证了一阶段的原子性。

    image-20240405144109220

  • 二阶段:

    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

二阶段分为2种情况

正常提交:

二阶段如是顺利提交的话,因为“业务 SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

image-20240405144226673

出现异常需要回滚

​ 二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。

​ 回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,

​ 如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

image-20240405144418547

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/306204.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

15. 【Android教程】按钮 Button/ImageButton

在前面两章我们讲了 TextView&#xff0c;它是一个纯输出的控件&#xff1b;而 EditText 在 TextView 基础之上加入了简单的输入功能&#xff1b;今天要讲的 Button 是一个和用户互动感很强的控件&#xff0c;从今往后不再是单纯的文本展示&#xff0c;我们可以通过 TextView、…

2024年第五届计算机视觉与信息技术国际会议(CVIT 2024)即将召开!

2024年第五届计算机视觉与信息技术国际会议&#xff08;CVIT 2024&#xff09;将于2024年8月16-18日在北京举行。CVIT 2024由北方工业大学主办&#xff0c;国内外的专家学者将齐聚一堂&#xff0c;共同分享最新的技术突破、研究方法和应用案例&#xff0c;共同推动计算机视觉与…

选择排序解读

在计算机科学中&#xff0c;排序算法是一种将数据元素按照某种顺序排列的算法。今天&#xff0c;我们要探讨的是选择排序&#xff08;Selection Sort&#xff09;&#xff0c;这是一种简单直观的排序方法&#xff0c;通过不断选择剩余元素中的最小&#xff08;或最大&#xff0…

MySOL之旅--------MySQL数据库基础( 3 )

本篇碎碎念:要相信啊,胜利就在前方,要是因为一点小事就停滞不前,可能你也不适合获取胜利,成功的路上会伴有泥石,但是走到最后,你会发现身上的泥泞皆是荣耀的勋章! 今日份励志文案: 凡是发生皆有利于我 目录 查询(select) 1.全列查询 2.指定列查询 3.查询字段为表达式 ​编…

加固系统安全,防范ssh暴力破解之Fail2Ban

你是否还在担心你的服务器被攻击&#xff1f;你是否还在担心你的博客的安全&#xff1f;你是否还在担心你的隐私&#xff1f;别急fail2ban它来了&#xff0c;它可以解决你的一切问题。 Fail2Ban 是什么&#xff1f; 现在让我们一起来认识一下今天的主角 – Fail2Ban。简单说来…

【Linux 命令】内核、驱动调试手段总结

文章目录 1. printk2. strace3. ltrace4. ptrace5. ftrace6. 动态打印7. perf8. devmem9. demsg参考&#xff1a; 1. printk printk() 是 Linux 内核中最广为人知的函数之一。它是我们打印消息的标准工具&#xff0c;通常也是追踪和调试的最基本方法。 虽然 printk() 是基于 …

深入理解中文编码:原理、应用与实践

title: 深入理解中文编码&#xff1a;原理、应用与实践 date: 2024/4/12 15:09:00 updated: 2024/4/12 15:09:00 tags: 中文编码字符集编码标准存储处理转换技术安全加密未来趋势 第一章&#xff1a;引言 编码的基本概念与作用 编码是将信息转换为特定格式以便存储、传输或处…

在启动Windows安装的Nacos时报错

Please set the JAVA_HOME variable in your environm 有可能时jdk版本过低引起的&#xff0c;所以安装一个1.8版本以上的jdk 在安装jdk完成以后配置好环境变量&#xff0c;测试一下 winr打开控制台&#xff0c;输入&#xff1a; Java -version 出现如下情况说明jdk安装配置…

护眼台灯哪个牌子最好,五款高中生护眼台灯推荐

​在当今时代&#xff0c;孩子们背负着沉重的学业负担&#xff0c;随着他们年龄的增长&#xff0c;作业量也随之增加&#xff0c;这意味着每天用眼的时间越来越长。尤其是在夜晚&#xff0c;许多孩子学习到深夜&#xff0c;若照明不当&#xff0c;极易导致视力下降。因此&#…

Gson的用法

1. 导入依赖 <dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.8.6</version> </dependency> 2. 使用Gson进行解析 2.1 Gson解析普通对象 package com.jiang.partnetbackend.…

LLM大语言模型(九):LangChain封装自定义的LLM

背景 想基于ChatGLM3-6B用LangChain做LLM应用&#xff0c;需要先了解下LangChain中对LLM的封装。本文以一个hello world的封装来示例。 LangChain中对LLM的封装 继承关系&#xff1a;BaseLanguageModel——》BaseLLM——》LLM LLM类 简化和LLM的交互 _call抽象方法定义 ab…

DXP学习002-PCB编辑器的环境参数及电路板参数相关设置

目录 一&#xff0c;dxp的pcb编辑器环境 1&#xff0c;创建新的PCB设计文档 2&#xff0c;PCB编辑器界面 1&#xff09;布线工具栏 2&#xff09;公用工具栏 3&#xff09;层标签栏 ​编辑 3&#xff0c;PCB设计面板 1&#xff09;打开pcb设计面板 4&#xff0c;PCB观…

【数据结构】单链表(二)

目录 1.查找数据 2.指定位置插入和删除节点 2.1 指定位置之前插入节点 2.2 指定位置之后插入节点 2.3 删除指定位置节点 2.4 删除指定位置之后的节点 3.销毁链表 我们接着上一篇【数据结构】单链表&#xff08;一&#xff09;-CSDN博客 来继续实现单链表 1.查找数据 在…

前端开发学习笔记 3 (Chrome浏览器调试工具、Emmet语法、CSS复合选择器、CSS元素选择模式、CSS背景)

文章目录 Chrome浏览器调试工具Emmet语法CSS复合选择器后代选择器子选择器并集选择器伪类选择器 CSS元素选择模式元素选择模式概述CSS块标签CSS行内标签CSS行内块标签CSS元素显示模式转换 CSS背景CSS背景颜色CSS背景图片CSS背景图片平铺CSS背景图片位置CSS背景图片固定CSS背景复…

SpringAI初体验之HelloWorld

目录 前言1.准备工作2.初始化项目3.解决问题3.1 Connection Time out 连接超时问题3.2 You exceeded your current quota 额度超限问题 4.访问调用5.总结 前言 在逛SpringBoot页面时突然看到页面上新增了一个SpringAI项目,于是试了一下&#xff0c;感觉还行。其实就是封装了各家…

Redis性能管理和集群的三种模式(二)

一、Redis集群模式 1.1 redis的定义 redis 集群 是一个提供高性能、高可用、数据分片、故障转移特性的分布式数据解决方案 1.2 redis的功能 数据分片&#xff1a;redis cluster 实现了数据自动分片&#xff0c;每个节点都会保存一份数据故障转移&#xff1a;若个某个节点发生故…

网络协议学习——以太网协议

目录 ​编辑 一&#xff0c;以太网简介 二&#xff0c;以太网通信的过程 为什么不用IP地址&#xff1f; 过程 MAC帧 MAC帧的字段介绍 ARP协议 传输过程的一些问题 RARP协议 提高效率 三&#xff0c;其他问题 ARP诈骗问题 URL解析过程 一&#xff0c;以太网简介 …

AI大模型基石:文字与数字的起源与演变

AI大模型基石&#xff1a;文字与数字的起源与演变 1、文字 1.1、起源 我们的祖先在还没有发明文字和语言之前就已经开始使用“咿咿呀呀”的声音来传播信息了&#xff0c;比如在野外活动遇到危险&#xff0c;然后发出“咿咿呀呀”的声音来提醒同伴小心&#xff0c;同伴在接收到…

GEE数据集——1986年—2022年加拿大全国烧毁面积综合数据 (NBAC)

简介 加拿大全国烧毁面积综合数据 (NBAC) 全国烧毁面积综合数据 (NBAC) 是一个地理信息系统数据库和系统&#xff0c;用于计算自 1986 年以来每年全国范围内烧毁的森林面积。这些数据用于帮助估算加拿大的碳排放量。烧毁面积是通过评估一系列可用数据源确定的&#xff0c;这些…

废品回收小程序推动回收行业的发展趋势

回收在全球都是一个重要行业&#xff0c;它为全球的环保作出了重要贡献。 随着科技的不断发展创新&#xff0c;废品回收的方式也逐渐多样&#xff0c;全新的线上回收小程序也逐渐出现在大众的生活中&#xff0c;在当下的手机时代&#xff0c;线上回收也为大众提供了更加便利的…