数据库的事务,并发控制,锁
数据库的事务
数据库并发控制
数据库锁
数据库事务
什么是事务
数据库几乎是所有系统的核心模块,它将数据有条理地保存在储存介质(磁盘)中,并在逻辑上,将数据以结构化的形态呈现给用户。支持数据的增、删、改、查,并在过程中保障数据的正确且可靠。
要做到这点并非易事,常见的例子就是银行转账,A账户给B账户转账一个100块,在这种交易的过程中,有几个问题值得思考:
如何同时保证上述交易中,A账户总金额减少100元,B账户总金额增加100元? A
A账户如果同时在和C账户交易(T2),如何让这两笔交易互不影响? I
如果交易完成时数据库突然崩溃,如何保证交易数据成功保存在数据库中? D
如何在支持大量交易的同时,保证数据的合法性(没有钱凭空产生或消失) ? C

要保证交易正常可靠地进行,数据库就得解决上面的四个问题,这也就是事务诞生的背景,它能解决上面的
四个问题,对应地,它拥有四大特性:

ACID
接下来详细地了解这四大特性:
原子性,确保不管交易过程中发生了什么意外状况(服务器崩溃、网络中断等),不能出现A账户少了100元,但B账户没到帐,或者A账户没变,但B账户却凭空收到100元(数据不一致).A和B账户的金额变动要么同时成功,要么同时失败(保持原状)。
隔离性,如果A在转账100元给B(T1),同时C又在转账200元给A(T2),不管T1和T2谁先执行完毕,最终结果必须是A账户增加200元,而不是300元,B增加100元,C减少300元。
持久性, 确保如果T1刚刚提交,数据库就发生崩溃,T1执行的结果依然会保持在数据库中。
一致性, 确保钱不会在系统内凭空产生或消失,依赖原子性和隔离性。

如何保证原子性

要保证上面操作的原子性,就得等begin和commit之间的操作全部成功完成后,才将结果统一提交给数据库保存,如果途中任意一个操作失败,就撤销前面的操作,且操作不会提交数
据库保存,这样就保证了同生共死。
如何保证隔离性
原子性的问题解决了,但是如果有另外的事务在同时修改数据A怎么办呢? 虽然可以保证事务的同生共死,但是数据一致性会被破坏。 此时需要引入数据的隔离机制,确保同时只
能有一个事务在修改A,一个修改完了,另一个才来修改。 这需要对数据A加上互斥锁:
1.先获得了锁,然后才能修改对应的数据A
2.事务完成后释放锁,给下一个要修改数据A的事务
3.同一时间,只能有一个事务持有数据A的互斥锁
4. 没有获取到锁的事务,需要等待锁释放
以上面的事务为例,称作T1,T1在更新A的时候,会给A加上互斥锁,保证同时只能有一个事务
在修改A.那么这个锁什么时候释放呢?当A更新完毕后,正在更新B时(T1还没有提交),有另外
一个事务T2想要更新A,它能获取到A的互斥锁吗?
这就是隔离性的关键,针对隔离性的强度,有以下四的级别(引用自这篇文章):
串行化(Serializable,SQLite默认模式):最高级别的隔离。两个同时发生的事务100%隔离,每个事务有自己的"世界", 串行执行。
可重复读(Repeatable read,MySQL默认模式):如果一个事务成功执行并且添加了新数据(事务提交),这些数据对其他正在执行的事务是可见的。但是如果事务成功修改
了一条数据,修改结果对正在运行的事务不可见。所以,事务之间只是在新数据方面突破了隔离,对已存在的数据仍旧隔离。
读取已提交(Read committed,DM,Oracle、PostgreSQL、SQL Server默认模式):可重复读+新的隔离突破。如果事务A读取了数据D,然后数据D被事务B修改(或删除)
并提交,事务A再次读取数据D时数据的变化(或删除)是可见的。这叫不可重复读(non-repeatable read)。
读取未提交(Read uncommitted):
最低级别的隔离,是读取已提交+新的隔离突破。
如果事务A读取了数据D,然后数据D被事务B修改(但并未提交,事务B仍在运行中),事务A再次读取数据D时,数据修改是可见的。如果事务B回滚,那么事务A第二次读取的数据D是
无意义的,因为那是事务B所做的从未发生的修改(已经回滚了嘛)。这叫脏读(dirty read)。

