SpringCloud Alibaba之 Seata与分布式事务

文章目录

  • 一、项目改造升级
  • 二、分布式事务解决方案
    • 2.1 XA分布式事务协议 - 2PC(两阶段提交实现)
    • 2.2 XA分布式事务协议 - 3PC(三阶段提交实现)
    • 2.3 TCC(补偿事务)
  • 三、Seata
    • 3.1 使用 file 模式部署
      • ①服务端部署
      • ②客户端挂载
      • ③配置 undo_log 数据源
      • ④开启分布式事务
    • 3.2 使用 nacos 模式部署
      • ①修改 seata 的 registry.conf
      • ②向 Nacos 导入 seata 配置
      • ③向 Nacos 添加事务组映射配置
      • ④客户端挂载
      • ⑤事务会话信息的存储
  • 四、思考


提示:以下是本篇文章正文内容,SpringCloud Alibaba 系列学习将会持续更新

SpringCloud Alibaba之 Seata与分布式事务_第1张图片

一、项目改造升级

这里我们对我们之前的图书管理系统进行升级:

  • 每个用户最多只能同时借阅 2 本书。
  • 图书馆中每种书都只有 3 本。
  • 用户借书流程:先调用图书服务书籍数量-1 -> 添加借阅记录 -> 调用用户服务用户可借阅数量-1
    在这里插入图片描述

1.1 数据库表修改

那么首先我们对数据库进行修改,这里为了简便,就直接在 user 表中添加一个字段 book_count (剩余的借阅次数):
SpringCloud Alibaba之 Seata与分布式事务_第2张图片

然后修改 book 表,也是直接添加一个字段 count (图书剩余数量):
在这里插入图片描述

回到目录…


1.2 userservice

@Mapper
@Repository
public interface UserMapper {
    @Select("select * from user where uid = #{uid}")
    User getUserById(int uid);

    @Select("select remain_count from user where uid = #{uid}")
    int getUserBookRemain(int uid);

    @Update("update user set remain_count = #{count} where uid = #{uid}")
    int updateBookCount(int uid, int count);
}
@Service
public class UserServiceImpl implements UserService {
    @Resource
    private UserMapper userMapper;

    @Override
    public User getUserById(int uid)  {
        return userMapper.getUserById(uid);
    }

    @Override
    public int getRemain(int uid) {
        return userMapper.getUserBookRemain(uid);
    }

    @Override
    public boolean setRemain(int uid, int count) {
        return userMapper.updateBookCount(uid, count) > 0;
    }
}
@RestController
public class UserController {
    @Resource
    private UserService userService;

    @GetMapping("/user/{uid}")
    public User findUserById(@PathVariable("uid") int uid) {
        return userService.getUserById(uid);
    }

    @GetMapping("/user/remain/{uid}")
    public int userRemain(@PathVariable("uid") int uid){
        return userService.getRemain(uid);
    }

    @GetMapping("/user/borrow/{uid}")
    public boolean userBorrow(@PathVariable("uid") int uid){
        int remain = userService.getRemain(uid);
        return userService.setRemain(uid, remain - 1);
    }
}

回到目录…


1.3 bookservice

@Mapper
@Repository
public interface BookMapper {
    @Select("select * from book where bid = #{bid}")
    Book getBookById(int bid);

    @Select("select count from book  where bid = #{bid}")
    int getRemain(int bid);

    @Update("update book set count = #{count}  where bid = #{bid}")
    int setRemain(int bid, int count);
}
@Service
public class BookServiceImpl implements BookService {
    @Resource
    private BookMapper bookMapper;
    
    @Override
    public Book getBookById(int bid) {
        return bookMapper.getBookById(bid);
    }

    @Override
    public boolean setRemain(int bid, int count) {
        return bookMapper.setRemain(bid, count) > 0;
    }

    @Override
    public int getRemain(int bid) {
        return bookMapper.getRemain(bid);
    }
}
@RestController
public class BookController {
    @Resource
    private BookService bookService;

