分布式事务

为什么需要锁?

场景:
多任务环境下 — 多对一操作
有状态的资源 — 非原子性操作,存在中间态

为什么需要事务?

场景:
服务需要做多件事情
但是其中可能某件事情会失败
非原子动作
通过事务保证要么一起成功,要么一起失败

假设这样一个场景:
​ A 给 B 转账 100, 流程步骤如下:
1、 A 减 100
2、 B 多 100。
如果第一步骤执行后, 系统崩溃掉了。 会怎么样呢?
问题: A 被减掉了 100, 但 B 的钱未能加 100. 此时, A + B 的金钱总额凭空少了 100。 数据
不一致了。
解决思路呢? 我们希望步骤 1 和步骤 2 能够绑定在一起执行, 不可分;并且在步骤 1 和步骤 2
执行的过程中, 尽量规避中间状态。 即谓事务。

本地事务概念

将非原子操作约束得像原子操作
都是内部矛盾
由一组家族内操作构成的可靠、独立的工作单元
ACID(原子性、一致性、隔离性、持久性)

原子性就是不可拆分的特性, 要么全部成功然后提交(commit), 要么全部失败然后回滚
(rollback)。 若开启事务, 在上述场景就不会出现 A 少 100 成功, B 多 100 失败 这种情况。MySQL通过Redo Log重做日志实现了原子性, 在将执行SQL语句时, 会先写入redo log buffer,再执行 SQL 语句, 若 SQL 语句执行出错就会根据 redo log buffer 中的记录来执行回滚操作,由此拥有原子性

一致性指事务将数据库从一种状态转变为下一种一致的状态。 比如有一个字段 name 有
唯一索引约束, 那么在事务前后都不能有重复的 name 出现违反唯一索引约束, 否则回滚。在上述场景中即金钱总数总是 200, 不能凭空增加减少。 MySQL 通过 undo Log 实现一致性,执行 SQL 语句时, 会先写入 undo log 再写入 redo log buffer。 undo 是逻辑日志, 会根据之前的 SQL 语句进行相应回滚, 比如之前是 insert 那么回滚时会执行一个 delete, 一个 update会执行 一个相反的 update。 并且除了回滚, undo log 还有一个作用是 MVCC, 当用户读取一行记录时, 若该记录已经被其他事务占用, 当前事务可通过 undo 读取之前的行版本信息实现非锁定读取。 并且 undo log 也会产生 redo log, 因为 undo log 也需要持久性的保护。

首先介绍如果没有隔离性会发生的 4 种情况

  1. 丢失更新
    ​ A 事务撤销时, 把已经提交的 B 事务的更新数据覆盖了。 这种错误可能造成很严重的问
    题, 通过下面的账户取款转账就可以看出来, MySQL 通过三级封锁协议的第一级解决了丢失更新, 事务 T 要修改数据 A 时必须加 X 锁, 直到 T 结束才释放锁。
  2. 脏读
    ​ 脏读主要是读取到了其他事务的数据, 而其他事务随后发生回滚。 MySQL 通过三级封锁协议的第二级解决了脏读, 在一级的基础上, 要求读取数据 A 时必须加 S 锁, 读取完马上释放 S 锁。
  3. 不可重复读
    ​ 不可重复读是读取到数据后, 随后其他事务对数据发生了修改, 无法再次读取。 MySQL通过三级封锁协议的第三级解决了不可重复读。 在二级的基础上, 要求读取数据 A 时必须加 S 锁, 直到事务结束了才能释放 S 锁。
  4. 幻读
    ​ 幻读是读取到数据后, 随后其他事务对数据发生了新增, 无法再次读取。 在 InnoDB 引擎Repeatable Read 的隔离级别下, MySQL 通过 Next-Key Lock 以及 MVCC 解决了幻读, 事务中分为当前读以及快照读。

持久性
一旦事务提交, 则其所做的修改就会永久保存到数据库中。 此时即使系统崩溃, 修改的数据也不会丢失。 具体实现原理就是在事务 commit 之前会将, redo log buffer 中的数据持久化到硬盘中的 redo log file, 这样在 commit 的时候, 硬盘中已经有了我们修改或新增的数据, 由此做到持久化。

事务的原子性是通过 undo log 来实现的
事务的持久性是通过 redo log 来实现的
事务的隔离性是通过 (读写锁+MVCC)来实现的
而事务的终极大 boss 一致性是通过原子性, 持久性, 隔离性来实现的! ! !

Spring中事务操作的实现
package com.enjoy.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

@Api(value = "事务示例", description = "事务示例")
@RestController
public class TransController {
    @Resource
    private JdbcTemplate jdbcTemplate;// ---- 会不会有变化

