分布式事务详解

1、概念和历程

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

jiagou

jiagou

2、几种模式

2.1、术语

TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

2.2、AT 模式

前提:

  • 基于支持本地 ACID 事务的关系型数据库。
  • Java 应用,通过 JDBC 访问数据库。

整体机制:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:

    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

写隔离:

  • 一阶段本地事务提交前,需要确保先拿到 全局锁 。
  • 拿不到 全局锁 ,不能提交本地事务。
  • 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

读隔离:

  • 在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
  • 如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

工作机制:

  • 一阶段--执行
  • 二阶段--回滚
  • 三阶段--提交
2.3、TCC 模式

回顾总览中的描述:一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:

  • 一阶段 prepare 行为
  • 二阶段 commit 或 rollback 行为

根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 ​Manual (Branch) Transaction Mode​。

AT 模式基于 支持本地 ACID 事务 的 ​关系型数据库​:

  • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
  • 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
  • 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。

相应的,TCC 模式,不依赖于底层数据资源的事务支持:

  • 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
  • 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
  • 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。

所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。

2.4、Saga 模式

Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

适用场景:

  • 业务流程长、业务流程多
  • 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口

优势:

  • 一阶段提交本地事务,无锁,高性能
  • 事件驱动架构,参与者可异步执行,高吞吐
  • 补偿服务易于实现

缺点:

3、集成 IDEA

3.1、部分效果

数据库设计,4个微服务,3个数据库

mysql

mysql

下单前,没有订单信息,商品数量为100,金额为1000

before

before

下单成功,有订单信息,商品数量-1,金额-100

success

success

下单异常,uodo_log表记录异常信息

refund

refund

异常全部回滚

3.2、配置文件

seata配置文件,把file.conf和registry.conf粘贴到resource目录下。由于项目配置使用的是nacos管理:
bootstrap.yml文件:

spring:
    application:
        name: seata-bussiness-service      # 注册到nacos上的服务名
    profiles:
        active: dev
    cloud:
        nacos:
            # 注册中心
            discovery:
                username: nacos
                password: nacos
                server-addr: localhost:8848     #Nacos地址
                namespace: 6d2e2958-3e7b-448b-8107-87ab6b022ba9
            # 配置中心
            config:
                username: nacos
                password: nacos
                server-addr: localhost:8848     #Nacos地址
                file-extension: yml         #指定文件后缀,默认properties
                namespace: 6d2e2958-3e7b-448b-8107-87ab6b022ba9
        # 治理中心
        sentinel:
            transport:
                dashboard: http://localhost:9555    #配置sentinel dashboard地址
                port: 8719
        alibaba:
            seata:
                tx-service-group: my_test_tx_group
#feign:
#    sentinel:
#        enabled: true   #打开sentinel对feign的支持

Yml

seata事务核心依赖:

<!--seata事务依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <version>2.2.0.RELEASE</version>
</dependency>
<!--druid数据源-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
</dependency>

XML

3.3、测试 demo

为了更加准确模拟异常和分布式环境,所以使用4个微服务,同时操作不同数据库,产生异常后全部回滚。
这4个服务,配置文件和pom依赖基本都一样,只是注册到nacos上的服务名不同罢了,举例一下的bussiness业务服务的结构图:

bussiness

bussiness

3.3.1 seata-account 账户服务

AccountController.java 控制层:

package com.llh.controller;

import com.llh.domain.Account;
import com.llh.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * User: sunjunfu
 * DateTime: 2023/12/15 15:03
 */
@RestController
@RequestMapping("/account")
public class AccountController {
    @Autowired
    AccountService accountService;

    @RequestMapping("/deduct")
    public String deduct(){
        // 扣钱
        Account account = accountService.getById(1);
        if(account!=null  && account.getMoney()>=100){
            account.setMoney(account.getMoney()-100);
            accountService.updateById(account);
        }
        return "扣除成功";
    }
}

