Distributed_事务_并发控制(隔离性)

1. 事务__并发控制(隔离性)

  1. 事务的定义:把应用程序的多个读、写操作捆绑在一起成为一个逻辑操作单元。且具有ACID特性
  2. 目的: 简化应用层的编程模型;应用层的处理就变得简单很多。

那该如何判断是否需要事务呢?

​ 我们需要确切地理解事务能够提供哪些安全性保证?背后的代价又是什么?

1. 事务的隔离性:

​ 解决的问题:如果数据库支持多个客户端同时访问,但是这些客户端如果访问的是相同的记录,则可能会遇到并发问题(存在竞争条件)。

竞争条件/依赖关系

​ 对于同一个数据,存在一个事务对其修改且另一个事务对其读取或者两个事务对该数据进行修改时,存在竞争条件

ACID隔离性:

​ 并发执行的事务相互隔离,它们不能互相交叉.但是隔离性根据不同程度可以分为未提交读、提交读、可重复读、可串行化(隔离性的经典定义其实就是把隔离性定义为可串行化即虽然实际上并发事务可以同时执行,但数据库系统要确保当事务提交时,其结果与串行执行(一个接一个)完全相同)

处理错误和中止:

​ 数据库对于错误的处理和中止主要有两个方面的理念:

  1. 如果存在违反原子性、隔离性或持久性的风险,则完全放弃整个事务,而不是部分放弃
  2. 数据库已经尽其所能,但万一遇到错误,系统并不会撤销已完成的操作,此时需要应用程序来负责从错误中进行恢复。

隔离级别中最高级别是串行化级别,但是串行化级别的隔离会严重影响性能,因此大多数数据库采用较弱的隔离级别。

2.弱隔离级别

实际上很多流行的关系数据库系统(通常被认为是ACID兼容)其实也采用的是弱级别隔离,所以它们未必可以阻止类似错误的发生.

概述:

​ 本小节将会介绍各种弱隔离级别类型、可能发生的竞争条件、以及实现的方法=> 最终可以根据自己的需求选择合适的隔离级别. 弱隔离级别:读-提交、可重复读、串行化

2.1 读-提交

提供的保证:

  1. 读数据,只能读取已经提交的数据(防止"脏读")

    需要防止脏读的情况(解决问题):

    1. 如果事务需要更新多个对象,脏读意味着另一个事务可能会看到部分更新,而非全部。
    2. 如果事务发生中止,则该事务所有写入操作都需要回滚。
  2. 写数据,只能覆盖已经提交的数据(防止“脏写”)

    需要防止脏写的情况(解决的问题):

    ​ 多个事务对同一个对象尝试并发更新,会出现非预期的错误结果。详见《数据密集型应用系统设计》P223

实现:

  1. 对于脏写的实现方法:

    数据库常采用行级锁来防止脏写:当事务想修改某个对象(例如行和文档)等,它必须获得该对象的锁,然后一直持有锁才能更新对象,否则必须等候。(一级封锁协议:获取的行级写锁)

  2. 对于脏读的实现方法:

    1. 数据库常采用行级锁来防止脏写:当事务想修改某个对象(例如行和文档)等,它必须获得该对象的锁,然后一直持有锁才能更新对象,否则必须等候,读取对象时也必须获得该锁,否则必须等候。 (写获取的是行级写锁,直到事务提交释放;读获取的是行级读锁,直到读取结束释放)=>但是对于如此防止脏读的方法,会造成大量读取事务等候(因为读取一个事务需要获得读锁但是有另一个事务获得该对象的写锁,所以无法读取该对象)
    2. 为避免大量读取事务等候,采用的是:对于每个待更新的对象,数据库都会维护其旧值和当前持锁事务将要设置的新值两个版本(当该对象被更新时会维护一个旧值供事务读取),在事务提交之前,所有其他读操作都将读取旧值;仅当写事务提交之后,才会读取新值。

