本地事务VS分布式事务—Seata分布式事务解决方案!

背景

本地事务在分布式系统中,只能控制住自己回滚,控制不了其他服务的回滚,在分布式系统中,事务如何保证,主要是有以下两个场景:

1.远程服务假失败问题,即远程服务业务其实成功了,但是由于网络故障,服务器环境等其他问题导致,远程调用异常(导致调用方业务回滚了,远程服务没有回滚)。

2.远程服务执行完成,下面的其他方法出现异常,导致已执行的的远程请求不能回滚。(无法控制远程请求的回滚)

本地事务

一、事务的基本性质

事务的几个特性:原子性,一致性、隔离性、持久性,简称ACID

  • 原子性:事务的一系列操作不可拆分,要么全部完成,要么全部失败。

  • 一致性:事务在执行前后,业务整体一致。比如转账,

    A:100,B:100 转:A->B:50 结果:A:50,B:150 (结果是一致的,不可能存在A扣钱了,B钱没加上的情况)

  • 隔离性:事务之前相互隔离(ABCD四个事务相互隔离执行,不会存在互相影响的情况)

  • 持久性:一但事务成功,数据一定会落在数据库。

二、事务的隔离级别

  • 读未提交(Read uncommitted)

该隔离级别的事务,会读到其他未提交事务的数据,此现象叫脏读。

  • 读已提交(Read committed)

可以读到另一个事务已提交的数据,但是事务里面多次读取可能会出现结果不一致的情况(B事务在两次读取的中间提交了事务,修改了数据,造成前后数据不一样),此现象叫不可重复读。

  • 可重复读(Repeatable read)

Mysql默认隔离级别,同一个事务里,select数据库读到的数据是事务开始状态时的数据,因此在这个事务里同样的select操作结果永远一致的,解决了不可重复读问题,但是会有幻读问题。

  • 串行化(Serializable )

在该隔离级别下,所有事务都是串行执行的(类似于加锁)。避免了脏读、幻读、不可重复读,但是效率极低。

三、事务传播行为

指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何运行。共7类传播行为,这里只讲最常用的:REQUIRED、REQUIRES_NEW。

REQUIRED:如果当前没有事务就创建一个事务,如果当前有事务,就加入该事务。(能上车就上车,没车上就自己来)

REQUIRES_NEW:创建一个新事务,不管当前有没有事务。(我不要你的,我要我自己的)

@Transactional  //a事务
public void a() {
    b(); // a事务(加入)
    c(); // c事务(新事务,不回滚)
    int i = 10/0; // 异常了c方法不回滚
}
@Transactional(propagation = Propagation.REQUIRED) // 有事务就加入,没有就创建
public void b() {
}
@Transactional(propagation = Propagation.REQUIRES_NEW) // 创建新事务
public void c() {
}

四、事务失效问题

事务是用代理对象来控制的,同一类下调用事务方法,不会走到代理对象,就会导致事务失效。比如上面的a方法调b、c事务方法(同一类下)b、c方法事务就会失效。解决方法的核心就是想办法使用代理对象来调用:

1.使用AopContext获取当前代理类,使用代理对象来调用事务方法。需要在启动类加上@EnableAspectJAutoProxy(exposeProxy = true)

OrderServiceImpl orderServiceImpl = (OrderServiceImpl)AopContext.currentProxy();
orderServiceImpl.b();

2.通过spring上下文获取到当前代理类

/**
 * 通过class获取Bean
 * @return bean
 */
public static <T> T getBean(Class<T> clazz) {
    return applicationContext.getBean(clazz);
}

分布式事务

一、CAP理论 & BASE理论

在分布式系统中,一致性、可用性、分区容错性3个元素最多只能同时满足两个,一般来说分区容错性不可避免,因此可认为P总是成立,所以分布式事务都是使用的CP和AP方案。对于大多数场景下,主机众多,集群规模越来越大,所以节点故障,网络故障是常态,基本上是要保证A舍弃C的,即使用AP方案

那我们一致性就不保证了吗?也并不是如此,AP方案下无法像本地事务一样保证强一致性,但是我们可以想办法做到弱一致性,即最终一致性,这就是BASE理论。其内容如下:

  • 基本可用(Basically Available):指分布式系统出现故障的时候,允许损失部分可用性(如响应时间、错误引导页面等等),来保证核心业务可用。

  • 软状态(Soft State):指系统允许在一个中间状态,并不是要么成要么败需要有一个同步中的状态。

  • 最终一致性(Eventual Consistency):指系统中所有数据副本在经过一段时间后,最终能达到一致的状态。

二、分布式事务的几种方案

1. 2PC方案(也叫XA协议)

由数据库支持的一个叫事务管理器的东西,该协议分为两个阶段:

第一阶段:事务管理器要求每个涉及到的事务,做预提交操作,反映是否可以提交事务。