如何保证持久性
隔离性的问题解决了,但是如果在事务提交后,事务的数据还没有真正落到磁盘上,此时数据
库奔溃了,事务对应的数据会不会丢?
事务会保证数据不会丢,当数据库因不可抗拒的原因奔溃后重启,它会保证:
成功提交的事务,数据会保存到磁盘
未提交的事务,相应的数据会回滚
并发控制
事务为什么需要并发执行?
如果事务都是串行执行,是不是就没有这么多问题了。确实是这样,数据库中所有事务都串行执行,整个数据库状态机以及状态转移的模型就非常简单,早期在硬件能力受限以及
并行处理能力尚未成熟时,事务串行执行确实可以满足要求了。但是随着硬件能力的提升、多核处理器、numa架构的cpu的兴起,以及业务对数据处理速度和吞吐要求的提升,事务
串行执行一方面没办法发挥硬件的能力,另外一方面也无法满足真实业务场景的需求。因此事务并发执行是必然的结果。
事务并发执行还需要保证ACID吗?
需要。事务的并发执行主要目的是满足业务对数据库系统的性能要求,而ACID是业务对数据库系统的基本要求。其实原子性和持久性的保证,在事务串行执行和并发执行时没有太大
的区别。事务并发控制最主要的问题是满足一致性和隔离性的要求。
事务并发控制如何保证正确性?
事务并发调度需要保证调度事务并发执行的结果与某一种事务串行执行的结果相同,也就是事务并发调度如果是可串行化的,那么认为该调度是正确的。”冲突可串行化”是验证
事务并发调度是否是可串行化的常用手段,通过交换事务间不冲突操作的执行顺序并保证冲突操作执行顺序的偏序关系,如果最终事务操作执行序列与某一种事务串行执行的操作执行
序列相同,那么认为该事务并发调度是”冲突可串行化的”
两个操作冲突需要满足以下两个条件:
1)操作的是相同的数据;
2)两个操作来自不同的事务,并且至少一个是write操作。因此冲突操作可以分为读写冲突、写读冲突、写写冲突。
事务并发调度保证了事务的ACID,那事务不同的隔离级别是怎么回事?
我们知道事务有很多不同的隔离级别,read uncommited、read commited、repeat read、snapshot isolation、serializable等待,事务隔离级别的定义也是经过了很长时间演进。截止目前,我们所说的隔离级别其实是指serializable。实际应用场景中发现满足serializable的并发调度,在事务的执行效率和吞吐仍然无法满足要求。
因此,就有了通过降低隔离性的保证来换取数据库事务执行的并发度的提高,熟悉的”trade off”,隔离级别就是用来定义不同程度的隔离性保证。理论上,隔离性越低,数据库事务的并发度越高。
serializable以下的隔离级别,是以牺牲一定的一致性和隔离性来换取数据库执行事务并发度的提高。
什么是并发控制协议?
并发控制协议是数据库用来调度多个事务操作并发执行并保证执行结果符合预期的方式。通常,可以认为并发控制协议就是用来保证事务并发执行时的schedule是冲突可串行化的。并发协议可以分为”悲观并发控制协议”和”乐观并发控制协议”两大类。
基于两阶段锁的并发控制协议
可以通过”交换不冲突的操作”或者”优先图”的方式来验证某个事务调度是否是”冲突可串行化的”。通过加锁的方式可以控制冲突操作的并发问题,并且如果事务在访问或修改某个数据项的过程中都通过相应的锁来保护,直到事务结束的时候才释放锁,那么其他事务与该事务所有的冲突的操作必然是可串行化的, 这就是两阶段锁的基本逻辑。
锁管理器负责锁的分配,如果请求的锁与其他的锁是相容的,那么成功分配,否则加锁请求需要等待或者失败。锁的类型可以分为s-lock(共享锁)、 x-lock(排他锁)。一般来说,读取操作需要持有s-lock、写入操作需要持有x-lock。只有两个加s-lock的请求是相容的,其他任意类型的两个加锁请求都是不相容的。

两阶段锁协议内容如下:
阶段一(Growing):事务向锁管理器申请所需要的锁,锁管理器分配或拒绝对应的锁。
阶段二(Shrinking):事务释放在Growing阶段申请的锁,并不能再申请新的锁。
级联回滚
级联回滚是指一个事务的回滚引起了一系列相关事务的回滚。在两阶段协议下,级联回滚是可能发生的。如下图所示,当T1回滚后,T2也必须回滚,因为T2中
读到T1中未提交的对A的未提交的修改,如果T2最终提交那么相当于读到了一个从未存在过的值,可能会违反一致性,并且T1相当于部分提交, 违反了原子性。

严格两阶段协议(Strict two-phase locking protocol)可以避免级联回滚,它在两阶段协议的基础上要求事务持有的所有排他锁必须在事务提交后方可释放。这个要求保证了未提
交事务所写的任何数据在该事务提交之前均以排他方式加锁,防止了其他事务读到这些数据
强两阶段协议(Strong Strict two-phase locking protocol)是在严格两阶段协议的基础上,进一步要求事务所有的共享锁也必须在事务提交后方可释放。这样其实Growing阶段横跨
了整个事务执行的过程,其实相当于只有一个阶段了,因此实现起来比较简单,被广泛使用。