2.2 可重复读

1.不可重复读(读倾斜):

在读-提交隔离级别的情况下,可能会导致一个事务对同一个对象进行多次读取,读取的结果可能不一致(两次读取的结果可能分别是另一个事务提交前后的值)

在这里插入图片描述
2.需要满足可重复读的场景:

  1. 备份场景:备份任务要复制整个数据库,这可能需要数小时才能完成。但是在备份的过程中可以继续写入数据库。因此得到的镜像里可能包含部分旧版本数据和部分新版本的数据。如果从这样的备份进行恢复,最终就导致永久性的不一致。

3.解决不可重复读的方法–快照级别隔离:

​ 其总体思想:每个事务都从数据库的一致性快照中读取,事务一开始所看到的是最近提交的数据,即使数据随后可能被另一个事务更改,但保证每个事务都只看到该特定时间点的旧数据。 每个事务的写操作则对其写对象加上行级写锁。

关键:读操作不会阻止写操作,反之亦然。这使得数据库可以在处理正常写入的同时,在一致性快照上执行长时间的只读查询,且两者之间没有任何锁的竞争。

3.1 多版本并发控制(MVCC):数据库保留了对象多个不同的提交版本来实现快照级别隔离

下面介绍了PostgreSQL如何实现基于MVCC的快照级别隔离

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J1LgVxEw-1657089978665)(Distributed_事务__并发控制(隔离性)].assets/image-20220617153609542-16554513704962-16554680088603.png)

  1. 事务的ID号和数据库中对象的版本号:

    当事务开始时,首先赋予一个唯一的、单调递增的事务ID(txid)。 每当事务向数据库写入新内容时,所写的数据都会被标记写入者的事务ID。

  2. 数据库中的每一行都有两个字段created-by、deleted-by。**created-by字段为创建该行的事务ID号。 ** deleted-by字段,初始为空。如果事务要删除某行,该行实际上并未从数据库中删除,而只是将deleted-by字段设置为请求删除的事务ID号 只有当删除该行事务提交后,且确定没有其他事务引用该标记删除的行时,数据库的垃圾回收进程才会真正删除并释放存储空间.

  3. 一致性快照的可见性原则(如何读取?): 当事务读取数据库时,通过事务ID可以决定哪些对象可见,哪些不可见。要想对上层应用维护好快照的一致性,需要精心定义数据的可见性规则。

    比如(需要下面连个):

    1. 事务开始的时刻,创建该对象的事务已经完成了提交。(防止脏读、防止读取到事务ID号更大的快照)
    2. 对象没有被标记为删除;或者即使标记了,但删除事务在当前事务开始时还没提交。

    快照A的可见的具体时刻要求:

    		快照A的created-by字段的事务提交时刻点         事务的开始时刻点           快照A的deleted-by字段的事务提交时刻点
    时间                                      <                       <
    
  4. 这种多版本数据库该如何支持索引呢?

    1. 方法1索引直接指向对象的所有版本,然后想办法过滤对当前事务不可见的那些版本

2.3 防止修改丢失(写丢失)

场景:

​ 更新丢失可能发生在这样一个操作场景中:应用程序从数据库中读取某些值,根据应用逻辑做出修改,然后写回新值(read-modify-write过程).如果有两个事务在同一个对象上执行类似操作,由于事务隔离性,第二个写操作并不会在第一个写操作修改后的值基础上进行更改。正确的效果应该是两个写事务的叠加,而不是两个事务的覆盖

解决方案:

  1. 原子写操作

    原子操作通常采用对读取对象采用**独占锁(类似于写锁)**的方式来实现

    或者强制所有原子操作都在单线程上执行

2.4 串行化

Overview:

  1. 可串行化是什么?以及如何执行的?
  2. 可串行化的数据库使用的三种技术:
    1. 严格按照串行顺序执行(实际的串行执行)
    2. 两阶段锁定(2PL),几十年来这几乎是唯一可行的选择
    3. 乐观并发控制,例如可串行化的快照隔离,