    @Autowired
    private TransactionTemplate transactionTemplate;

    @Autowired
    private DataSourceTransactionManager transactionManager;

    private String sql_james = "INSERT INTO bank_a(money,user_name)VALUES (?,?)";
    private String sql_peter = "INSERT INTO bank_b(money,user_name)VALUES (?,?)";

    @ApiOperation(value="1.spring声明式事务")
    @RequestMapping(value = "/spring/transfer/declare", method = RequestMethod.GET)
    @Transactional
    public String declareTransfer(int money) {
        int resultJames = jdbcTemplate.update(sql_james,-money,"james");
        int resultPeter = jdbcTemplate.update(sql_peter,money,"peter");
        if (money > 20){
            throw new RuntimeException("money too large");
        }
        return resultJames+"-"+resultPeter;
    }

	@ApiOperation(value="2.spring编程式事务")
    @RequestMapping(value = "/spring/transfer/code", method = RequestMethod.GET)
    public String codeTransfer(final int money) {
        // 通过transactionTemplate进行事务的管理
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                int resultJames = jdbcTemplate.update(sql_james,-money,"james");
                int resultPeter = jdbcTemplate.update(sql_peter,money,"peter");
                if (money > 20){
                    throw new RuntimeException("money too large");
                }
            }
        });
        return "success";
    }

    @ApiOperation(value="3.java编程式事务")
    @RequestMapping(value = "/java/transfer/code", method = RequestMethod.GET)
    public String trans(final int money) throws InterruptedException {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {//建副本
            int resultJames = jdbcTemplate.update(sql_james,-money,"james");
            int resultPeter = jdbcTemplate.update(sql_peter,money,"peter");
            if (money > 20){
                throw new RuntimeException("money too large");
            }
        } catch (DataAccessException ex) {
            transactionManager.rollback(status); // 也可以執行status.setRollbackOnly();
            throw ex;
        }

        transactionManager.commit(status);
        return "success";
    }

}
分布式事务

隔壁老王遛狗不栓狗绳,我找他理论,他竟然不理我
事务:由一组跨家族的操作构成的可靠、独立的工作单元
难点: 高度并发、资源分布、时间跨度大

CAP理论

CAP 定理, 又被叫作布鲁尔定理。
CAP 指的是: 一致性(Consistency) 、 可用性(Availability) 、 分区容错性(Partition tolerance) 。
CAP 定律说的是, 在一个分布式系统中, 最多只能满足 C、 A、 P 中的两个, 不可能三个同时满足。

在分布式系统中, 网络无法 100% 可靠, 分区其实是一个必然现象。
如果我们选择了 CA 而放弃了 P, 那么当发生分区现象时, 为了保证一致性, 这个时候必须拒绝请求, 但是 A 又不允许, 所以分布式系统理论上不可能选择 CA 架构, 只能选择 CP 或者 AP 架构。(P必须被保证)
而且, 显然, 任何横向扩展策略都要依赖于数据分区。 因此, 设计人员必须在一致性与可用性之间做出选择。

标准分布式事务

全局事务:由事务管理器全局管理
事务管理器:管理全局事务状态和参与的资源,协同资源的一致提交和回滚
TX协议:应用/应用服务器与事务管理器的接口
XA协议:全局事务管理器与资源管理器的接口
在这里插入图片描述

XA是由X/Open组织提出的分布式事务的规范。
XA规范主要定义了(全局)事务管理器(TM) 和(局部)资源管理器(RM)之间的接口。主流的关系型数据库产品都是实现了XA接口的。

XA接口是双向的系统接口,在事务管理器(TM)以及一个或多个资源管理器(RM)之间形成通信桥梁。

XA之所以需要引入事务管理器是因为,在分布式系统中,从理论上讲两台机器理论上无法达成一致的状态,需要引入一个单点进行协调。

由全局事务管理器管理和协调的事务,可以跨越多个资源(如数据库或JMS队列)和进程。全局事务管理器一般使用XA二阶段提交协议与数据库进行交互。

在分布式系统中,每一个机器节点虽然都能够明确知道自己在进行事务操作过程中的结果是成功或失败,但是却无法直接获取到其他分布式节点的操作结果,因此,当一个事务操作需要跨越多个分布式节点的时候,为了保持事务处理的ACID特性,就需要引入一个称为"协调者(coordinator)“的组件来统一调度所有分布式节点的执行逻辑,这些被调度的分布式节点则被称为"参与者(participant)”.协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务真正进行提交,基于这个思想,衍生出了二阶段提交和三阶段提交两种协议。

优点:尽量保证了数据的强一致性,实现成本较低,在各大主流数据库都有自己实现,对于MYSQL从5.5开始支持