    @GetMapping("/book/{bid}")
    public Book findBookById(@PathVariable("bid") int bid) {
        return bookService.getBookById(bid);
    }

    @GetMapping("/book/remain/{bid}")
    public int bookRemain(@PathVariable("bid") int uid){
        return bookService.getRemain(uid);
    }

    @GetMapping("/book/borrow/{bid}")
    public boolean bookBorrow(@PathVariable("bid") int uid){
        int remain = bookService.getRemain(uid);
        return bookService.setRemain(uid, remain - 1);
    }
}

回到目录…


1.4 borrowservice

添加:新增借阅信息的SQL

@Mapper
@Repository
public interface BorrowMapper {
    @Select("select * from borrow where uid = #{uid}")
    List<Borrow> getBorrowsByUid(int uid);
    @Select("select * from borrow where bid = #{bid}")
    List<Borrow> getBorrowsByBid(int bid);
    @Select("select * from borrow where uid = #{uid} and bid = #{bid}")
    Borrow getBorrow(int uid, int bid);
    
    @Insert("insert into borrow (uid, bid) value (#{uid}, #{bid})")
    int addBorrow(int uid, int bid);
}

远程调用的客户端接口

@FeignClient(value = "userservice")
public interface UserClient {
    @GetMapping("/user/{uid}")
    User findUserById(@PathVariable("uid") int uid);

    @GetMapping("/user/borrow/{uid}")
    boolean userBorrow(@PathVariable("uid") int uid);

    @GetMapping("/user/remain/{uid}")
    int userRemain(@PathVariable("uid") int uid);
}
@FeignClient(value = "bookservice")
public interface BookClient {
    @GetMapping("/book/{bid}")
    Book findBookById(@PathVariable("bid") int bid);

    @GetMapping("/book/borrow/{bid}")
    boolean bookBorrow(@PathVariable("bid") int bid);

    @GetMapping("/book/remain/{bid}")
    int bookRemain(@PathVariable("bid") int bid);
}

添加:新增借阅信息的业务

@Service
public class BorrowServiceImpl implements BorrowService {
    @Resource
    private BorrowMapper borrowMapper;
    @Resource
    private UserClient userClient;
    @Resource
    private BookClient bookClient;

    @Override
    public UserBorrowView getBorrowViewByUid(int uid) {
        List<Borrow> borrowList = borrowMapper.getBorrowsByUid(uid);
        User user = userClient.findUserById(uid);
        List<Book> bookList = borrowList
                .stream()
                .map(b -> bookClient.findBookById(b.getBid()))
                .collect(Collectors.toList());
        return new UserBorrowView(user, bookList);
    }

    @Override
    public boolean doBorrow(int uid, int bid) {
        //1. 判断图书和用户是否都支持借阅
        if(bookClient.bookRemain(bid) < 1)
            throw new RuntimeException("图书数量不足");
        if(userClient.userRemain(uid) < 1)
            throw new RuntimeException("用户借阅量不足");
        //2. 首先将图书的数量-1
        if(!bookClient.bookBorrow(bid))
            throw new RuntimeException("在借阅图书时出现错误!");
        //3. 添加借阅信息
        if(borrowMapper.getBorrow(uid, bid) != null)
            throw new RuntimeException("此书籍已经被此用户借阅了!");
        if(borrowMapper.addBorrow(uid, bid) <= 0)
            throw new RuntimeException("在录入借阅信息时出现错误!");
        //4. 用户可借阅-1
        if(!userClient.userBorrow(uid))
            throw new RuntimeException("在借阅时出现错误!");
        //完成
        return true;
    }
}

添加:新增借阅信息的接口

@RestController
public class BorrowController {
    @Resource
    private BorrowService borrowService;
    @GetMapping("/borrow/{uid}")
    public UserBorrowView findUserBorrows(@PathVariable("uid") int uid) {
        return borrowService.getBorrowViewByUid(uid);
    }