多版本控制
多版本并发控制(Multi-Version Concurrency Control,以下简称MVCC) 是当今数据库领域最流行的并发控制实现,MVCC 在最大化并发度的情况下尽可能保证事务的正
确性,其好处有:
写不会阻塞读
只读事务无需数据库锁就能支持可重复读可以很好地支持历史数据查询
MVCC的关键在于首先假设数据库读写冲突不会很大,其次通过维护同一份数据的多个版本,是的事务之间的冲突尽可能小;当一个事务修改数据的时候,创建一个新的
版本,当一个事务读数据的时候,返回最新版本数据;所有对于数据的修改都发生在事务的私有空间内,在提交的时候进行验证。
MVCC的技术要点:
1.并发控制协议
2.多版本存储
3.垃圾回收
4.索引管理
并发控制协议
MVTO
通过预先计算顺序的方式来控制并发;事务的读操作返回最新的没有被写锁
锁定数据的版本;事务的写操作过程如下:
当前没有活跃的事务锁定数据
当前事务的事务编号大于最新数据中的读事务的事务编号
如果这上述条件成立,那么创建一个新的数据版本
MVOCC
在 MVOCC 中,事务被分成三个阶段,分别是:
读数据阶段,着这个阶段新的版本被创建出来。
验证阶段,在这个阶段一个提交编号被分配给该事务,然后基于这个编号进行验证;
提交阶段,完成提交。
MV2PL
顾名思义,MV2PL 是传统的两阶段锁在多版本并发控制中的应用;事务读写或者创建数
据版本都需要获得对应的锁。
SSI
可串行化快照隔离(serializable snapshot isolation或SSI)是在快照隔离级别之上,支
持串行化。PosgtreSQL 中实现了这种隔离级别,数据库通过维护一个串行的图移除事
务并发造成的危险结构。

多版本存储
数据库通过无锁指针链表维护多个版本,使得事务可以方便的读取特定版本的数据。

仅限追加存储(Append-Only)
所有的版本存储在同一个表空间
更新的时候追加在版本链表上追加新节点
链表可以以最旧到最新的方式组织,
链表也可以以最新到最旧的方式组织,表头为最新版本
时序存储(Time-Travel Storage)
每次更新的时候将之前的版本放到旧表空间
更新主表空间中的版本
仅差异存储(Delta Storage)
每次更新近存储修改的部分,将其加入链表,主表空间存储当前版本
通过旧的修改部分,可以创建旧版本
垃圾回收

MVCC 在事务过程中不可避免的会产生很多的旧版本,这些旧版本会在下列情况下被回收
对应的数据上没有活跃的事务
某版本数据的创建事务被终止
数据行级别垃圾回收(Tuple Level)
通过检查数据来判断是否需要回收旧版本,有两种做法:
启动一个后台线程进行数据行级的垃圾回收
当事务操作数据行时,顺便做一些垃圾回收的事情
事务级别垃圾回收(Transaction Level)
事务自己追踪旧版本,数据库管理系统不需要通过扫描数据行的方式来判断数据是否需要回收。
索引管理
数据有多个版本,而索引只有一份,更新和维护多个版本的时候如何同步索引?
主键(Primary Key)
主键一般指向多版本链表头
副索引(Secondary Indexes)
有两种做法,逻辑指针和物理地址;前者通过增加一个中间层的方式实现,缩影指向该中间层,中间层指向数据的物理地址,避免应为多版本的物理地址改变引起的索引树的更新;后者索
引直接指向数据物理地址。
并发操作会带来哪些问题呢?
并发操作会带来的数据不一致性:
1. 丢失数据(W-W)
两个事务T1、T2同时读入同一数据并修改,T2提交的结果破坏了T1提交的结果,导致T1
的修改被丢失

2. 不可重复读(R-W)
事务T1读取某一个数据后,事务T2执行更新操作,使T1无法再现前一次读取结果,包括三种情况:
(1)T2执行修改操作,T1再次读数据时,得到与前一次不同的值
(2)T2执行删除操作,T1再次读数据时,发现某些记录神秘的消失了
(3)T2执行插入操作,T1再次读数据时,发现多了一些记录
(4)发生的不可重复读有时也称为幻影现象。

3. 读“脏”数据(W-R)
事务T1修改某一数据并将其写回磁盘,事务T2读取同一数据后,T1由于某种原因被撤销,这时被T1修改过的数据恢复原值,T2读到的数据就与数据库中的数据不一致,则T2读到的数据就为“脏”数据,即不正确的数据。