可串行化的定义:

​ 即使事务可能会并行执行,但最终的结果与每次一个即串行执行结果相同.换句话说,数据库可以防止所有可能的竞争条件.

技术一:实际的串行执行

解决方案:在一个线程上顺序方式每次只执行一个事务

技术二:两阶段加锁

​ 两阶段加锁的关键之处:(快照级别的隔离读写是互不干扰的,而两阶段加锁的读写是干扰的)多个事务可以同时读取同一个对象,但是只要出现任意的写操作,则必须加锁以独占访问。(实际上就是对数据库加上读写锁即Go中的sync.RWMutex,防止脏写中加的是互斥锁即Go中的sync.Mutex)

	1. 如果事务A已经读取了某个对象,此时事务B想要写入该对象,那么B必须等到A提交或中止才能继续。以确保B不会在事务A执行的过程中间去修改对象。
	1. 如果事务A已经修改了对象,此时事务B想要读取该对象,则B必须等到A提交或中止之后才能继续。

如何实现两阶段加锁(2PL)?

前提:为每个对象用一个读写锁来隔离读写操作。

2PL两阶段加锁的由来:

  1. 一个事务开始时刻:需要获取所有涉及对象的锁(对对象读取则获得读锁,对对象写操作则获的写锁),如果一个对象的锁无法获得则该事务必须等候。(加锁阶段/开始阶段)
  2. 一个事务结束时刻:才释放所有对象的锁。(解锁阶段/结束阶段)

2.5 可串行化的快照隔离(SSI,Serializable Snapshot Isolation)

串行化相对于弱隔离级别,虽然可以避免边界条件(如更新丢失、写倾斜、幻读等)但是对数据库的性能影响巨大。因此,串行化的隔离和性能是不是从根本上就是互相冲突而无法兼得呢?

或许并非如此,最近一种称为可串行化的快照隔离(Serializable Snapshot Isolation)

2.5.1.乐观和悲观的并发控制

  1. 乐观和悲观的并发控制的最大区别:
    1. 悲观并发控制在提交前不会进行冲突检查,而乐观并发控制则在提交前进行冲突检查。
    2. 悲观并发控制对于可能发生出错的操作(比如与其他并发事务发生了锁冲突)直接丢弃,而乐观并发控制则对于该类操作继续执行。

​ 乐观并发控制的设计原则:如果一个事务存在潜在冲突或出错,则会继续执行而不是中止,直到提交时会检查冲突(即违反了隔离性原则),如果是的话,终止事务并继续重试.

2.5.2.检测写入之间的串行化冲突算法(基于过期的条件做决定)

​ 当应用程序执行查询时,数据库无法预知应用层如何使用这些查询结果。安全起见,数据库当查询结果与数据库当前对象不符(任何变化)时都会使写事务失效。因此必须检测:

检测是否读取了过期的MVCC对象:

​ SSI在提交时刻可能读取的MVCC对象(快照)是过期的(**过期定义:**提交时刻数据库的版本号与此时事务读取的对象的版本号不一致.),因此

SSI隔离级别中当事务提交时,数据库会检查是否存在一些当初被忽略但当前已经提交的写操作,如果存在这中止该事务.

因为基于MVCC的快照隔离中,一个事务A读取会根据快照可见性原则会忽略该A事务开始时刻还没提交的快照,但是在该事务提交时,可能被忽略的快照被提交了,造成该A事务读取的快照是过期的.

2.5.3 SSI的优点

  1. 可串行化快照隔离不需要等候其他事务所持有的锁。读写通常不会阻塞。特别是,在一致性快照上执行只读查询不需要任何锁。

本文是根据阅读《数据密集型应用系统设计》和查阅相关论文等而写成。

猜你喜欢

转载自blog.csdn.net/Blockchain210/article/details/125639565
今日推荐