Java

3.3.2 seata-order 订单服务

OrderController.java 控制层:

package com.llh.controller;

import com.llh.api.AccountFeign;
import com.llh.domain.Order;
import com.llh.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * User: sunjunfu
 * DateTime: 2023/12/15 14:54
 */
@RestController
@RequestMapping("/order")
public class OrderController {
    @Autowired
    OrderService orderService;

    @Autowired
    AccountFeign accountFeign;

    @RequestMapping("/porder")
    public String porder(){
        Order order = new Order();
        order.setCount(1);
        order.setCommodityCode("C1000");
        order.setUserId("U1000");
        order.setMoney(100);

        // 1、下单
        orderService.save(order);
        //2、扣钱(RPC)
        accountFeign.deduct();
        return "下单成功";
    }
}

Java

AccountFeign.java 接口:

package com.llh.api;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * User: sunjunfu
 * DateTime: 2023/12/15 15:27
 */
@FeignClient(value = "seata-account-service",path = "account")
public interface AccountFeign {
    @RequestMapping("/deduct")
    public String deduct();
}

Java

3.3.3 seata-storage 储存服务

StorageController.java 控制层:

package com.llh.controller;

import com.llh.domain.Storage;
import com.llh.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * User: sunjunfu
 * DateTime: 2023/12/15 14:46
 */
@RestController
@RequestMapping("/storage")
public class StorageController {
    @Autowired
    StorageService storageService;

    @RequestMapping("/reduce")
//    @GlobalTransactional(rollbackFor = Exception.class)//全局事务,有这个注解:TM
    public String reduce(){
        // 减库存
        Storage storage = storageService.getById(1);
        if(storage!=null && storage.getCount()>0){
            storage.setCount(storage.getCount()-1);
            storageService.updateById(storage);
        }
        return "扣减库存成功";
    }
}

Java

3.3.4 seata-bussiness 业务服务

BussinessController.java 控制层:

package com.llh.controller;

import com.llh.api.OrderFeign;
import com.llh.api.StorageFeign;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * User: sunjunfu
 * DateTime: 2023/12/15 15:52
 */
@RestController
@RequestMapping("/bussiness")
public class BussinessController {
    @Autowired
    OrderFeign orderFeign;

    @Autowired
    StorageFeign storageFeign;

    @RequestMapping("/pay")
    @GlobalTransactional(rollbackFor = Exception.class)//全局事务,有这个注解:TM
    public String pay(){
        storageFeign.reduce();
        orderFeign.porder();
        int i = 1/0;
        return "支付成功";
    }
}

Java

OrderFeign.java 接口:

package com.llh.api;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * User: sunjunfu
 * DateTime: 2023/12/15 15:53
 */
@FeignClient(value = "seata-order-service",path = "order")
public interface OrderFeign {
    @RequestMapping("/porder")
    public String porder();
}

Java

StorageFeign.java 接口:

package com.llh.api;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * User: sunjunfu
 * DateTime: 2023/12/15 15:53
 */
@FeignClient(value = "seata-storage-service",path = "storage")
public interface StorageFeign {
    @RequestMapping("/reduce")
    public String reduce();
}

Java

3.3.5 uodo_log建表语句:
/*
 Navicat Premium Data Transfer

 Source Server         : MySQL
 Source Server Type    : MySQL
 Source Server Version : 80031 (8.0.31)
 Source Host           : localhost:3306
 Source Schema         : seata-storage

 Target Server Type    : MySQL
 Target Server Version : 80031 (8.0.31)
 File Encoding         : 65001

 Date: 16/12/2023 11:35:55
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `branch_id` bigint NOT NULL,
  `xid` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
  `context` varchar(128) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `ux_undo_log`(`xid` ASC, `branch_id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of undo_log
-- ----------------------------

SET FOREIGN_KEY_CHECKS = 1;
  • 54
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值