16. Seata 分布式事务

2023年 9月 28日 73.3k 0

Spring Cloud 微服务系列文章,点击上方合集↑

1. 简介

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。

事务是保障一系列操作要么都成功,要么都失败。就比如转账:A转账100元给B,先从A账户扣除100元,然后从B账户增加100元,假如从A账户里面已经成功扣除了100元,但是增加B账户的钱的过程中发生了异常,导致没有增加成功。这里就需要恢复A账户里面的钱(回滚)。整个转账过程就必须是事务操作。

官网地址:seata.io/zh-cn/

2. 下载运行

可以直接下载二进制包或通过源码编译打包。

2.1 直接下载(推荐)

从官网 github.com/seata/seata…下载服务器软件包,将其解压缩。

官网下载很慢,网盘下载(推荐):「seata-server-1.6.1.zip」来自UC网盘分享
drive.uc.cn/s/2cfffd43e…

2.2 编译安装

# 下载源码
git clone https://gitee.com/seata-io/seata.git

cd seata

# 切换分支
git checkout v1.6.1

# 编译打包
mvn -Prelease-seata -Dmaven.test.skip=true clean install -U

位置:distribution/target/seata-server-1.6.1

2.3 运行

# windows
seata-server.bat -p 8091 -h 127.0.0.1 -m file

# mac/linux
sh seata-server.sh -p 8091 -h 127.0.0.1 -m file
  • -p 8091 指定端口
  • -h 127.0.0.1 指定地址

3. SpringCloud 集成 Seata

3.1 业务说明

用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:

  • 商品服务:扣除商品数量
  • 订单服务:创建订单
  • 账户服务:扣除账户余额

3.2 架构图

业务调用订单服务(创建订单)和商品服务(减少库存),订单服务再调用账户服务(减少余额)。

这个过程必须是事务性的,要么都成功,要么都失败。

3.3 创建 undo_log 表

-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
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;

undo_log表中记录相关的操作日志,主要包含被修改数据的原始值和相应的逆向操作。

3.4 创建业务表

account 账户表

  • id
  • username 用户名
  • money 余额

product 商品表

  • id
  • product_name 商品名称
  • product_number 商品数量

product_order 订单表

  • id
  • user_id 用户id
  • product_id 商品id
  • purchase_number 购买数量
  • purchase_money 购买金额

sql脚本如下:

DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `username` varchar(50) DEFAULT NULL COMMENT '用户名',
  `money` int(11) DEFAULT 0 COMMENT '余额',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `product_name` varchar(50) DEFAULT NULL COMMENT '商品名称',
  `product_number` int(11) DEFAULT 0 COMMENT '商品数量',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `product_order`;
CREATE TABLE `product_order` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
  `product_id` bigint(20) DEFAULT NULL COMMENT '商品id',
  `purchase_number` int(11) DEFAULT 0 COMMENT '购买数量',
  `purchase_money` int(11) DEFAULT 0 COMMENT '购买金额',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `account` VALUES (1, '老王', 10000);

INSERT INTO `product` VALUES (1, '贵州茅台', 10);

  • 账户表插入一条用户信息
  • 商品表插入一条商品信息

实际情况下账户表、商品表、订单表可能在不同的数据库中,这里就模拟放在一个库里。

3.5 pom.xml

添加如下依赖包:

  • spring-boot-starter-web spring boot包依赖
  • spring-cloud-starter-alibaba-nacos-discoverynacos服务注册与发现包依赖
  • spring-cloud-starter-openfeignopenfeign服务调用依赖
  • spring-cloud-loadbalancer负载均衡包依赖
  • spring-cloud-starter-alibaba-seataseata分布式事务包依赖
  • mysql-connector-javamysql包依赖
  • spring-boot-starter-data-jpa jpa包依赖
  • lombok lombok包依赖

    
        org.springframework.boot
        spring-boot-starter-web
    

    
        com.alibaba.cloud
        spring-cloud-starter-alibaba-nacos-discovery
    

    
        org.springframework.cloud
        spring-cloud-starter-openfeign
    

    
        org.springframework.cloud
        spring-cloud-loadbalancer
    

    
        com.alibaba.cloud
        spring-cloud-starter-alibaba-seata
    

    
        mysql
        mysql-connector-java
    

    
        org.springframework.boot
        spring-boot-starter-data-jpa
    

    
        org.projectlombok
        lombok
        provided
    

3.6 application.properties

  • nacos服务注册与发现地址配置
  • 数据库连接信息配置
  • seata地址、服务组等配置
# nacos
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
spring.cloud.nacos.discovery.server-addr=http://localhost:8848
# 数据库连接信息
spring.datasource.url=jdbc:mysql://localhost:3306/seata_demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# seata 相关
seata.config.type=file
seata.service.grouplist.default=127.0.0.1:8091
seata.tx-service-group=test_group
seata.service.vgroup-mapping.test_group=default

启动类上要@EnableFeignClients()注解开启服务调用。

3.7 seata-product 服务

创建seata-product商品服务模块,提供decreaseNumber扣除库存数量方法。

@RestController
@RequestMapping("product")
public class ProductController {
    @Resource
    private ProductService productService;

    @GetMapping("decreaseNumber")
    public void decreaseNumber(@RequestParam Long id,
                              @RequestParam int number) {
        productService.decreaseNumber(id, number);
    }
}

public interface ProductService {

    /**
     * 减少库存数量
     */
    void decreaseNumber(Long id, int number);

}

@Service
public class ProductServiceImpl implements ProductService {

    @Resource
    private ProductRepository productRepository;

