Skip to content

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

当前项目环境版本:springboot 2.3.12.RELEASE、springcloud alibaba 2.2.7.RELEASE、SpringCloud Hoxton-SR12。

1.认识 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:用于处理长事务,每个执行者需要实现事务的正向操作和补偿操作:

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

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

img

img

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

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

项目介绍

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

img

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

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

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

看一下数据库表:

img

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

img

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

img

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

img

Nacos 服务创建命名空间

创建命名空间为 seata-demo:

img

book-service 服务

img

引入依赖:

xml
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.0</version>
    </dependency>
</dependencies>

1、yaml 配置项:application.yaml

yml
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

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

java
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;
}
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.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>
java
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;

/**
 * <p>
 *  Mapper 接口
 * </p>
 *
 * @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);

}

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

java
package com.changlu.seatabookservcie.service;


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

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author ChangLu
 * @since 2022-08-02
 */
public interface BookService extends IService<BookModel> {


    int bookRemain(Integer id);

    int minusBookRemain(Integer id);

}
java
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;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @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);
    }
}

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

java
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;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @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);
    }

}

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

java
@MapperScan("com.changlu.seatabookservcie.mapper")
@EnableDiscoveryClient

user-service 服务

img

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

1、配置文件:application.yaml

yml
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

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

java
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;

/**
 * <p>
 *
 * </p>
 *
 * @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;
}
java
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;

/**
 * <p>
 *  Mapper 接口
 * </p>
 *
 * @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);

}
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.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>

3、service 接口:

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

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author ChangLu
 * @since 2022-08-02
 */
public interface UserService extends IService<UserModel> {


    int getUserRemainBook(Integer uid);

    int minusUserBookCount(Integer uid);

}
java
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;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @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);
    }
}

4、控制器:

java
package com.changlu.seatauserservice.controller;


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

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @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);
    }

}

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

java
@MapperScan("com.changlu.seatauserservice.mapper")
@EnableDiscoveryClient

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

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

img

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

xml
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependencyManagement>
     <dependencies>
         <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-dependencies</artifactId>
             <version>${spring-cloud.version}</version>
             <type>pom</type>
             <scope>import</scope>
         </dependency>
     </dependencies>
</dependencyManagement>

1、配置文件:applicaion.yaml

yml
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

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

java
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;
}
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.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>
java
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;

/**
 * <p>
 *  Mapper 接口
 * </p>
 *
 * @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);

}

3、两个服务的 feign 接口:

java
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);

}
java
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);

}

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

java
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);
}
java
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;
    }
}

5、控制器:

java
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;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @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;
    }
}

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

java
@MapperScan("com.changlu.seataborrowservice.mapper")
@EnableDiscoveryClient
@EnableFeignClients

问题复现测试

提前准备

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

img

img

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

img

img

img

开始测试

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

img

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

img

img

img

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

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

img

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

img

来看下当前的数据库吧:

img

img

img

可以看到红色横线的部分就是未回滚的 book-service 服务,如何解决这类问题呢?

我们可以集成阿里的 Seata 组件来进行尝试!

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

2.2.1、启动 seata-server

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

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

下载解压后的目录如下:

img

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

img

java
seata-server.bat -p 8868

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

img

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

img

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

1、引入 seata 依赖:

xml
<!--    引入seata依赖    -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

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

img

seata-user-service:

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

seata-book-service:

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

seata-borrow-service:

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

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

java
@EnableAutoDataSourceProxy //开启seata事务配置

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

  • 由于三个服务都使用的一个数据库 seata-demo,所以我们直接在一个数据库中创建即可
sql
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;

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

java
@GlobalTransactional

img

2.2.3、测试

提前准备

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

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

img

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

img

开始测试

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

img

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

img

img

img

再次来进行借阅下:

img

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

img

img

img

debug

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

img

看下 undo_log 表:

img

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

img

再次看下 undo_log 表:

img

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

2.3、采用 nacos 模式来集成 seata 服务

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

2.3.1、配置完整步骤

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

img

2、修改 seata 的配置文件

registry.conf:

yml
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"
  }
}

==3、将 seata 的 registry.conf 配置文件导入到 web 终端的 nacos 中==

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

下载地址:seata 开发包

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

这里我们直接进入到下载的 seate 包中的 nacos 目录下执行已有的四个脚本,在 windows 中我们使用 gitBash 命令行工具来执行:

img

java
./nacos-config-interactive.sh

img

最终效果如下:所有的 seata 配置文件都上传到 nacos 了!

img

4、手动在命名空间 seata 中再添加三个 nacos 客户端的配置项

img

内容写 default 即可!

java
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

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

这里的配置必须和 seata 端的配置,即 registry.conf 配置文件是一样的!

yml
# 2、nacos模式
seata:
  # 注册
  registry:
    # 使用Nacos,不使用file
    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

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

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

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

img

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

注:实际上修改的就是 seata 的 registry.conf 配置文件,但是现在我们把它上传到了 nacos 中,所以现在只需要在 web 端的 nacos 中修改即可,并且在上传时是将这个文件打散的,每一行配置都单独作为一项!

  • 1、修改两个配置项:store.session.mode的值为dbstore.mode的值为db(在 nacos 中搜索)

  • 2、接着我们对数据库信息进行一下配置项:(在 nacos 中搜索)

    • 数据库驱动(8.0 的需要修改)store.db.driverClassName:com.mysql.cj.jdbc.Driver
    • 数据库 URLstore.db.url:默认就是 seata 数据库就好。
    • 数据库用户名密码 store.db.user、store.db.password :都为 seata 即可。

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

img

sql
-- -------------------------------- 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);

那么配置就已经完成了!

后面运行时产生的会话记录都会存在数据库中,而不是运行目录下的文件中了!

2.3.2、测试

前提准备

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

img

执行 seata 启动的命令:

java
seata-server.bat -p 8868

img

启动三个服务:

img

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

img

测试

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

img

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

img

看下数据库:

img

没有问题!

2.3.3、debug

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

img

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

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

img

此时来看数据库的情况:

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

img

img

img

img

seata-demo 中的 undo_log 表

img

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

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

注意

除了分布式事务,还有分布式锁,两者都要配置,才能保证分布式的绝对安全!

思考问题:如果 100 个人同时抢三本书,这时候光有分布式事务能保证安全吗?

分布式锁和分布式事务的区别: 分布式事务只能保证“一系列的操作的原子性”,但是一些细节,比如一开始读到“书

本数量>0”,然后执行减一。在两台机子同时执行这个动作,当采用的是“单机数据库 select”快照读,会导致最终书

本结果可能为负数。

总而言之,分布式锁保证“多个进程”之间的并发问题,只能少数进程拿到锁执行-1 操作。而分布式事务就是保证 “一

系列操作”统一执行完成