2PC

计算机网络尤其是在数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务处理过程中能够保持原子性和一致性而设计的一种算法,通常,二阶段提交协议也被称为一种一致性协议,用来保证分布式系统数据的一致性。
在这里插入图片描述

阶段一:提交事务请求 – 投票阶段(Prepare阶段)

  • 事务询问:协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
  • 执行事务:各参与者节点执行事务操作,并将Undo和Redo信息记入事务日志中
  • 各参与者向协调者范阔事务询问的响应:如果参与者执行了事务操作,那么就反馈给协调者Yes响应,表示事务可以执行,如果参与者没有成功执行事务,那么就反馈给协调者No响应,表示事务不可以执行。

以上阶段也可以认为是 “投票阶段”,以下称为"执行阶段"

阶段二 :执行事务阶段(Commit或Roll Back)

  • 如果所有参与者反馈的都是YES 执行事务提交:
  • 发送提交请求
  • 事务提交
  • 反馈事务提交结果
  • 完成事务
  • 假如有一个参与者返回NO 中断事务请求
  • 发送回滚请求
  • 事务回滚
  • 反馈事务回滚结果
  • 中断事务

二阶段提交将一个事务的处理阶段分为了投票和执行两个阶段,其核心是对每个事务都采用先尝试后提交的处理方式,因此通常看做一个强一致性的算法

2PC基于XA协议的实现
需要依赖:

    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jta-atomikos</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.15</version>
        </dependency>

配置文件

server:
  port: 8090
spring:
  datasource:
    druid:
      jamesDB:
        url: jdbc:mysql://192.168.0.128:3307/enjoy?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
        username: root
        password: root
      peterDB:
        url: jdbc:mysql://192.168.0.128:3307/enjoy2?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
        username: root
        password: root
  #jta相关参数配置
  jta:
    log-dir: classpath:tx-logs
    transaction-manager-id: txManager

进行xa数据源的配置

package com.enjoy.config;

import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.jta.JtaTransactionManager;

import javax.sql.DataSource;
import javax.transaction.UserTransaction;
import java.util.Properties;

@Configuration
public class XADataSourceConfig {
	@Autowired
	private Environment env;

	@Bean(name = "jamesDataSource")
	@Primary
	public DataSource jamesDataSource(Environment env) {
		AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
		ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
		ds.setUniqueResourceName("jamesDB");
		ds.setPoolSize(5);
		ds.setXaProperties(build("spring.datasource.druid.jamesDB."));
		return ds;
	}

	@Bean(name = "peterDataSource")
	public DataSource peterDataSource(Environment env) {
		AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
		Properties prop = build("spring.datasource.druid.peterDB.");
		ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
		ds.setUniqueResourceName("peterDB");
		ds.setPoolSize(5);
		ds.setXaProperties(prop);

		return ds;
	}

	@Bean("jamesJdbcTemplate")
	public JdbcTemplate jamesJdbcTemplate(@Qualifier("jamesDataSource") DataSource ds) {
		return new JdbcTemplate(ds);
	}

	@Bean("peterJdbcTemplate")
	public JdbcTemplate peterJdbcTemplate(@Qualifier("peterDataSource") DataSource ds) {
		return new JdbcTemplate(ds);
	}

	@Bean
	public JtaTransactionManager regTransactionManager () {
		UserTransactionManager userTransactionManager = new UserTransactionManager();
		UserTransaction userTransaction = new UserTransactionImp();
		return new JtaTransactionManager(userTransaction, userTransactionManager);
	}

	private Properties build(String prefix) {
		Properties prop = new Properties();
		prop.put("url", env.getProperty(prefix + "url"));
		prop.put("username", env.getProperty(prefix + "username"));
		prop.put("password", env.getProperty(prefix + "password"));
		return prop;
	}

}

事务接口和实现

package com.enjoy.service;

public interface TransferService {
    String transfer(int money) ;
}
package com.enjoy.service.impl;

import com.enjoy.service.TransferService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class XATransferServiceServiceImpl implements TransferService{
    @Autowired
    @Qualifier("jamesJdbcTemplate")
    private JdbcTemplate jamesJdbcTemplate;

    @Autowired
    @Qualifier("peterJdbcTemplate")
    private JdbcTemplate peterJdbcTemplate;

    @Transactional
    public String transfer(int money) {
        int resultJames = jamesJdbcTemplate.update("INSERT INTO bank_a(money,user_name)VALUES (?,?)",-money,"james");
        int resultPeter = peterJdbcTemplate.update("INSERT INTO bank_b(money,user_name)VALUES (?,?)",money,"peter");
        if (money > 20){
            throw new RuntimeException("money too large");//系统宕机了怎么办?
        }
        return resultJames+"-"+resultPeter;
    }
}