    @GetMapping("/borrow/take/{uid}/{bid}")
    JSONObject borrow(@PathVariable("uid") int uid,
                      @PathVariable("bid") int bid){
        borrowService.doBorrow(uid, bid);

        JSONObject object = new JSONObject();
        object.put("code", "200");
        object.put("success", false);
        object.put("message", "借阅成功!");
        return object;
    }
}

这样,只要我们的图书借阅过程中任何一步出现问题,都会抛出异常。

回到目录…


1.5 启动项目,测试

以前的业务没有任何问题:
SpringCloud Alibaba之 Seata与分布式事务_第3张图片

此时,我们测试借阅书籍的业务,当 book.count > 0 && user.remain_count > 0 时,借阅成功,且数据库信息正确更新!
在这里插入图片描述

用户重复借同一本书 时,借阅失败,说明我们的业务逻辑没有问题,数据库中的唯一索引也发挥了作用。
SpringCloud Alibaba之 Seata与分布式事务_第4张图片
在这里插入图片描述

但是此时数据库中的信息已经发生改变,没有回滚到借阅前的状态!
在这里插入图片描述
这就破坏了数据库一致性原则。而且由于我们的不同微服务之间使用的并不是同一个数据库,所以传统的 @Transactional 注解无效,这时就得借助 Seata 提供分布式事务了。

回到目录…
在这里插入图片描述

二、分布式事务解决方案

上面这种情况就是一种多服务多数据源的分布式事务模型(比较常见)。因此,为了解决这种情况,我们就得实现分布式事务,让这整个流程保证原子性。我们得先从理论上开始下手,我们来了解一下常用的分布式事务解决方案。

2.1 XA分布式事务协议 - 2PC(两阶段提交实现)

这里的 PC 实际上指的是 PrepareCommit,也就是说它分为两个阶段:准备提交,整个过程的参与者一共有两个角色:事务的执行者和事务的协调者,实际上整个分布式事务的运作都需要依靠协调者来维持:
SpringCloud Alibaba之 Seata与分布式事务_第5张图片

在准备和提交阶段,会进行:

  1. 准备阶段:
    ①一个分布式事务是由协调者来开启的,首先协调者会向所有的事务执行者发送事务内容,等待所有的事务执行者答复。
    ②各个事务执行者开始执行事务操作,但是不进行提交,并将undo和redo信息记录到事务日志中。
    ③如果事务执行者执行事务成功,那么就告诉协调者成功 Yes,否则告诉协调者失败 No,不能提交事务。
  2. 提交阶段:
    ①当所有的执行者都反馈完成之后,进入第二阶段。
    ②协调者会检查各个执行者的反馈内容,如果所有的执行者都返回成功,那么就告诉所有的执行者可以提交事务了,最后再释放锁资源。
    ③如果有至少一个执行者返回失败或是超时,那么就让所有的执行者都回滚,分布式事务执行失败。

虽然这种方式看起来比较简单,但是存在以下几个问题:

  • 事务协调者是非常核心的角色,一旦出现问题,将导致整个分布式事务不能正常运行。
  • 如果提交阶段发生网络问题,导致某些事务执行者没有收到协调者发来的提交命令,将导致某些执行者提交某些执行者没提交,这样肯定是不行的。

回到目录…

2.2 XA分布式事务协议 - 3PC(三阶段提交实现)

三阶段提交是在二阶段提交基础上的改进版本,主要是加入了超时机制,同时在协调者和执行者中都引入了超时机制。