第二阶段:每个事务分别提交数据,其中,如果有任何一个事务否决提交,那么所有事务都会回滚这一次的操作。

2. 柔性事务—TCC事务(Try - Confirm - Cancel)

Try是指预留操作、Confirm是指确认操作、Cancel 是指回滚操作。因此,使用时我们每一个业务场景,都要设计实现这三步操作(要自己写业务回滚逻辑),缺点就是对业务的侵入比较大,紧耦合了。

3. 使用消息队列—事务消息

首先本地向消息中心发送事务消息(这个消息对于消费者来说暂时不可见),发送成功后执行本地事务。

再根据本地事务的结果向消息中心发送Commit或RollBck命令,如果是commit订阅方此时能接收到消息,并消费,如果是RollBck订阅方收不到这条消息,等于没执行过。

  • 最大努力通知方案:不保证数据一定能够通知成功,但提供可查询操作接口进行核对。

  • 可靠消息通知方案:可靠消息+最终一致性方案。

三、使用Seata控制分布式事务

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

img

TC - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

Seata 提供了 AT、TCC、SAGA 和 XA 事务模式,这里以AT模式为例,演示一下如何使用:

1. 每一个微服务必须先创建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;
2. 安装事务协调器(Seata服务器),下载后将其解压。https://github.com/seata/seata/releases
3. SpringBoot整合seata:

1)导入依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

2)解压并启动seata-server,找到目录下的seata\bin\seata-server.bat,启动。

需要注意修改两个配置文件,registry.conf:注册中心配置(我这里以nacos为注册中心),修改注册中心地址:

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa 
  # type修改为nacos
  type = "nacos" 

  nacos {
      serverAddr = "localhost:8848"
      namespace = ""
      cluster = "default"
  }
  ...
config {
  # file、nacos 、apollo、zk、consul、etcd3
  # 指定配置类型,默认为文件,可以修改
  type = "file"
  ...

file.conf文件配置,可选择按文件存储或db存储,db存储需要配置上数据库地址,我这里就选择以文件:

## transaction log store, only used in seata-server
store {
    ## store mode: file、db
    mode = "file"

    ## file store property
    file {
    ## store location dir
    dir = "sessionStore"
    }
    ## database store property
    db {
        ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
        datasource = "dbcp"
        ## mysql/oracle/h2/oceanbase etc.
        db-type = "mysql"
        driver-class-name = "com.mysql.jdbc.Driver"
        url = "jdbc:mysql://127.0.0.1:3306/seata"
        user = "mysql"
        password = "mysql"
    }
}

3)所有想要用到的分布式事务的微服务使用seata的DataSourceProxy代理自己的数据源。每个服务添加如下配置类:

@Configuration
public class MySeataConfig {
    @Autowired
    DataSourceProperties dataSourceProperties;
    /**
     * 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚
     * @param dataSourceProperties
     * @return
     */
    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties) {
        HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(dataSourceProperties.getName())) {
            dataSource.setPoolName(dataSourceProperties.getName());
        }
        return new DataSourceProxy(dataSource);
    }
}

4)同时每一个微服务都必须配置上:registry.conf、file.conf,将这两个文件放在resources目录下,vgroup_mapping配置必须和应用名spring.application.name一致:

vgroup_mapping.{application.name}-fescar-server-group = "default"

5)至此,我们所有seata的配置已经全部完成了,接下来进行使用:

只需给我们的全局事务入口加上@GlobalTransactional(rollbackFor = Exception.class)

@GlobalTransactional(rollbackFor = Exception.class)
@Transactional(rollbackFor = Exception.class)
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
    // 全局事务里面调用远程方法
    wmsFeignService.orderLockStock(lockVo);
    // 模拟代码异常
    int i = 10/0;
}

接下来远程事务,我们不需要加@GlobalTransactional了,只需加上普通的事务注解@Transactional

@Transactional(rollbackFor = Exception.class)
public boolean orderLockStock(WareSkuLockVo vo) {
    ...
}

执行结果:在全局事务里面,调用完远程方法以后,程序抛了异常,结果远程事务也成功回滚了。

总结

Seata相当于是对2PC方案的一个升级,使用简单、上手快,SpringBoot也做了很好的支持。相较于普通用数据库实现二阶提交,性能上有一定优化。相较于传统TCC方案,对业务的侵入比较少,不用手动去写回滚代码。

但是!也是所有2PC方案的通病,无法支持高并发,一但流量上去了,整个系统的性能会大打折扣!

Seata只适合支持类似保存商品、提交资料等理论上不会出现大量请求,但是业务比较庞大复杂的场景。像那种下订单、抢票、秒杀等高并发场景还是要使用上我们的消息事务。在另外的文章里,我将会做详细的说明。

感谢阅读,再见!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值