掘金 后端 ( ) • 2024-05-02 17:57

刘诗诗-1.jpeg

一、基础概念

1.1 事务简介

事务可以看做是一次大的活动,它由不同的小活动组成,这些活动要么全部执行成功,要么全部执行失败。事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。简单地说,事务提供一种“ 要么什么都不做,要么做全套(All or Nothing)”机制。

20240502164317.png

1.2 事务的特性

事务是基于数据进行操作,需要保证事务的数据通常存储在数据库中,所以介绍到事务,就不得不介绍数据库事务的特性。事务的特性(通常被称为ACID属性)以及隔离性是数据库管理系统(DBMS)中非常重要的概念,它们确保了数据的一致性和完整性。其中,事务的特性如下表所示:

特性 说明 原子性(Atomicity) 事务是一个原子操作单元,其对数据的所有操作要么全都执行,要么全都不执行。
这意味着,如果事务中的某个操作失败,那么整个事务都会回滚到初始状态,就像没有执行过一样。 一致性(Consistency) 事务必须使数据库数据从一个一致性状态变换到另一个一致性状态。
这意味着,在事务开始之前和结束之后,数据库的完整性约束必须被满足。 隔离性(Isolation) 多个事务并发执行时,一个事务的执行不应影响其他事务。
也就是说,每个事务都应该在自己的独立空间中执行,并且不受其他并发事务的干扰。 持久性(Durability) 事务完成之后,该事务对数据的更改会持久到数据库,且不会被回滚。
一旦事务提交,则其结果就是永久性的,即使系统崩溃也不会丢失。

而事务的隔离性是确保并发事务不会相互干扰的关键属性。在数据库系统中,多个事务可能同时运行,如果不进行适当的隔离,它们可能会互相影响,导致数据不一致或其他问题。隔离性通常通过锁(locking)和多版本并发控制(MVCC)等技术实现,锁可以确保在事务访问数据期间,其他事务不能修改这些数据。而MVCC则允许每个事务看到数据的一个一致的快照,即使其他事务正在修改数据。

为了解决数据库中事务并发所产生的问题,在标准SQL规范中,定义了事务隔离级别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。隔离级别决定了事务在多大程度上被隔离,不同的数据库管理系统提供了不同的隔离级别,包括:

隔离级别 说明 读未提交(ReadUncommitted) 是最低的隔离级别,一个事务可以读取另一个未提交事务的修改,这可能导致脏读、不可重复读和幻读。 读已提交(ReadCommitted) 这是大多数数据库系统的默认隔离级别。
它只能读取已经提交的数据。这可以防止脏读,但可能出现不可重复读和幻读。 可重复读(RepeatableRead) 在同一事务内,多次读取同一数据返回的结果是一样的。这可以防止脏读和不可重复读,但可能出现幻读。 串行化(Serializable) 这是最高的隔离级别。
它通过对事务进行排序,使之不可能相互冲突,从而解决脏读、不可重复读和幻读问题,但这也可能导致性能下降。

低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。选择合适的隔离级别需要权衡并发性能和数据一致性的需求,不同的应用场景可能需要不同的隔离级别。

1.3 本地事务

在计算机系统中,更多的是通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,因此叫数据库事务。由于应用主要靠关系数据库来控制事务,而数据库通常和应用在同一个服务器,所以基于关系型数据库的事务又被称为本地事务。尤其对于数据库而言,为了数据安全,提供了以下的几个步骤来完成本地事务的提交以及回滚:

transaction begin
-- insert/delete/update
-- insert/delete/update
-- ...

-- 成功,则提交
transaction commit
-- 失败,则回滚
transaction rollback

传统的单体应用通常会将数据全部存储在一个数据库中,会借助关系型数据库来完成事务控制。尤其某些业务之间存在强依赖性、强耦合性,数据需要保持特别严苛的强一致性。比如购买功能:我们购买对应的商品,一部分需要扣除我们的余额,余额扣减后商家就安排相应的发货。如果不使用事务,比如第一步扣减余额出现问题,后面商家就直接发货了。再或者余额扣了,商家发货执行失败。无论哪种情况出现,都会造成巨大的缺陷,要不就是客户付钱不发货,要不就是客户不付钱却收到了货,这两种无论出现哪种问题后果都将会是致命的。所以我们需要事务控制,把两者作为一个整体,要不两者同时成功,要不全部失败;不可能出现一个成功,一个失败的情况。

1.4 分布式事务

随着互联网的快速发展,软件系统由原来的单体应用转变为分布式应用,分布式系统会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务。例如电商项目中用户下单,此时就需要订单系统和库存系统协同来完成整个下单操作,即首先调用订单系统添加订单,然后调用库存系统扣减库存。