三个阶段分别进行:

  1. CanCommit 阶段:
    ①协调者向执行者发送 CanCommit 请求,询问是否可以执行事务提交操作,然后开始等待执行者的响应。
    ②执行者接收到请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态,否则返回No

  2. PreCommit 阶段:
    ①协调者根据执行者的反应情况来决定是否可以进入第二阶段事务的 PreCommit 操作。

    ②如果所有的执行者都返回 Yes,则协调者向所有执行者发送 PreCommit 请求,并进入 Prepared 阶段,执行者接收到请求后,会执行事务操作,并将 undo 和 redo 信息记录到事务日志中,如果成功执行,则返回成功响应。

    ③如果所有的执行者至少有一个返回 No,则协调者向所有执行者发送 abort 请求,所有的执行者在收到请求或是超过一段时间没有收到任何请求时,会直接中断事务。

  3. DoCommit 阶段: 该阶段进行真正的事务提交。
    ①协调者接收到所有执行者发送的成功响应,那么他将从 PreCommit 状态进入到 DoCommit 状态,并向所有执行者发送 doCommit 请求,执行者接收到 doCommit 请求之后,开始执行事务提交,并在完成事务提交之后释放所有事务资源,并最后向协调者发送确认响应,协调者接收到所有执行者的确认响应之后,完成事务。

    ②如果因为网络问题导致执行者没有收到 doCommit 请求,执行者会在超时之后直接提交事务,虽然执行者只是猜测协调者返回的是 doCommit 请求,但是因为前面的两个流程都正常执行,所以能够在一定程度上认为本次事务是成功的,因此会直接提交。

    ③协调者没有接收至少一个执行者发送的成功响应(也可能是响应超时),那么就会执行中断事务,协调者会向所有执行者发送 abort 请求,执行者接收到 abort 请求之后,利用其在 PreCommit 阶段记录的 undo 信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源,执行者完成事务回滚之后,向协调者发送确认消息, 协调者接收到参与者反馈的确认消息之后,执行事务的中断。

相比两阶段提交,三阶段提交的优势是显而易见的,当然也有缺点:

  • 3PC 在 2PC 的第一阶段和第二阶段中插入一个准备阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。
  • 一旦参与者无法及时收到来自协调者的信息之后,会默认执行 Commit,这样就不会因为协调者单方面的故障导致全局出现问题。
  • 但是我们知道,实际上超时之后的 Commit 决策本质上就是一个赌注罢了,如果此时协调者发送的是 abort 请求超时导致执行者未接收,那么就会直接导致数据一致性问题。

回到目录…

2.3 TCC(补偿事务)

补偿事务TCC 就是 TryConfirmCancel,它对业务有侵入性,一共分为三个阶段:

  1. Try 阶段:
    比如我们需要在借书时,将书籍的库存 -1,并且用户的借阅量也 -1,但是这个操作,除了直接对库存和借阅量进行修改之外,还需要将减去的值,单独存放到冻结表中,但是此时不会创建借阅信息,也就是说只是预先把关键的东西给处理了,预留业务资源出来。

  2. Confirm 阶段:
    如果 Try 执行成功无误,那么就进入到 Confirm 阶段,接着之前,我们就该创建借阅信息了,只能使用 Try 阶段预留的业务资源,如果创建成功,那么就对 Try 阶段冻结的值,进行解冻,整个流程就完成了。当然,如果失败了,那么进入到 Cancel 阶段。

  3. Cancel 阶段:
    不用猜了,那肯定是把冻结的东西还给人家,因为整个借阅操作压根就没成功。就像你付了款买了东西但是网络问题,导致交易失败,钱不可能不还给你吧。

跟 XA 协议相比,TCC 就没有协调者这一角色的参与了,而是自主通过上一阶段的执行情况来确保正常,充分利用了集群的优势,性能也是有很大的提升。但是缺点也很明显,它与业务具有一定的关联性,需要开发者去编写更多的补偿代码,同时并不一定所有的业务流程都适用于这种形式。

回到目录…
在这里插入图片描述

三、Seata

SpringCloud Alibaba 为我们提供了用于处理分布式事务的组件 Seata,官方文档:https://seata.io/zh-cn/docs/overview/what-is-seata.html

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

