文章目录
前言
本节配套案例代码:Gitee仓库、Github仓库
所有博客文件目录索引:博客目录索引(持续更新)
学习视频:SpringCloud 教程 已完结(IDEA 2022.1最新版)4K蓝光画质 微服务开发
PS:本章节中部分图片是直接引用学习课程课件,如有侵权,请联系删除。
当前项目环境版本:springboot 2.3.12.RELEASE
、springcloud alibaba 2.2.7.RELEASE
、SpringCloud Hoxton-SR12
。
一、Seata介绍
1.1、认识Seata
SpringCloud Alibaba为我们提供了用于处理分布式事务的组件Seata。
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”
- 一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
- 二阶段如果确认提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可,当然如果需要回滚,那么就用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
- TCC:和我们上面讲解的思路是一样的。
- XA:同上,但是要求数据库本身支持这种模式才可以。
- Saga:用于处理长事务,每个执行者需要实现事务的正向操作和补偿操作:
二、实战:集成Seata实现分布式事务(AT模式)
说明:demo项目其中的部分代码写的比较简陋,我们将重心放在分布式事务上即可!
对于2.1部分的代码我已经进行了打包:在对应gitee或github仓库的指定位置下载即可,可直接复现分布式事务问题!
2.1、本地项目搭建(复现分布式事务问题)
项目介绍
为了能够集成Seata组件来实现分布式事务数据一致性的效果,来构建多个微服务进行远程调用。
本次使用到的分布式组件包含:nacos(注册中心)、feign(远程调用组件)
服务包含:book-service(图书服务)、borrow-service(借阅服务)、user-service(用户服务)。
事务问题描述(目标复现):在borrow-service服务中会在一个service中会执行本地事务,远程调用book-service以及user-service的接口,这两个服务的接口都与数据库有交互操作,在没有使用Seata组件前,若是其中某个服务出现异常,那么之前提交的操作都不能够进行回滚,因为这涉及到多个不同的事务管理器。
看一下数据库表:
db_book
:图书表,其中count表示该数的库存数量。对应的是book-service。
db_borrow
:借阅表。对应的是borrow-service。
db_user
:用户表。对应的是user-service。
Nacos服务创建命名空间
创建命名空间为seata-demo:
book-service服务
引入依赖:
<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
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:
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;
}
<?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>
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
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);
}
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、控制器,对外暴露两个接口,一个是查询以及一个更改数据:
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扫描:启动器上添加
@MapperScan("com.changlu.seatabookservcie.mapper")
@EnableDiscoveryClient
user-service服务
引入的依赖与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:
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;
/**
* <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;
}
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 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接口:
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);
}
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、控制器:
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一致
@MapperScan("com.changlu.seatauserservice.mapper")
@EnableDiscoveryClient
borrow-service服务(分布式事务问题产生见其中service方法)
在borrow-service服务中,还包含有feign组件,该服务会对book-service、user-service服务来进行远程调用,那么本次服务的分布式事务问题也是从这里产生的!
引入依赖:与前面服务一致同样也有nacos注册依赖,唯一多了一个就是feign组件
<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
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:
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;
}
<?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>
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接口:
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);
}
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方法:
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);
}
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、控制器:
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包扫描增强
@MapperScan("com.changlu.seataborrowservice.mapper")
@EnableDiscoveryClient
@EnableFeignClients
问题复现测试
提前准备
ok,此时我们的项目环境搭建已经完成,此时就来启动nacos以及我们的三个服务,来进行接口测试吧!
我们来看下数据库当前的一些数据信息:每本书的库存是3本,借阅记录当前没有,用户借阅次数是3次
开始测试
我们来访问borrow-service接口:http://localhost:8082/borrow/1/2
可以看到借阅成功!此时看一下数据库的信息:
可以看到西游记库存扣减1,借阅记录+1,用户借阅书籍数量-1,没有问题,那么我们此时再次调用找个接口试下:
问题提前指出:再次进行请求前我们来看下在borrow-service中的借阅方法怎么写的,若是我们再次调用上次接口,由于我们已经借阅了该书,那么此时就会在下面x位置报出异常,问题就出现了,那么book-service这里做的-1操作就产生数据不一致问题!
来吧,测试一下:果然不出所料
来看下当前的数据库吧:
可以看到红色横线的部分就是未回滚的book-service服务,如何解决这类问题呢?我们可以集成阿里的Seata组件来进行尝试!
2.2、采用file模式来集成seata服务
2.2.1、启动seata-server
服务端下载地址:seata-server 1.4.2,由于是外网下载太慢,可使用下面链接下载
链接:https://pan.baidu.com/s/1AqmcHZY9Op2IucG7rHbjOQ
提取码:bb6f
下载解压后的目录如下:
进入到bin目录之后,我们来进行输入命令执行其中bat工具,直接来启动服务就好(默认是file模式):
指定在8868端口来进行执行,启动效果如下:
2.2.2、服务集成seata组件实现全局分布式事务
三个服务都进行集成seata依赖,主要配置步骤如下:
1、引入seata依赖:
<!-- 引入seata依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
2、配置文件来添加seata配置项,下面给出三个服务的不同配置项,下面需要特别注意的就是黄线的部分需要根据不同的服务名来进行配置:
seata-user-service:
seata:
service:
vgroup-mapping:
# 这里需要对事务组做映射,默认的分组名为 应用名称-seata-service-group,将其映射到default集群
# 这个很关键,一定要配置对,不然会找不到服务
user-service-seata-service-group: default
grouplist:
default: localhost:8868
seata-book-service:
seata:
service:
vgroup-mapping:
# 这里需要对事务组做映射,默认的分组名为 应用名称-seata-service-group,将其映射到default集群
# 这个很关键,一定要配置对,不然会找不到服务
book-service-seata-service-group: default
grouplist:
default: localhost:8868
seata-borrow-service:
seata:
service:
vgroup-mapping:
# 这里需要对事务组做映射,默认的分组名为 应用名称-seata-service-group,将其映射到default集群
# 这个很关键,一定要配置对,不然会找不到服务
borrow-service-seata-service-group: default
grouplist:
default: localhost:8868
3、三个服务的启动器都去开启seata事务注解:
@EnableAutoDataSourceProxy //开启seata事务配置
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;
5、最终在我们要进行分布式事务的service方法中添加全局事务注解!也就是borrow-service服务中的borrow()方法:
2.2.3、测试
提前准备
首先将seata-server
服务器启动。
接着启动我们的三个服务:在启动时向seata-server
去进行注册
看下seata-server
服务的控制台:可以看到确实三个服务已经注册成功了
开始测试
首先来进行测试:http://localhost:8082/borrow/1/2
第一次借阅是没有问题的,看下数据库:
再次来进行借阅下:
ok,此时再看下数据库的各个表:原本在book表中产生问题的数据在这里就没有再出现了,可以看到中间出现异常能够成功回滚了
debug
我们在修改、删除操作上进行debug:
看下undo_log表:
扣减步骤完成后执行下一步:
再次看下undo_log表:
其中包含一个全局唯一xid:全局事务就是根据这一条记录来进行回滚管理的!
2.3、采用nacos模式服务
对于项目中引入依赖以及添加注解相关操作间2.2.2中的配置步骤,这里不再做演示。
2.3.1、配置完整步骤
1、在nacos中创建一个命名空间seata
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"
}
}
3、下载develop包,执行其中的script中脚本来对nacos的配置中心中seata命名空间配置项进行初始化:
下载地址:seata开发包
链接:https://pan.baidu.com/s/1iiQphUPbvgcIyXcIjUcjZA
提取码:cbr2
进入到其中nacos目录下执行脚本,在windows中我们使用git工具来使用:
./nacos-config-interactive.sh
最终效果如下:
4、手动在命名空间seata中添加三个服务的配置项:
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、在三个服务项目中的各个服务添加配置项(替换之前的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:
6、关于nacos-server的会话存储位置(默认是file)
此时注册和配置相关的会话都继承在Nacos中进行了
还可以配置一下事务会话信息的存储方式,默认是file类型,那么就会在运行目录下创建file_store
目录,可以看下启动seata-server后创建的文件效果:
6.1、其实我们可以将其搬到数据库中存储,只需要修改一下配置即可,在seata的命名空间中进行修改配置内容如下:
- 1、修改两个配置:
store.session.mode
、``store.mode的值为
db` - 2、接着我们对数据库信息进行一下配置:
- 数据库驱动(8.0的需要修改):
com.mysql.cj.jdbc.Driver
- 数据库URL:默认就是seata数据库就好。
- 数据库用户名密码:
store.db.user
、store.db.password
6.2、创建一个数据库【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(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服务如下:
执行seata启动的命令:
启动三个服务:
与此同时,可以看到在seata服务的控制台中你可以看到里面的服务注册信息:
测试
访问借阅地址:http://localhost:8082/borrow/1/2
再此访问,肯定在中途去判断是否该用户借阅了书阶段出现异常,进行回滚,我们只需要关注book表中的书籍借阅数量有没有-1的问题,其实就是看其有没有回滚:
看下数据库:
没有问题!
2.3.3、debug
在这里我们来进行打上断点:
看看seata数据库以及我们自己本身的数据库undo_log中的记录是否产生变化:
接着来了一个请求,我们看debug的目前阶段:
此时来看数据库的情况:
seata数据库:记录依次是红框从上往下
seata-demo中的undo_log表:
可以看到用户表中的xid是依赖于tc也就是seata-server来进行回滚的。
参考资料
[1]. SpringCould笔记(二)微服务进阶 Cloud Alibaba
我是长路,感谢你的耐心阅读。如有问题请指出,我会积极采纳!
欢迎关注我的公众号【长路Java】,分享Java学习文章及相关资料
Q群:851968786 我们可以一起探讨学习
注明:转载可,需要附带上文章链接