这里强调的是多个系统通过网络协同完成一个事务的过程,并不强调多个系统访问了不同的数据库,即使多个系统访问的是同一个数据库也是分布式事务。 如下图两种形式都属于分布式事务:

graph TD
    A[创建订单] --> B1[订单系统] --> B2[(订单数据库)]
    A --> C1[库存系统] --> C2[(库存数据库)]
    B[创建订单] --> D1[订单系统] --> D2[(同一个数据库,<br>存储订单,库存)]
    B --> E1[库存系统] --> D2

我们知道本地事务依赖数据库本身提供的事务特性来实现,因此以下逻辑可以控制本地事务:

begin transaction;
-- 1、本地数据库操作:张三减少金额
-- 2、本地数据库操作:李四增加金额
commit transation;

但是在分布式环境下,会变成下边这样:

begin

-- 2、远程调用:让李四增加金额
commit transation;

可以设想,当远程调用让李四增加金额成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就回滚了张三减少金额的操作,此时张三和李四的数据就不一致了。

二、分布式事务的基本概念

2.1 分布式事务复杂性

因此在分布式架构的基础上,传统数据库事务就无法使用了,张三和李四的账户不在一个数据库中甚至不在一个应用系统里,实现转账事务需要通过远程调用,由于网络问题就会导致分布式事务问题。当本地事务要扩展到分布式时,它的复杂性进一步增加了。首先就是存储端的多样性。本地事务的情况下,所有数据都会落到同一个DB中,但是,在分布式的情况下,就会出现数据可能要落到多个DB,或者还会落到Redis,落到MQ等中,如下图所示:

graph TD
    A((事务)) --> B[(Redis)]
    A --> C[(MQ)]
    A --> D[(DB2)]
    A --> E[(DB2)]
    style B fill:#fbb369
    style C fill:#fcff66
    style D fill:#6765fe
    style E fill:#6765fe

本地事务的情况下,通常所有事务相关的业务操作,会被我们封装到一个Service方法中。而在分布式的情况下,请求链路被延展、拉长,一个操作会被拆分成多个服务,它们呈现线状或网状,依靠网络通信构建成一个整体。在这种情况下,事务无疑变得更复杂,如下图所示:

graph LR
    A[ServiceA] --> B[ServiceB] --> C[ServiceC] --> D[ServiceD]
    A1[ServiceA] --> B1[ServiceB]
    A1 --> C1[ServiceC] --> D1[ServiceD]
    C1 --> E1[ServiceE]

基于上述两个复杂性,期望有一个统一的分布式事务方案,能够像本地事务一样,以几乎无侵入的方式,满足各种存储介质,各种复杂链路,是不现实的。至少,在当前,还没有一个十分成熟的解决方案。所以,一般情况下,在分布式下,事务会被拆分解决,并根据不同的情况,采用不同的解决方案。

2.2 什么是分布式事务

事务涉及多个操作,多个操作之间的数据必须保持强一致性。对于分布式系统而言,需要保证分布式系统中的数据一致性,保证数据在子系统中始终保持一致,避免业务出现问题。分布式系统中对数要么一起成功,要么一起失败,必须是一个整体性的事务。分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,多个节点的协调工作必须保持原子性,多个节点的逻辑必须同时成功或者同时失败。不能出现部分节点成功,部分失败的情况。

简单的说,在分布式系统上一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务节点上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。举个例子:在电商网站中,用户对商品进行下单,需要在订单表中创建一条订单数据,同时需要在库存表中修改当前商品的剩余库存数量,两步操作一个添加,一个修改,我们一定要保证这两步操作一定同时操作成功或失败,否则业务就会出现问题。

任何事务机制在实现时,都应该考虑事务的ACID特性,包括本地事务、分布式事务。对于分布式事务而言,即使不能都很好的满足,也要考虑支持到什么程度。本质上来说,分布式事务就是为了保证不同数据库、不同服务器节点的数据一致性。

graph LR
    A[ServiceA] --> B[ServiceB] --> D[ServiceD]
    A--> C[ServiceC]
    B--> E[ServiceE]
    A--> R1[(Resource)]
    C--> R2[(Resource)]
    D--> R3[(Resource)]
    E--> R4[(Resource)]
    style R1 fill:#fbb369
    style R2 fill:#fbb369
    style R3 fill:#fbb369
    style R4 fill:#fbb369

三、分布式事务基础理论

通过前面的学习,我们了解到了分布式事务的基础概念。与本地事务不同的是,分布式系统之所以叫分布式,是因为提供服务的各个节点分布在不同机器上,相互之间通过网络交互。不能因为有一点网络问题就导致整个系统无法提供服务,网络因素成为了分布式事务的考量标准之一。因此,分布式事务需要更进一步的理论支持,接下来,我们先来学习一下分布式事务的理论。

