• 16. Seata 分布式事务


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

    1. 简介

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

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

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

    2. 下载运行

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

    2.1 直接下载(推荐)

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

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

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    位置: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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • -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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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);
    
    
    • 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
    • 账户表插入一条用户信息
    • 商品表插入一条商品信息

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

    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包依赖
    <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.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>
    
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-loadbalancerartifactId>
        dependency>
    
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-seataartifactId>
        dependency>
    
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>
    
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-jpaartifactId>
        dependency>
    
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <scope>providedscope>
        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
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    启动类上要@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);
        }
    }
    
    • 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

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

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

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

    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(推荐):https://gitee.com/cunzaizhe/xiaohuge-blog

    github:https://github.com/tigerleeli/xiaohuge-blog

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

  • 相关阅读:
    解决readme.md文件中粘贴的图片放到GitHub上无法显示问题
    Kotlin设计模式:深入理解桥接模式
    C/C++条件编译:#ifdef、#else、#endif等
    WEB网络渗透的基础知识
    web开发模式——一般两种
    matlab 计算数组中所有值的均值
    程序员脱发怎么办
    【阅读论文】-- IDmvis:面向1型糖尿病治疗决策支持的时序事件序列可视化
    Springboot信息泄露以及heapdump的利用
    SSM+基于Vue框架的在线投票系统的设计与实现 毕业设计-附源码221604
  • 原文地址:https://blog.csdn.net/qq_28883885/article/details/133376089