官网给出的是这样的一个架构图,那么图中的RM、TM、TC代表着什么意思呢?SpringCloud Alibaba之 Seata与分布式事务_第6张图片

  • RM(Resource Manager):用于直接执行本地事务的提交和回滚。
  • TM(Transaction Manager):TM是分布式事务的核心管理者。比如现在我们需要在借阅服务中开启全局事务,来让其自身、图书服务、用户服务都参与进来,也就是说一般全局事务发起者就是TM。
  • TC(Transaction Manager):这个就是我们的 Seata 服务器,用于全局控制,比如在 XA 模式下就是一个协调者的角色,而一个分布式事务的启动就是由 TM 向 TC 发起请求,TC 再来与其他的 RM 进行协调操作。

流程:

  1. TM 请求 TC 开启一个全局事务,TC 会生成一个 XID 作为该全局事务的编号,XID 会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起;
  2. RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联;
  3. TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚;
  4. TC 驱动 RM 将 XID 对应的自己的本地事务进行提交还是回滚;

Seata 支持 4 种事务模式:

  1. AT:本质上就是 2PC 的升级版,在 AT 模式下,用户只需关心自己的 “业务SQL”。

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

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

  4. Saga:用于处理长事务,每个执行者需要实现事务的正向操作和补偿操作:
    SpringCloud Alibaba之 Seata与分布式事务_第7张图片

以AT模式为例,Seata 客户端是通过对数据源进行代理实现的,使用的是 DataSourceProxy 类,所以我们只需要将对应的代理类注册为 Bean 即可(0.9版本之后支持自动进行代理,不用我们手动操作)

接下来,我们就以 AT 模式为例进行讲解…

回到目录…

3.1 使用 file 模式部署

Seata 也是以服务端形式进行部署的,然后每个服务都是客户端。

服务端下载地址:https://github.com/seata/seata/releases/download/v1.4.2/seata-server-1.4.2.zip

源码也下载一下:https://github.com/seata/seata/archive/refs/heads/develop.zip

①服务端部署

下载完成之后,放入到 IDEA 项目目录中,添加启动配置,这里端口使用 8868:
SpringCloud Alibaba之 Seata与分布式事务_第8张图片
Seata 服务端支持本地部署或是基于注册发现中心部署(比如Nacos、Eureka等),这里我们首先演示一下最简单的本地部署,不需要对 Seata 的配置文件做任何修改。

Seata 存在着事务分组机制:

  • 事务分组:seata 的资源逻辑,可以按微服务的需要,在应用程序(客户端)对自行定义事务分组,每组取一个名字。
  • 集群:seata-server 服务端一个或多个节点组成的集群 cluster。 应用程序(客户端)使用时需要指定事务逻辑分组与 Seata 服务端集群(默认为default)的映射关系。

为啥要设计成通过事务分组再直接映射到集群?干嘛不直接指定集群呢?

  • 这样设计后,事务分组可以作为资源的逻辑隔离单位,出现某集群故障时可以快速 failover,只切换对应分组,可以把故障缩减到服务级别,但前提也是你有足够 server 集群。

②客户端挂载

接着我们需要将我们的各个服务作为 Seate 的客户端,只需要导入依赖即可:

<dependency>
    <groupId>com.alibaba.cloudgroupId>
    <artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>

然后添加配置:

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

这样就可以直接启动了,但是注意现在只是单纯地连接上,并没有开启任何的分布式事务。

③配置 undo_log 数据源

我们前面说了,Seata 会分析修改数据的 sql,同时生成对应的反向回滚 SQL,这个回滚记录会存放在 undo_log 表中。所以要求每一个 Client 的数据库都有一个对应的 undo_log 表(也就是说每个服务连接的数据库都需要创建这样一个表,这里由于我们三个服务都用的同一个数据库,所以说就只用在这个数据库中创建 undo_log 表即可),表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;

在这里插入图片描述

④开启分布式事务

