• SpringCloud Alibaba之Seata分布式事务学习笔记


    前言

    本节配套案例代码:Gitee仓库Github仓库

    所有博客文件目录索引:博客目录索引(持续更新)

    学习视频:SpringCloud 教程 已完结(IDEA 2022.1最新版)4K蓝光画质 微服务开发

    PS:本章节中部分图片是直接引用学习课程课件,如有侵权,请联系删除。

    当前项目环境版本:springboot 2.3.12.RELEASEspringcloud alibaba 2.2.7.RELEASESpringCloud Hoxton-SR12

    一、Seata介绍

    1.1、认识Seata

    SpringCloud Alibaba为我们提供了用于处理分布式事务的组件Seata。

    img

    Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

    实际上,就是多了一个中间人来协调所有服务的事务。


    1.2、Seata的四种事务模式

    Seata支持4种事务模式,官网文档:https://seata.io/zh-cn/docs/overview/what-is-seata.html

    • AT:本质上就是2PC的升级版,在 AT 模式下,用户只需关心自己的 “业务SQL”
      1. 一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
      2. 二阶段如果确认提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可,当然如果需要回滚,那么就用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
    • TCC:和我们上面讲解的思路是一样的。

    • XA:同上,但是要求数据库本身支持这种模式才可以。

    • Saga:用于处理长事务,每个执行者需要实现事务的正向操作和补偿操作:


    二、实战:集成Seata实现分布式事务(AT模式)

    说明:demo项目其中的部分代码写的比较简陋,我们将重心放在分布式事务上即可!

    对于2.1部分的代码我已经进行了打包:在对应gitee或github仓库的指定位置下载即可,可直接复现分布式事务问题!

    image-20220802212750356

    image-20220802212851648


    2.1、本地项目搭建(复现分布式事务问题)

    项目介绍

    为了能够集成Seata组件来实现分布式事务数据一致性的效果,来构建多个微服务进行远程调用。

    image-20220802202850589

    本次使用到的分布式组件包含:nacos(注册中心)、feign(远程调用组件)

    服务包含:book-service(图书服务)、borrow-service(借阅服务)、user-service(用户服务)。

    事务问题描述(目标复现):在borrow-service服务中会在一个service中会执行本地事务,远程调用book-service以及user-service的接口,这两个服务的接口都与数据库有交互操作,在没有使用Seata组件前,若是其中某个服务出现异常,那么之前提交的操作都不能够进行回滚,因为这涉及到多个不同的事务管理器。

    看一下数据库表:

    image-20220802203238768

    db_book:图书表,其中count表示该数的库存数量。对应的是book-service。

    image-20220802203304750

    db_borrow:借阅表。对应的是borrow-service。

    image-20220802203319177

    db_user:用户表。对应的是user-service。

    image-20220802203331754


    Nacos服务创建命名空间

    创建命名空间为seata-demo:

    image-20220802210001056


    book-service服务

    image-20220802205815764

    引入依赖:

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
    
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
        dependency>
    
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
    
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <scope>runtimescope>
        dependency>
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.4.0version>
        dependency>
    dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    1、yaml配置项:application.yaml

    server:
      port: 8083
    
    spring:
      application:
        name: book-service
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/seata-demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
        username: root
        password: 123456
      cloud:
        nacos:  # 如果不指定命名空间会默认注册到public里面去 如果没有指定分组 会注册到DEFAULT_GROUP
          server-addr: localhost:8848  # 指定服务注册地址
          username: nacos
          password: nacos
          discovery:
            namespace: 0245d1ab-5611-486e-8444-957bebab6d78   # 若是不指定,默认就是public
            group: BOOK_GROUP   # 若是不指定,默认是DEFAULT_GROUP
            service: book-service   # 默认使用的是spring.application.name,这里可以进行指定
    
    #控制台打印sql(默认不会有打印sql语句)
    mybatis-plus:
      mapper-locations: classpath*:/mapperxxx/**/*.xml
      configuration:
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    2、Mapper接口以及Mapper配置文件、pojo对象

    package com.changlu.seatauserservice.pojo;
    
    import com.baomidou.mybatisplus.annotation.TableField;
    import com.baomidou.mybatisplus.annotation.TableId;
    import com.baomidou.mybatisplus.annotation.TableName;
    import lombok.Data;
    import lombok.EqualsAndHashCode;
    
    import java.io.Serializable;
    
    @Data
    @EqualsAndHashCode(callSuper = false)
    @TableName("db_user")
    public class UserModel implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        @TableId("uid")
        private Integer uid;
    
        @TableField("name")
        private String name;
    
        @TableField("age")
        private Integer age;
    
        @TableField("book_count")
        private Integer bookCount;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    
    DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.changlu.seatabookservcie.mapper.BookMapper">
    
        
        <resultMap id="BaseResultMap" type="com.changlu.seatabookservcie.pojo.BookModel">
            <id column="id" property="id" />
            <result column="name" property="name" />
            <result column="count" property="count" />
        resultMap>
    
    mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    package com.changlu.seatabookservcie.mapper;
    
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.changlu.seatabookservcie.pojo.BookModel;
    import org.apache.ibatis.annotations.Select;
    import org.apache.ibatis.annotations.Update;
    
    /**
     * 

    * Mapper 接口 *

    * * @author ChangLu * @since 2022-08-02 */
    public interface BookMapper extends BaseMapper<BookModel> { @Select("SELECT count from db_book WHERE id = #{id}") int bookRemain(Integer id); @Update("UPDATE db_book set count = count - 1 where id = #{id} and count > 0") int minusBookRemain(Integer id); }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    3、Service接口以及实现类:com.changlu.seatabookservcie.service.BookService

    package com.changlu.seatabookservcie.service;
    
    
    import com.baomidou.mybatisplus.extension.service.IService;
    import com.changlu.seatabookservcie.pojo.BookModel;
    
    /**
     * 

    * 服务类 *

    * * @author ChangLu * @since 2022-08-02 */
    public interface BookService extends IService<BookModel> { int bookRemain(Integer id); int minusBookRemain(Integer id); }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    package com.changlu.seatabookservcie.service.impl;
    
    
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.changlu.seatabookservcie.mapper.BookMapper;
    import com.changlu.seatabookservcie.pojo.BookModel;
    import com.changlu.seatabookservcie.service.BookService;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    
    /**
     * 

    * 服务实现类 *

    * * @author ChangLu * @since 2022-08-02 */
    @Service public class BookServiceImpl extends ServiceImpl<BookMapper, BookModel> implements BookService { @Resource private BookMapper bookMapper; @Override public int bookRemain(Integer id) { return bookMapper.bookRemain(id); } @Override public int minusBookRemain(Integer id) { return bookMapper.minusBookRemain(id); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    4、控制器,对外暴露两个接口,一个是查询以及一个更改数据:

    package com.changlu.seatabookservcie.controller;
    
    
    import com.changlu.seatabookservcie.service.BookService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 

    * 前端控制器 *

    * * @author ChangLu * @since 2022-08-02 */
    @RestController @RequestMapping("/book") public class BookController { @Autowired private BookService bookService; @GetMapping("/remain/{id}") public int bookRemain(@PathVariable("id") Integer id) { return bookService.bookRemain(id); } @GetMapping("/minus/{id}") public int minusBookRemain(@PathVariable("id") Integer id) { return bookService.minusBookRemain(id); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    5、启动器开启服务注册以及Mapper扫描:启动器上添加

    @MapperScan("com.changlu.seatabookservcie.mapper")
    @EnableDiscoveryClient
    
    • 1
    • 2

    user-service服务

    image-20220802210350173

    引入的依赖与book-service一致,不再贴出。

    1、配置文件:application.yaml

    server:
      port: 8081
    
    spring:
      application:
        name: user-service
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/seata-demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
        username: root
        password: 123456
      cloud:
        nacos:  # 如果不指定命名空间会默认注册到public里面去 如果没有指定分组 会注册到DEFAULT_GROUP
          server-addr: localhost:8848  # 指定服务注册地址
          username: nacos
          password: nacos
          discovery:
            namespace: 0245d1ab-5611-486e-8444-957bebab6d78   # 若是不指定,默认就是public
            group: BOOK_GROUP   # 若是不指定,默认是DEFAULT_GROUP
            service: user-service   # 默认使用的是spring.application.name,这里可以进行指定
    
    #控制台打印sql(默认不会有打印sql语句)
    mybatis-plus:
      mapper-locations: classpath*:/mapperxxx/**/*.xml
      configuration:
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    2、mapper接口以及mapper映射配置文件、pojo类

    package com.changlu.seataborrowservice.pojo;
    
    import com.baomidou.mybatisplus.annotation.TableField;
    import com.baomidou.mybatisplus.annotation.TableId;
    import com.baomidou.mybatisplus.annotation.TableName;
    import lombok.Data;
    import lombok.EqualsAndHashCode;
    
    import java.io.Serializable;
    
    /**
     * 

    * *

    * * @author ChangLu * @since 2022-08-02 */
    @Data @EqualsAndHashCode(callSuper = false) @TableName("db_borrow") public class BorrowModel implements Serializable { private static final long serialVersionUID = 1L; @TableId("user_id") private Integer userId; @TableField("book_id") private Integer bookId; }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    package com.changlu.seatauserservice.mapper;
    
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.changlu.seatauserservice.pojo.UserModel;
    import org.apache.ibatis.annotations.Select;
    import org.apache.ibatis.annotations.Update;
    
    /**
     * 

    * Mapper 接口 *

    * * @author ChangLu * @since 2022-08-02 */
    public interface UserMapper extends BaseMapper<UserModel> { @Select("SELECT book_count from db_user WHERE uid = #{uid}") int getUserRemainBook(Integer uid); @Update("UPDATE db_user set book_count = book_count - 1 where uid = #{uid} and book_count > 0") int minusUserBookCount(Integer uid); }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    
    DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.changlu.seatauserservice.mapper.UserMapper">
    
        
        <resultMap id="BaseResultMap" type="com.changlu.seatauserservice.pojo.UserModel">
            <id column="uid" property="uid" />
            <result column="name" property="name" />
            <result column="age" property="age" />
            <result column="book_count" property="bookCount" />
        resultMap>
    
    mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    3、service接口:

    package com.changlu.seatauserservice.service;
    import com.baomidou.mybatisplus.extension.service.IService;
    import com.changlu.seatauserservice.pojo.UserModel;
    
    /**
     * 

    * 服务类 *

    * * @author ChangLu * @since 2022-08-02 */
    public interface UserService extends IService<UserModel> { int getUserRemainBook(Integer uid); int minusUserBookCount(Integer uid); }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    package com.changlu.seatauserservice.service.impl;
    
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.changlu.seatauserservice.mapper.UserMapper;
    import com.changlu.seatauserservice.pojo.UserModel;
    import com.changlu.seatauserservice.service.UserService;
    import org.springframework.stereotype.Service;
    import javax.annotation.Resource;
    
    /**
     * 

    * 服务实现类 *

    * * @author ChangLu * @since 2022-08-02 */
    @Service public class UserServiceImpl extends ServiceImpl<UserMapper, UserModel> implements UserService { @Resource private UserMapper userMapper; @Override public int getUserRemainBook(Integer uid) { return userMapper.getUserRemainBook(uid); } @Override public int minusUserBookCount(Integer uid) { return userMapper.minusUserBookCount(uid); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    4、控制器:

    package com.changlu.seatauserservice.controller;
    
    
    import com.changlu.seatauserservice.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;
    
    /**
     * 

    * 前端控制器 *

    * * @author ChangLu * @since 2022-08-02 */
    @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @GetMapping("/remainbook/{uid}") public int getUserRemainBook(@PathVariable("uid")Integer uid) { return userService.getUserRemainBook(uid); } @GetMapping("/minusbook/{uid}") public int minusUserBookCount(@PathVariable("uid")Integer uid) { return userService.minusUserBookCount(uid); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    5、启动器上添加注解,与book-service一致

    @MapperScan("com.changlu.seatauserservice.mapper")
    @EnableDiscoveryClient
    
    • 1
    • 2

    borrow-service服务(分布式事务问题产生见其中service方法)

    在borrow-service服务中,还包含有feign组件,该服务会对book-service、user-service服务来进行远程调用,那么本次服务的分布式事务问题也是从这里产生的!

    image-20220802210800467

    引入依赖:与前面服务一致同样也有nacos注册依赖,唯一多了一个就是feign组件

    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
    dependency>
    
    <dependencyManagement>
         <dependencies>
             <dependency>
                 <groupId>org.springframework.cloudgroupId>
                 <artifactId>spring-cloud-dependenciesartifactId>
                 <version>${spring-cloud.version}version>
                 <type>pomtype>
                 <scope>importscope>
             dependency>
         dependencies>
    dependencyManagement>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    1、配置文件:applicaion.yaml

    server:
      port: 8082
    
    spring:
      application:
        name: borrow-service
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/seata-demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
        username: root
        password: 123456
      cloud:
        nacos:  # 如果不指定命名空间会默认注册到public里面去 如果没有指定分组 会注册到DEFAULT_GROUP
          server-addr: localhost:8848  # 指定服务注册地址
          username: nacos
          password: nacos
          discovery:
            namespace: 0245d1ab-5611-486e-8444-957bebab6d78   # 若是不指定,默认就是public
            group: BOOK_GROUP   # 若是不指定,默认是DEFAULT_GROUP
            service: borrow-service   # 默认使用的是spring.application.name,这里可以进行指定
    
    #控制台打印sql(默认不会有打印sql语句)
    mybatis-plus:
      mapper-locations: classpath*:/mapperxxx/**/*.xml
      configuration:
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    2、mapper接口以及mapper映射文件、pojo类

    package com.changlu.seataborrowservice.pojo;
    
    import com.baomidou.mybatisplus.annotation.TableField;
    import com.baomidou.mybatisplus.annotation.TableId;
    import com.baomidou.mybatisplus.annotation.TableName;
    import lombok.Data;
    import lombok.EqualsAndHashCode;
    
    import java.io.Serializable;
    
    /**
     * @author ChangLu
     * @since 2022-08-02
     */
    @Data
    @EqualsAndHashCode(callSuper = false)
    @TableName("db_borrow")
    public class BorrowModel implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        @TableId("user_id")
        private Integer userId;
    
        @TableField("book_id")
        private Integer bookId;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    
    DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.changlu.seataborrowservice.mapper.BorrowMapper">
    
        
        <resultMap id="BaseResultMap" type="com.changlu.seataborrowservice.pojo.BorrowModel">
            <id column="user_id" property="userId" />
            <result column="book_id" property="bookId" />
        resultMap>
    
    mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    package com.changlu.seataborrowservice.mapper;
    
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.changlu.seataborrowservice.pojo.BorrowModel;
    import org.apache.ibatis.annotations.Insert;
    import org.apache.ibatis.annotations.Param;
    import org.apache.ibatis.annotations.Select;
    
    /**
     * 

    * Mapper 接口 *

    * * @author ChangLu * @since 2022-08-02 */
    public interface BorrowMapper extends BaseMapper<BorrowModel> { @Select("select * from db_borrow where user_id = #{userId} AND book_id = #{bookId}") BorrowModel getBorrow(@Param("userId")Integer userId, @Param("bookId")Integer bookId); @Insert("insert into db_borrow(user_id, book_id) values(#{userId}, #{bookId})") int addBorrow(@Param("userId")Integer userId, @Param("bookId")Integer bookId); }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    3、两个服务的feign接口:

    package com.changlu.seataborrowservice.feign;
    
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    
    /**
     * @Description:
     * @Author: changlu
     * @Date: 7:52 PM
     */
    @FeignClient(value = "user-service")
    public interface BorrowUserFeign {
    
        @GetMapping("/user/remainbook/{uid}")
        int getUserRemainBook(@PathVariable("uid")Integer uid);
    
        @GetMapping("/user/minusbook/{uid}")
        int minusUserBookCount(@PathVariable("uid")Integer uid);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    package com.changlu.seataborrowservice.feign;
    
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    
    /**
     * @Description:
     * @Author: changlu
     * @Date: 7:52 PM
     */
    @FeignClient(value = "book-service")
    public interface BorrowBookFeign {
    
        @GetMapping("/book/minus/{id}")
        int minusBookRemain(@PathVariable("id") Integer id);
    
        @GetMapping("/book/remain/{id}")
        int bookRemain(@PathVariable("id") Integer id);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    4、出现分布式事务问题的service方法:

    package com.changlu.seataborrowservice.service;
    
    
    import com.baomidou.mybatisplus.extension.service.IService;
    import com.changlu.seataborrowservice.pojo.BorrowModel;
    
    /**
     * @author ChangLu
     * @since 2022-08-02
     */
    public interface BorrowService extends IService<BorrowModel> {
    
        Boolean borrow(Integer uid, Integer bookId);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    package com.changlu.seataborrowservice.service.impl;
    
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.changlu.seataborrowservice.feign.BorrowBookFeign;
    import com.changlu.seataborrowservice.feign.BorrowUserFeign;
    import com.changlu.seataborrowservice.mapper.BorrowMapper;
    import com.changlu.seataborrowservice.pojo.BorrowModel;
    import com.changlu.seataborrowservice.service.BorrowService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    
    /**
     * @author ChangLu
     * @since 2022-08-02
     */
    @Service
    public class BorrowServiceImpl extends ServiceImpl<BorrowMapper, BorrowModel> implements BorrowService {
    
        @Resource
        private BorrowMapper borrowMapper;
    
        @Autowired
        private BorrowBookFeign borrowBookFeign;
    
        @Autowired
        private BorrowUserFeign borrowUserFeign;
    
        @Override
        public Boolean borrow(Integer uid, Integer bookId) {
            //1、判断图书与用户是否都支持借阅
            if (borrowBookFeign.bookRemain(bookId) < 0) {
                throw new RuntimeException("该图书库存不足,无法借阅!");
            }
            if (borrowUserFeign.getUserRemainBook(uid) < 1){
                throw new RuntimeException("该用户借阅图书数量已上限!");
            }
            //2、扣减图书库存数量
            if (borrowBookFeign.minusBookRemain(bookId) < 1) {  //book-service服务修改数据
                throw new RuntimeException("扣减图书数量失败!");
            }
            //3、添加图书用户借阅记录
            if (borrowMapper.getBorrow(uid, bookId) != null) {
                throw new RuntimeException("用户已借阅该图书!");
            }
            if (borrowMapper.addBorrow(uid, bookId) < 1) {  //本身服务新增数据
                throw new RuntimeException("图书借阅失败!");
            }
            //4、用户自己本身借阅数量-1
            if (borrowUserFeign.minusUserBookCount(uid) < 1) {  //user-service服务修改数据
                throw new RuntimeException("用户借阅书籍数量更新有误!");
            }
            return true;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    5、控制器:

    package com.changlu.seataborrowservice.controller;
    
    
    import com.alibaba.fastjson.JSONObject;
    import com.changlu.seataborrowservice.service.BorrowService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 

    * 前端控制器 *

    * * @author ChangLu * @since 2022-08-02 */
    @RestController @RequestMapping("/borrow") public class BorrowController { @Autowired private BorrowService borrowService; @GetMapping("/{uid}/{bookId}") public JSONObject borrow(@PathVariable("uid") Integer uid, @PathVariable("bookId") Integer bookId) { JSONObject object = new JSONObject(); Boolean res = false; try { res = borrowService.borrow(uid, bookId); }catch (Exception ex) { object.put("code", 500); object.put("msg", ex.getMessage()); return object; } if (res) { object.put("code", 200); object.put("msg", "借阅成功!"); }else { object.put("code", 500); object.put("msg", "借阅失败!"); } return object; } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    6、启动器上添加注解来进行服务注册、mapper扫描以及feign包扫描增强

    @MapperScan("com.changlu.seataborrowservice.mapper")
    @EnableDiscoveryClient
    @EnableFeignClients
    
    • 1
    • 2
    • 3

    问题复现测试

    提前准备

    ok,此时我们的项目环境搭建已经完成,此时就来启动nacos以及我们的三个服务,来进行接口测试吧!

    image-20220802211708450

    image-20220802211721999

    我们来看下数据库当前的一些数据信息:每本书的库存是3本,借阅记录当前没有,用户借阅次数是3次

    image-20220802211804627

    image-20220802211813954

    image-20220802211821247

    开始测试

    我们来访问borrow-service接口:http://localhost:8082/borrow/1/2

    image-20220802211914740

    可以看到借阅成功!此时看一下数据库的信息:

    image-20220802211947163 image-20220802211953487 image-20220802212002422

    可以看到西游记库存扣减1,借阅记录+1,用户借阅书籍数量-1,没有问题,那么我们此时再次调用找个接口试下:

    问题提前指出:再次进行请求前我们来看下在borrow-service中的借阅方法怎么写的,若是我们再次调用上次接口,由于我们已经借阅了该书,那么此时就会在下面x位置报出异常,问题就出现了,那么book-service这里做的-1操作就产生数据不一致问题!

    image-20220802212156716

    来吧,测试一下:果然不出所料

    image-20220802212309849

    来看下当前的数据库吧:

    image-20220802212336883 image-20220802212344575 image-20220802212353100

    可以看到红色横线的部分就是未回滚的book-service服务,如何解决这类问题呢?我们可以集成阿里的Seata组件来进行尝试!


    2.2、采用file模式来集成seata服务

    2.2.1、启动seata-server

    服务端下载地址:seata-server 1.4.2,由于是外网下载太慢,可使用下面链接下载

    链接:https://pan.baidu.com/s/1AqmcHZY9Op2IucG7rHbjOQ 
    提取码:bb6f 
    
    • 1
    • 2

    下载解压后的目录如下:

    image-20220803134023879

    进入到bin目录之后,我们来进行输入命令执行其中bat工具,直接来启动服务就好(默认是file模式):

    image-20220803134101188

    seata-server.bat -p 8868
    
    • 1

    指定在8868端口来进行执行,启动效果如下:

    image-20220803134304947


    2.2.2、服务集成seata组件实现全局分布式事务

    image-20220803134423122

    三个服务都进行集成seata依赖,主要配置步骤如下

    1、引入seata依赖:

    
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-seataartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2、配置文件来添加seata配置项,下面给出三个服务的不同配置项,下面需要特别注意的就是黄线的部分需要根据不同的服务名来进行配置:

    image-20220803134634743

    seata-user-service:

    seata:
      service:
        vgroup-mapping:
          # 这里需要对事务组做映射,默认的分组名为 应用名称-seata-service-group,将其映射到default集群
          # 这个很关键,一定要配置对,不然会找不到服务
          user-service-seata-service-group: default
        grouplist:
          default: localhost:8868
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    seata-book-service:

    seata:
      service:
        vgroup-mapping:
            # 这里需要对事务组做映射,默认的分组名为 应用名称-seata-service-group,将其映射到default集群
            # 这个很关键,一定要配置对,不然会找不到服务
          book-service-seata-service-group: default
        grouplist:
          default: localhost:8868
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    seata-borrow-service:

    seata:
      service:
        vgroup-mapping:
          # 这里需要对事务组做映射,默认的分组名为 应用名称-seata-service-group,将其映射到default集群
          # 这个很关键,一定要配置对,不然会找不到服务
          borrow-service-seata-service-group: default
        grouplist:
          default: localhost:8868
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3、三个服务的启动器都去开启seata事务注解:

    @EnableAutoDataSourceProxy //开启seata事务配置
    
    • 1

    4、在本地数据库中创建undo_log日志表

    • 由于三个服务都使用的一个数据库seata-demo,所以我们直接在一个数据库中创建即可
    CREATE TABLE `undo_log`
    (
      `id`            BIGINT(20)   NOT NULL AUTO_INCREMENT,
      `branch_id`     BIGINT(20)   NOT NULL,
      `xid`           VARCHAR(100) NOT NULL,
      `context`       VARCHAR(128) NOT NULL,
      `rollback_info` LONGBLOB     NOT NULL,
      `log_status`    INT(11)      NOT NULL,
      `log_created`   DATETIME     NOT NULL,
      `log_modified`  DATETIME     NOT NULL,
      `ext`           VARCHAR(100) DEFAULT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
    ) ENGINE = InnoDB
      AUTO_INCREMENT = 1
      DEFAULT CHARSET = utf8;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    5、最终在我们要进行分布式事务的service方法中添加全局事务注解!也就是borrow-service服务中的borrow()方法:

    @GlobalTransactional
    
    • 1

    image-20220803135518838


    2.2.3、测试

    提前准备

    首先将seata-server服务器启动。

    接着启动我们的三个服务:在启动时向seata-server去进行注册

    image-20220803140013883

    看下seata-server服务的控制台:可以看到确实三个服务已经注册成功了

    image-20220803140125457

    开始测试

    首先来进行测试:http://localhost:8082/borrow/1/2

    image-20220803140319554

    第一次借阅是没有问题的,看下数据库:

    image-20220803140351927 image-20220803140401585 image-20220803140411020

    再次来进行借阅下:

    image-20220803140431224

    ok,此时再看下数据库的各个表:原本在book表中产生问题的数据在这里就没有再出现了,可以看到中间出现异常能够成功回滚了

    image-20220803140453579 image-20220803140501903 image-20220803140510317


    debug

    我们在修改、删除操作上进行debug:

    image-20220803140907757

    看下undo_log表:

    image-20220803140940093

    扣减步骤完成后执行下一步:

    image-20220803140954431

    再次看下undo_log表:

    image-20220803141055346

    其中包含一个全局唯一xid:全局事务就是根据这一条记录来进行回滚管理的!


    2.3、采用nacos模式服务

    对于项目中引入依赖以及添加注解相关操作间2.2.2中的配置步骤,这里不再做演示。

    2.3.1、配置完整步骤

    1、在nacos中创建一个命名空间seata

    image-20220803144927861

    2、修改配置文件

    registry.conf:

    registry {
      # file 、nacos 、eureka、redis、zk、consul、etcd3、sof
      type = "nacos"
    
      nacos {
        # 应用名固定为seata-server
        application = "seata-server"
        # 注册中心的地址
        serverAddr = "127.0.0.1:8848"
        # 默认
        group = "SEATA_GROUP"
        # 命名空间的id
        namespace = "c30eb1d8-8e49-4b5d-beca-b1bf9479e94a"
        # 默认
        cluster = "default"
        # 连接用户名与密码
        username = "nacos"
        password = "nacos"
      }
    }
      
    config {
      # file、nacos 、apollo、zk、consul、etcd3
      type = "nacos"
    
      nacos {
        serverAddr = "127.0.0.1:8848"
        namespace = "c30eb1d8-8e49-4b5d-beca-b1bf9479e94a"
        group = "SEATA_GROUP"
        username = "nacos"
        password = "nacos"
        dataId = "seataServer.properties"
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    3、下载develop包,执行其中的script中脚本来对nacos的配置中心中seata命名空间配置项进行初始化:

    下载地址:seata开发包

    链接:https://pan.baidu.com/s/1iiQphUPbvgcIyXcIjUcjZA 
    提取码:cbr2 
    
    • 1
    • 2

    进入到其中nacos目录下执行脚本,在windows中我们使用git工具来使用:

    image-20220803144519570

    ./nacos-config-interactive.sh
    
    • 1

    image-20220803143051403

    最终效果如下:

    image-20220803161712312

    4、手动在命名空间seata中添加三个服务的配置项:

    image-20220803145654882

    service.vgroupMapping.user-service-seata-service-group   SEATA_GROUP   default
    service.vgroupMapping.book-service-seata-service-group  SEATA_GROUP  default
    service.vgroupMapping.borrow-service-seata-service-group  SEATA_GROUP  default
    
    • 1
    • 2
    • 3

    5、在三个服务项目中的各个服务添加配置项(替换之前的file模式):

    # 2、nacos模式
    seata:
      # 注册
      registry:
        # 使用Nacos
        type: nacos
        nacos:
          # 使用Seata的命名空间,这样才能正确找到Seata服务,由于组使用的是SEATA_GROUP,配置默认值就是,就不用配了
          namespace: c30eb1d8-8e49-4b5d-beca-b1bf9479e94a
          username: nacos
          password: nacos
      # 配置
      config:
        type: nacos
        nacos:
          namespace: c30eb1d8-8e49-4b5d-beca-b1bf9479e94a
          username: nacos
          password: nacos
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    6、关于nacos-server的会话存储位置(默认是file)

    此时注册和配置相关的会话都继承在Nacos中进行了

    还可以配置一下事务会话信息的存储方式,默认是file类型,那么就会在运行目录下创建file_store目录,可以看下启动seata-server后创建的文件效果:

    image-20220803150635786

    6.1、其实我们可以将其搬到数据库中存储,只需要修改一下配置即可,在seata的命名空间中进行修改配置内容如下:

    • 1、修改两个配置:store.session.mode、``store.mode的值为db`
    • 2、接着我们对数据库信息进行一下配置:
      • 数据库驱动(8.0的需要修改):com.mysql.cj.jdbc.Driver
      • 数据库URL:默认就是seata数据库就好。
      • 数据库用户名密码:store.db.userstore.db.password

    6.2、创建一个数据库【seata】:

    image-20220803151707925

    -- -------------------------------- The script used when storeMode is 'db' --------------------------------
    -- the table to store GlobalSession data
    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(255),
        `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 = InnoDB
      DEFAULT 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 = InnoDB
      DEFAULT 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`)
    ) ENGINE = InnoDB
      DEFAULT 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 = InnoDB
      DEFAULT CHARSET = utf8mb4;
    
    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('HandleAllSession', ' ', 0);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    那么配置就已经完成了!


    2.3.2、测试

    前提准备

    启动nacos服务、seata服务如下:

    image-20220803162049068

    执行seata启动的命令:

    seata-server.bat -p 8868
    
    • 1

    image-20220803162158904

    启动三个服务:

    image-20220803162323674

    与此同时,可以看到在seata服务的控制台中你可以看到里面的服务注册信息:

    image-20220803162345887

    测试

    访问借阅地址:http://localhost:8082/borrow/1/2

    image-20220803162724864

    再此访问,肯定在中途去判断是否该用户借阅了书阶段出现异常,进行回滚,我们只需要关注book表中的书籍借阅数量有没有-1的问题,其实就是看其有没有回滚:

    image-20220803162900990

    看下数据库:

    image-20220803162923054

    没有问题!


    2.3.3、debug

    在这里我们来进行打上断点:

    image-20220803163021273

    看看seata数据库以及我们自己本身的数据库undo_log中的记录是否产生变化:

    接着来了一个请求,我们看debug的目前阶段:

    image-20220803163540694

    此时来看数据库的情况:

    seata数据库:记录依次是红框从上往下

    image-20220803163300534

    image-20220803163148348

    image-20220803163205472

    image-20220803163231371

    seata-demo中的undo_log表

    image-20220803163140302

    可以看到用户表中的xid是依赖于tc也就是seata-server来进行回滚的。


    参考资料

    [1]. SpringCould笔记(二)微服务进阶 Cloud Alibaba

    我是长路,感谢你的耐心阅读。如有问题请指出,我会积极采纳!
    欢迎关注我的公众号【长路Java】,分享Java学习文章及相关资料
    Q群:851968786 我们可以一起探讨学习
    注明:转载可,需要附带上文章链接

  • 相关阅读:
    线性代数-Python-02:矩阵的基本运算 - 手写Matrix及numpy中的用法
    静态代理模式
    CEX的梅克尔树储备证明是什么?
    GaussDB数据库SQL系列-子查询
    初识Canal以及使用Docker安装配置
    硬件系统工程师宝典(44)-----差分信号走线“相位失配”怎么破?
    【面试经典 150 | 滑动窗口】串联所有单词的子串
    C++ Reference: Standard C++ Library reference: C Library: cwchar: vswprintf
    Uniapp导出的iOS应用上架详解
    ERD Online 4.0.3数据库在线建模(免费、更美、更稳定)
  • 原文地址:https://blog.csdn.net/cl939974883/article/details/126149627