3.1 分布式事务产生的情景

  • 跨JVM进程产生分布式事务

典型的场景就是微服务架构了,微服务之间通过远程调用完成事务操作。比如:订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减少库存,如下图所示:

graph LR
    A[订单服务] --远程调用--> B[库存服务]
    A --> C[订单数据库]
    B --> D[库存数据库]
  • 跨数据库实例产生分布式事务

单体系统访问多个数据库实例当单体系统需要访问多个数据库(实例)时就会产生分布式事务。比如:用户信息和订单信息分别在两个MySQL实例存储,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信息,由于数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据,此时产生分布式事务,如下图所示:

graph TD
    A[用户管理系统] --> B[用户数据库]
    A --> C[订单数据库]
  • 多服务访问同一个数据库实例

订单微服务和库存微服务即使访问同一个数据库也会产生分布式事务,原因就是跨JVM进程,两个微服务持有了不同的数据库链接进行数据库操作,此时产生分布式事务,如下图所示:

graph LR
    A[订单服务] --远程调用--> B[库存服务]
    A --> C[订单数据库]
    B --> C

3.2 CAP 基础理论

数据库事务ACID 四大特性,无法满足分布式事务的实际需求,这个时候又如何保证分布式事务?CAP理论是分布式事务处理的理论基础,了解了CAP理论有助于我们研究分布式事务的处理方案。 CAP理论是分布式系统在设计时只能在一致性(Consistency)、可用性(Availability)、分区容忍性(Partition Tolerance)中满足两种,无法兼顾三种。通过下图理解CAP理论:

graph TD
    A[Nginx] --> B[节点A 用户数据]
    A --> C[节点B 用户副本]
    A --> D[节点C 用户副本]

其中:

  • 一致性(Consistency):服务A、B、C三个节点都存储了用户数据, 三个节点的数据需要保持同一时刻数据一致性。
  • 可用性(Availability):服务A、B、C三个节点,其中一个节点宕机不影响整个集群对外提供服务。如果只有服务A节点,当服务A宕机整个系统将无法提供服务,增加服务B、C是为了保证系统的可用性。
  • 分区容忍性(Partition Tolerance):分布式系统不可避免的出现了多个系统通过网络协同工作的场景,节点之间难免会出现网络中断、网延延迟等现象,这种现象一旦出现就导致数据被分散在不同的结点上,这就是网络分区。 当一个数据项只在一个节点中保存,那么分区出现后,和这个节点不连通的部分就访问不到这个数据了,这时分区就是无法容忍的。提高分区容忍性的办法就是一个数据项复制到多个节点上,那么出现分区之后,这一数据项就可能分布到各个区里,容忍性就提高了。总的来说就是,数据存在的节点越多,分区容忍性越高,但要复制更新的数据就越多,一致性就越难保证。为了保证一致性,更新所有节点数据所需要的时间就越长,可用性就会降低。

3.3 CAP 组合方式

分布式系统能否兼顾C、A、P? 在保证分区容忍性的前提下一致性和可用性无法兼顾,如果要提高系统的可用性就要增加多个结点,如果要保证数据的一致性就要实现每个结点的数据一致,节点越多可用性越好,但是数据一致性越差。 所以,在进行分布式系统设计时,同时满足“一致性”、“可用性”和“分区容忍性”三者是几乎不可能的。目前很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。基于 CAP 理论,很多系统在设计之初就要对这三者做出取舍:

组合方式 说明 CA 放弃分区容忍性,加强一致性和可用性,关系数据库按照CA进行设计。 AP 放弃一致性,加强可用性和分区容忍性,追求最终一致性,很多NoSQL数据库按照AP进行设计。
这里放弃一致性是指放弃强一致性,强一致性就是写入成功立刻要查询出最新数据。
追求最终一致性是指允许暂时的数据不一致,只要最终在用户接受的时间内数据一致即可。 CP 放弃可用性,加强一致性和分区容忍性。比如跨行转账,一次转账请求要等待双方银行系统都完成整个事务才算完成。

在分布式系统设计中AP的应用较多,即保证分区容忍性和可用性,牺牲数据的强一致性(写操作后立刻读取到最 新数据),保证数据最终一致性。比如:订单退款,今日退款成功,明日账户到账,只要在预定的用户可以接受的时间内退款事务走完即可。

小结

把今天最好的表现当作明天最新的起点…...~

  投身于天地这熔炉,一个人可以被毁灭,但绝不会被打败!一旦决定了心中所想,便绝无动摇。迈向光明之路,注定荆棘丛生,自己选择的路,即使再荒谬、再艰难,跪着也要走下去!放弃,曾令人想要逃离,但绝境重生方为宿命。若结果并非所愿,那就在尘埃落定前奋力一搏!

划重点.gif