现在我们接着来配置开启分布式事务,首先在3个微服务启动类添加 @EnableAutoDataSourceProxy 注解,此注解会添加一个后置处理器将数据源封装为支持分布式事务的代理数据源(虽然官方表示配置文件中已经默认开启了自动代理,但是实测1.4.2版本下只能打注解的方式才能生效):

@EnableAutoDataSourceProxy
@SpringBootApplication
public class BookApplication {
    public static void main(String[] args) {
        SpringApplication.run(BookApplication.class, args);
    }
}

接着我们需要在开启分布式事务的方法上添加 @GlobalTransactional 注解:

@GlobalTransactional
@Override
public boolean doBorrow(int uid, int bid) {
    if(bookClient.bookRemain(bid) < 1)
        throw new RuntimeException("图书数量不足");
    if(userClient.userRemain(uid) < 1)
        throw new RuntimeException("用户借阅量不足");
    if(!bookClient.bookBorrow(bid))
        throw new RuntimeException("在借阅图书时出现错误!");
    if(mapper.getBorrow(uid, bid) != null)
        throw new RuntimeException("此书籍已经被此用户借阅了!");
    if(mapper.addBorrow(uid, bid) <= 0)
        throw new RuntimeException("在录入借阅信息时出现错误!");
    if(!userClient.userBorrow(uid))
        throw new RuntimeException("在借阅时出现错误!");
    return true;
}

我们试着再次让用户借阅重复的图书,看看是否可以成功回滚:
SpringCloud Alibaba之 Seata与分布式事务_第9张图片

borrowservice 控制台:
SpringCloud Alibaba之 Seata与分布式事务_第10张图片

bookservice 控制台: xid 一致,说明是同一全局事务
在这里插入图片描述

数据库信息前后一致,成功回滚!
在这里插入图片描述

回到目录…

3.2 使用 nacos 模式部署

前面我们实现了本地 Seata 服务的 file 模式部署,现在我们来看看如何让其配合 Nacos 进行部署,利用 Nacos 的配置管理和服务发现机制,Seata 能够更好地工作。

①修改 seata 的 registry.conf

a. 我们先单独为 Seata 配置一个命名空间,用于管理 seata 的相关信息:
在这里插入图片描述

b. 我们打开 seata/conf 目录中的 registry.conf 配置文件:修改 seata 的注册点 和 配置文件源

# 负责 seata 的注册
registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = "4e15f24d-81ff-482f-a1ae-33eecbec2d46"
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
    aclToken = ""
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