并发操作会带来哪些问题呢?
并发操作会带来的数据不一致性:
1. 丢失数据(W-W)
两个事务T1、T2同时读入同一数据并修改,T2提交的结果破坏了T1提交的结果,导致T1
的修改被丢失

如何避免发生这种数据不一致的现象?
并发控制机制的任务:对并发操作进行正确调度、保证事务的隔离性、保证数据库的一致性。
并发控制的主要技术有:封锁、时间戳、乐观控制法、多版本并发控制。
数据库锁
锁机制是数据库一个比较重要的机制,在处理事务的并发性方面起着至关重要的作用
三个问题:
一是把上面这些锁都是什么,怎么定义的搞明白;
二是把这些乱七八糟的锁什么场景下使用、怎么使用搞明白;
三是这些锁和事务之间是什么关系

锁的分类
1.1、乐观锁和悲观锁(从策略上划分)
乐观锁:顾名思义就是非常乐观,非常相信真善美,每次去读数据都认为其它事务没有在写数据,所以就不上锁,快乐的读取数据,而只在提交数据的时候判断其它事务
是否搞过这个数据了,如果搞过就rollback。乐观锁相当于一种检测冲突的手段,可通过为记录添加版本或添加时间戳来实现。
悲观锁:对其它事务抱有保守的态度,每次去读数据都认为其它事务想要作祟,所以每次读数据的时候都会上锁,直到取出数据。悲观锁大多数情况下依靠数据库的锁机
制实现,以保证操作最大程度的独占性,但随之而来的是各种开销。
悲观锁相当于一种避免冲突的手段。
选择标准:如果并发量不大,或数据冲突的后果不严重,则可以使用乐观锁;而如果并发量大或数据冲突后果比较严重(对用户不友好),那么就使用悲观。
**注意**:首先明确一点乐观锁和悲观锁是一种编程策略,并不是数据库具有悲观锁和乐观锁。
悲观锁实现代码:

乐观锁两种实现方式:
一是使用数据版本(Version)记录机制实现。这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字
类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值+1。当我们提交更新的时候,判断数据库表对应
记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
如果更新操作顺序执行,则数据的版本(version)依次递增,不会产生冲突。但是如果发生有不同的业务操作对同一版本的数据进行修改,那么,先提交的操作会把数据
version更新为2,当A在B之后提交更新时发现数据的version已经被修改了,那么A的更新操作会失败。乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的
table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp).和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进
行对比,如果一致则OK,否则就是版本冲突。

1.2、共享锁和排它锁(从读写角度划分)
共享锁(S锁,Shared Lock):也叫读锁(Read Lock),持有S锁的事务只读不可写。如果事务A对数据D加上S锁后,其它事务只能对D加上S锁而不能加X锁。
排它锁(X锁,Exclusive Lock):也叫写锁(Write Lock),持有X锁的事务可读可写。如果事务A对数据D加上X锁后,其它事务不能再对D加任何锁,直到A对D的锁解除。
选择标准:数据库根据sql语句选择加什么锁
如何使用:数据库自身创建
1.3、表级锁和行级锁(从粒度角度划分)
表级锁(Table Lock):表级锁将整个表加锁,性能开销最小。用户可以同时进行读操作。当一个用户对表进行写操作时,用户可以获得一个写锁,写锁禁止其他的用户读写操作。写
锁比读锁的优先级更高,即使有读操作已排在队列中,一个被申请的写锁仍可以排在所队列的前列。
行级锁(Row Lock):行级锁仅对指定的记录进行加锁,这样其它进程可以对同一个表中的其它记录进行读写操作。行级锁粒度最小,开销大,能够支持高并发,可能会出现死锁。
选择标准:数据库根据sql语句选择加什么锁
如何使用:数据库自行加锁
在使用以下语句时,Oracle会自动应用行级锁:
INSERT、UPDATE、DELETE、SELECT … FOR UPDATE [OF columns] [WAIT n | NOWAIT];
SELECT … FOR UPDATE语句允许用户一次锁定多条记录进行更新.使用commit
或者rollback释放锁。
关于共享锁排它锁、表级锁行级锁的解释清参考下面:


最后总结下:
如果执行insert、update、delete、select .... for update语句,表上共享锁,对应的数据行上会加行级排它锁。该表不能执行ddl语句,对应的数据行不能执行update、delete
(因为排它锁会阻塞,直到锁释放),可以执行select语句,其他行数据可以执行除ddl的任何记录。
执行ddl语句会加表级排它锁。表里的所有数据都不能执行dml语句。也证明了行级锁只有排他属性。
社区地址:https://eco.dameng.com