控制层调用

package com.enjoy.controller;

import com.enjoy.service.TransferService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AccountController {
    @Autowired
    private TransferService transferService;

    @GetMapping("/transfer")
    public String transfer(int money){
        return transferService.transfer(money);
    }
}

优缺点
优点:原理简单、实现方便
缺点:同步阻塞、单点问题、脑裂、太过保守

同步阻塞:各个参与者都要等待最慢的那个参与者完成响应,才能进入执行阶段
单点问题:一旦协调者出现问题,所有参与者都无法运转
数据不一致:在提交阶段,由于脑裂,导致有的参与者无法真正进行提交或回滚导致数据不一致
太过保守:二阶段提交协议没有涉及较为完善的容错机制,任何一个节点的失败都会导致整个事务的失败

3PC

三阶段提交,是2PC的改进版,其将二阶段提交协议的"提交事务请求"过程一份为二,形成了CanCommit、PreCommit和doCommit三个阶段组成的事务处理协议。

在这里插入图片描述

阶段一:CanCommit

  • 事务询问 : 协调者发送包含事务内容的canCommit请求,询问是否可以执行事务提交操作
  • 参与反馈:参与者认为可以顺利执行事务,则反馈Yes,并进入预备状态,否则范阔No响应

阶段二:PreCommit

  • 执行事务预提交

  • 发送预提交请求 : 协调者发出preCommit请求,并进入Prepared阶段

  • 事务预提交:接收请求,执行事务操作,并将Undo和Redo信息记录到事务日志中

  • 反馈事务进行的响应:参与者根据事务操作成功或失败进行Ack响应,并等待commit或终止

  • 中断事务
    如果任何一个参与者反馈了No响应,或者在等待超时之后,协调者仍然无法接收反馈响应,就会中断事务

  • 发送中断请求

  • 中断事务

阶段三:doCommit
该阶段进行真正的事务提交

  • 执行事务

  • 发送提交请求

  • 事务提交

  • 反馈事务提交结果

  • 完成事务

  • 中断事务

  • 发送中断请求

  • 事务回滚

  • 反馈事务回滚结果

  • 中断事务

需要注意的是:一旦进入阶段三,可能会存在如下两种故障:

  1. 协调者出现问题
  2. 协调者和参与者之间的网络出现故障
    无论哪种情况,最终都会导致参与者无法及时接收来自协调者的doCommit或是abort请求,针对这种异常情况,参与者都会在等待超时之后,继续进行事务提交

优缺点:
三阶段提交协议的优点,相较于二阶段提交协议,最大的优点是降低了参与者的阻塞范围(第一个阶段是不阻塞的),并且能够在出现单点故障后继续达成一致(3PC会根据协调者的状态进行进行回滚或者提交)
缺点: 取出阻塞的同时也引入了新的问题,那就是在参与者接收到preCommit消息后,如果网络出现分区,协调者所在的节点和参与者无法进行正常的网络通信,这种情况下,该参与者依然会进行事务的提交,必然出现数据的不一致性。

BASE理论

扯理论好没用,好使才是王道
什么原子不原子的追求,我不是追求完美的人
BA:Basically Avaliablity 基本业务可用性
S : Soft state 柔性状态(中间态不一致)
E : Eventual Consistency 最终一致性
为了可用性,牺牲一致性
难点:业界尚未有特别成熟、大规模普及的无侵入性的框架方案

TCC事务补偿型

TCC 其实就是采用的补偿机制, 其核心思想是: 针对每个操作, 都要注册一个与其业务逻辑对应的确认和补偿(撤销) 操作。
其将整个业务逻辑的每个分支显式的分成了 Try、 Confirm、 Cancel 三个操作。 Try 部分完成业务的准备工作, confirm 部分完成业务的提交, cancel部分完成事务的回滚。

优缺点
优点:跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性要比2PC要差一些
缺点:属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,而且补偿的时候也可能失败,在一些场景中,一些业务流程可能用TCC不太好定义和处理。

异步-确保型

在这里插入图片描述

消息生产方,
额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交。
然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。

消息消费方,
需要处理这个消息,并完成自己的业务逻辑。
此时如果本地事务处理成功,则标明已经处理成功了,如果失败,那么就会重试执行。
如果是业务上面的失败,则给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。

最大努力通知型

特点:
业务处理后,向被动方发送通知消息(允许消息丢失)
主动发可以设置时间梯度通知规则,在通知失败后按照规则重复通知,直到通知N次后不再通知

主动方提供查询接口供校对查询,如:
收款通知、注册通知等等(支付宝/微信/12306,付款后页面自动跳转)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lang20150928

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值