    @Override
    public void decreaseNumber(Long id, int number) {
        Product product = productRepository.getById(id);
        product.setProductNumber(product.getProductNumber() - number);
        productRepository.save(product);
    }
}

3.8 seata-account 服务

创建seata-account账户服务模块,提供decreaseMoney扣除账户余额方法。


@RestController
@RequestMapping("account")
public class AccountController {
    @Resource
    private AccountService accountService;

    @GetMapping("decreaseMoney")
    public void decreaseMoney(@RequestParam Long userId,
                              @RequestParam int money) {
        accountService.decreaseMoney(userId, money);
    }
}

public interface AccountService {

    /**
     * 减少用户余额
     */
    void decreaseMoney(Long userId, int money);

}

@Service
public class AccountServiceImpl implements AccountService {

    @Resource
    private AccountRepository accountRepository;


    @Override
    public void decreaseMoney(Long userId, int money) {
        Account account = accountRepository.getById(userId);
        account.setMoney(account.getMoney() - money);
        accountRepository.save(account);
    }
}

3.9 seata-order 服务

创建seata-order订单服务模块,提供createOrder创建订单方法,它远程调用了账户服务decreaseMoney扣除余额方法。

@RestController
@RequestMapping("order")
public class OrderController {
    @Resource
    private OrderService orderService;

    @GetMapping("createOrder")
    public void createOrder(@RequestParam Long userId,
                            @RequestParam Long productId,
                            @RequestParam int number,
                            @RequestParam int money) {
        orderService.createOrder(userId, productId, number, money);
    }
}

public interface OrderService {

    /**
     * 创建订单
     */
    void createOrder(Long userId, Long productId, int number, int money);

}


@Service
public class OrderServiceImpl implements OrderService {
    @Resource
    private OrderRepository orderRepository;

    @Resource
    private AccountService accountService;

    @Override
    public void createOrder(Long userId, Long productId, int number, int money) {
        Order order = new Order();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setPurchaseNumber(number);
        order.setPurchaseMoney(money);
        // 创建订单
        orderRepository.save(order);

        // 调用账户服务 扣除账户余额
        accountService.decreaseMoney(userId, money);
    }
}

@FeignClient(name = "seata-account")
public interface AccountService {

    @GetMapping("/account/decreaseMoney")
    void decreaseMoney(@RequestParam Long userId,
                       @RequestParam int money);
}

3.10 seata-business

创建seata-business业务服务模块,提供purchase购买商品方法,它远程调用了订单服务createOrder创建订单方法和商品服务decreaseNumber扣除库存数量方法。

purchase()方法加上@GlobalTransactional注解开启事务,当有异常发生时会进行回滚,这里通过int i = 1 / 0 ;模拟异常。

@RestController
@RequestMapping("business")
public class BusinessController {

    @Resource
    private BusinessService businessService;

    @GetMapping("purchase")
    public String purchase() {
        businessService.purchase();
        return "操作成功";
    }
}

public interface BusinessService {
    void purchase();
}

@Service
public class BusinessServiceImpl implements BusinessService {

    @Resource
    private ProductService productService;

    @Resource
    private OrderService orderService;

    @GlobalTransactional
    @Override
    public void purchase() {
        // 调用订单服务创建订单
        orderService.createOrder(1L, 1L, 1, 1499);
        // 模拟异常
        int i = 1 / 0 ;
        // 调用商品服务扣除库存
        productService.decreaseNumber(1L, 1);
    }
}

// 远程Order服务接口
@FeignClient(name = "seata-order")
public interface OrderService {

    @GetMapping("/order/createOrder")
    void createOrder(@RequestParam Long userId,
                     @RequestParam Long productId,
                     @RequestParam int number,
                     @RequestParam int money);
}

// 远程product服务接口
@FeignClient(name = "seata-product")
public interface ProductService {

    @GetMapping("/product/decreaseNumber")
     void decreaseNumber(@RequestParam Long id,
                               @RequestParam int number);
}

3.11 测试

访问接口地址:http://localhost:8304/business/purchase

BusinessServiceImpl#purchase方法不加@GlobalTransactional注解:订单表已经创建新的订单并且账户余额已扣除,但是商品库存数量没有减少。

BusinessServiceImpl#purchase方法加上@GlobalTransactional注解:订单表和账号表会回滚,也就是订单记录和账户余额都没有发生变化。

这里只在seata-business服务模拟发生异常,实际上不管在那个服务上发生异常(订单服务、商品服务、账户服务),数据都会回滚到之前的状态。

Seata将被修改数据的原始值和相应的逆向操作记录在undo_log表中,如果发生异常通过undo_log表中的内容进行回滚,我们可以通过调试模式打个断点,然后去查看数据库的undo_log表就可以查看到相关数据。

  • 在事务方法执行过程中会新增undo_log表数据记录,并在事务方法执行结束后清除记录,所以只能通过断点去查看。

4. 结语

本文通过用户购买商品的案例来使用Seata分布式事务:创建订单、扣除余额、减少库存,可以看出通过Seata使用分布式事务非常的简单方便,只需要一个@GlobalTransactional注解。

Spring Cloud 微服务系列 完整的代码在仓库的sourcecode/spring-cloud-demo目录下。

gitee(推荐):gitee.com/cunzaizhe/x…

github:github.com/tigerleeli/…

关注微信公众号:“小虎哥的技术博客”,让我们一起成为更优秀的程序员❤️!

相关文章

服务器端口转发,带你了解服务器端口转发
服务器开放端口,服务器开放端口的步骤
产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
如何使用 WinGet 下载 Microsoft Store 应用
百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

发布评论