# 负责 seata 的配置文件
config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = "4e15f24d-81ff-482f-a1ae-33eecbec2d46"
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties"
  }
  consul {
    serverAddr = "127.0.0.1:8500"
    aclToken = ""
  }
  apollo {
    appId = "seata-server"
    ## apolloConfigService will cover apolloMeta
    apolloMeta = "http://192.168.1.204:8801"
    apolloConfigService = "http://192.168.1.204:8080"
    namespace = "application"
    apolloAccesskeySecret = ""
    cluster = "seata"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
    nodePath = "/seata/seata.properties"
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

回到目录…

②向 Nacos 导入 seata 配置

接着,我们需要将配置导入到 Nacos 中,让 Nacos 管理配置,这样我们就可以对配置进行热更新了,一旦环境需要变化,只需要直接在 Nacos 中修改即可。

我们打开一开始下载的源码script/config-center/nacos目录,这是官方提供的上传脚本,我们直接运行 nacos-config-interactive.sh 脚本即可。(windows 下没对应的 bat,但是我们可以使用 git 命令来执行,一样可以成功)这里我们使用这个可交互的版本:
SpringCloud Alibaba之 Seata与分布式事务_第11张图片

导入成功之后,可以在对应的命名空间下看到对应的配置。(为啥非要一个一个配置项单独搞,就不能写一起吗)
SpringCloud Alibaba之 Seata与分布式事务_第12张图片

回到目录…

③向 Nacos 添加事务组映射配置

我们需要将对应的事务组映射配置也添加上,DataId 格式为service.vgroupMapping.事务组名称,比如我们就使用默认的名称,值全部依然使用 default 即可:

SpringCloud Alibaba之 Seata与分布式事务_第13张图片

最终,我们也可以看到新增的配置:
SpringCloud Alibaba之 Seata与分布式事务_第14张图片

回到目录…

④客户端挂载

现在我们就完成了服务端的 Nacos 配置,接着我们需要对客户端也进行 Nacos配置!

a. 修改客户端的 application.yml 配置文件:

seata:
	# 注册
  registry:
  	# 使用 Nacos
    type: nacos
    nacos:
    	# 使用 Seata 的命名空间,这样才能正确找到 Seata 服务,由于组使用的是 SEATA_GROUP,配置默认值就是,就不用配了
      namespace: 4e15f24d-81ff-482f-a1ae-33eecbec2d46
      username: nacos
      password: nacos
  # 配置
  config:
    type: nacos
    nacos:
      namespace: 4e15f24d-81ff-482f-a1ae-33eecbec2d46
      username: nacos
      password: nacos

b. 现在我们就可以启动这三个服务了,可以在 Nacos 中看到 Seata 以及三个服务都正常注册了:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
SpringCloud Alibaba之 Seata与分布式事务_第15张图片

c. 接着我们就可以访问一下服务试试看了:
在这里插入图片描述
SpringCloud Alibaba之 Seata与分布式事务_第16张图片
可以看到效果和上面是一样的,不过现在我们的注册和配置都继承在 Nacos 中进行了。

回到目录…

⑤事务会话信息的存储

我们还可以配置一下事务会话信息的存储方式,默认是 file 类型,那么就会在运行目录下创建file_store目录,我们可以将其搬到数据库中存储,只需要修改一下配置即可:
SpringCloud Alibaba之 Seata与分布式事务_第17张图片

a. 我们直接在 Nacos 管理的 seata 配置列表中进行修改:

  1. store.mode 的值修改为 db
  2. store.session.mode 的值修改为 db
  3. 数据库驱动,store.db.driverClassName (根据自己数据库版本改,MySQL5.7 可以不改)
  4. 数据库URL,store.db.url (根据自己数据库版本改,MySQL5.7 可以不改)
  5. 数据库用户名和密码,store.db.userstore.db.password

b. 创建 seata 数据库,然后执行下面的建表语句:

-- -------------------------------- 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(128),
    `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);

在这里插入图片描述
完成之后,重启 Seata 服务端即可:
在这里插入图片描述
看到了数据源初始化成功,现在已经在使用数据库进行会话存储了。

如果 Seata 服务端出现报错,可能是我们自定义事务组的名称太长了:
在这里插入图片描述
globle_table 表的字段 transaction_server_group 长度适当增加一下即可:VARCHAR(255)

到此,关于基于 nacos 模式下的 Seata 部署,就完成了。

四、思考

虽然我们这里实现了分布式事务,但是还是给各位同学提出一个问题(可以把自己所认为的结果打在弹幕上),就我们目前这样的程序设计,在高并发下,真的安全吗?比如同一时间 100 个同学抢同一个书,但是我们知道同一个书就只有 3 本,如果这时真的同时来了 100 个请求要借书,会正常地只借出 3 本书吗?如果不正常,该如何处理?

  • 我们可以设置分布式锁 ,可以参考往期文章:Redis 事务和锁
  • 那么 Java 中如何加 Redis 分布式锁呢?可以参考往期文章:Redis在Java中的应用
在这里插入图片描述 回到目录......

总结:
提示:这里对文章进行总结:
本文是对Seata分布式事务服务的学习,首先介绍了3种分布式事务的解决方案,学习了Seata默认的AT事务模式,以及seata的两种部署方式:file模式和nacos模式。之后的学习内容将持续更新!!!

你可能感兴趣的:(SpringCloud,spring,cloud,分布式,seata,分布式事务,nacos)