1.数据库并发控制的作用

1.1 事务的概念

在介绍并发控制前,首先需要了解事务。数据库提供了增删改查等几种基础操作,用户可以灵活地组合这几种操作,实现复杂的语义。在很多场景下,用户希望一组操作可以做为一个整体一起生效,这就是事务。事务是数据库状态变更的基本单元,包含一个或多个操作(例如多条SQL语句)。经典的转账事务,就包括三个操作:(1)检查A账户余额是否足够。(2)如果足够,从A扣减100块。(3)B账户增加100块。

事务有个基本特性:这一组操作要么一起生效,要么都不生效,事务执行过程中如遇错误,已经执行的操作要全部撤回,这就是事务的原子性。如果失败发生后,部分生效的事务无法撤回,那数据库就进入了不一致状态,与真实世界的事实相左。例如转账事务从A账户扣款100块后失败了,B账户还未增加款项,如果A账户扣款操作未撤回,这个世界就莫名奇妙丢失了100块。原子性可以通过记日志(更改前的值)来实现,还有一些数据库将事务操作缓存在本地,如遇失败,直接丢弃缓存里的操作。

事务只要提交了,它的结果就不能改变了,即使遇到系统宕机,重启后数据库的状态与宕机前一致,这就是事务的持久性。数据只要存储非易失存储介质,宕机就不会导致数据丢失。因此数据库可以采用以下方法来保证持久性:(1)事务完成前,所有的更改都保证存储到磁盘上了。或(2)提交完成前,事务的更改信息,以日志的形式存储在磁盘,重启过程根据日志恢复出数据库系统的内存状态。一般而言,数据库会选择方法(2),原因留给读者思考。

数据库为了提高资源利用率和事务执行效率、降低响应时间,允许事务并发执行。但是多个事务同时操作同一对象,必然存在冲突,事务的中间状态可能暴露给其它事务,导致一些事务依据其它事务中间状态,把错误的值写到数据库里。需要提供一种机制,保证事务执行不受并发事务的影响,让用户感觉,当前仿佛只有自己发起的事务在执行,这就是隔离性。隔离性让用户可以专注于单个事务的逻辑,不用考虑并发执行的影响。数据库通过并发控制机制保证隔离性。由于隔离性对事务的执行顺序要求较高,很多数据库提供了不同选项,用户可以牺牲一部分隔离性,提升系统性能。这些不同的选项就是事务隔离级别。

数据库反映的是真实世界,真实世界有很多限制,例如:账户之间无论怎么转账,总额不会变等现实约束;年龄不能为负值,性别最多只能有男、女、跨性别者三种选项等完整性约束。事务执行,不能打破这些约束,保证事务从一个正确的状态转移到另一个正确的状态,这就是一致性。不同与前三种性质完全由数据库实现保证,一致性既依赖于数据库实现(原子性、持久性、隔离性也是为了保证一致性),也依赖于应用端编写的事务逻辑。

1.2 事务并发控制如何保证隔离性

为了保证隔离性,一种方式是所有事务串行执行,让事务之间不互相干扰。但是串行执行效率非常低,为了增大吞吐,减小响应时间,数据库通常允许多个事务同时执行。因此并发控制模块需要保证:事务并发执行的效果,与事务串行执行的效果完全相同(serializability),以达到隔离性的要求。

为了方便描述并发控制如何保证隔离性,我们简化事务模型。事务是由一个或多个操作组成,所有的操作最终都可以拆分为一系列读和写。一批同时发生的事务,所有读、写的一种执行顺序,被定义为一个schedule,例如:

T1、T2同时执行,一个可能的schedule: T1.read(A),T2.read(B),T1.write(A),T1.read(B),T2.write(A)
如果并发事务执行的schedule效果与串行执行的schedule(serial schedule)等价,就可以满足serializability。一个schedule不断调换读写操作的顺序,总会变成一个serializable schedule,但是有的调换可能导致事务执行的结果不一样。一个schedule中,相邻的两个操作调换位置导致事务结果变化,那么这两个操作就是冲突的。冲突需要同时满足以下条件:

1.这两个操作来自不同事务
2.至少有一个是写操作
3.操作对象相同

因此常见的冲突包括:
• 读写冲突。事务先A读取某行数据、事务B后修改该行数据,和事务B先修改某行事务、事务A后读该行记录两种schedule。事务A读到的结果不同。这种冲突可能会导致不可重复读异象和脏读异象。

• 写读冲突。与读写冲突产生的原因相同。这种冲突可能会导致脏读异象。
• 写写冲突。两个操作先后写一个对象,后一个操作的结果决定了写入的最终结果。这种冲突可能会导致更新丢失异象。

数据库只要保证,并发事务的schedule,保持冲突操作的执行顺序不变,只调换不冲突的操作,可以成为serial schedule,就可以认为它们等价。这种等价判断方式叫做conflict equivalent:两个schedule的冲突操作顺序相同。

例如下图的例子,T1 write(A)与T3 read(A)冲突,且T1先于T3发生。T1 read(B)和 T2 write(B)冲突,且T2先于T1,因此左图事务执行的schedule,与T2,T1,T3串行执行的serial schedule(右图) 等价。左图的执行顺序满足conflict serializablity。

再分析一个反例:T1 read(A)与T2 write(A)冲突且T1先于T2,T2 write(A)与T2 write(A)冲突且T2先于T1。下图这个个schedule无法与任何一个serial schedule等价,是一个不满足conflict serializablity的执行顺序,会造成更新丢失的异象。

总体来说,serializability是比较严格的要求,为了提高数据库系统的并发性能,很多用户愿意去降低隔离性的要求以寻求更好的性能。数据库系统往往会实现多种隔离级别,供用户灵活选择,关于事务隔离级别,可以参看这篇文章。

并发控制的要求清楚了,如何实现呢?后文将依据冲突检测的乐观程度,一一介绍并发控制常见的实现方法。

2.基于两阶段锁的并发控制

2.1 2PL

既然要保证操作按正确的顺序执行,最容易想到的方法就是加锁保护访问对象。数据库系统的锁管理器模块,专门负责给访问对象加锁和释放锁,保证只有持有锁的事务,才能操作相应的对象。锁可以分为两类:S-Lock和X-Lock,S-Lock是读请求使用的共享锁,X-Lock是写请求使用的排他锁。它们的兼容性如下:操作同一个对象,只有两个读请求相互兼容,可以同时执行,读写和写写操作都会因为锁冲突而串行执行。

2PL(Two-phase locking)是数据库最常见的基于锁的并发控制协议,顾名思义,它包含两个阶段:

• 阶段一:Growing,事务向锁管理器请求它需要的所有锁(存在加锁失败的可能)。

• 阶段二:Shrinking,事务释放Growing阶段获取的锁,不允许再请求新锁。

为什么加锁和放锁要泾渭分明地分为两个阶段呢?
2PL并发控制目的是为了达到serializable,如果并发控制不事先将所有需要的锁申请好,而是释放锁后,还允许再次申请锁,可能出现事务内两次操作同一对象之间,其它事务修改这一对象(如下图所示),进而无法达到conflict serializable,出现不一致的现象(下面的例子是lost update)。

2PL可以保证conflict serializability,因为事务必须拿到所有需要的锁才能执行。例如正在执行的事务A与事务B冲突,事务B要么已经执行完,要么还在等待。因此那些冲突操作的执行顺序,与BA或AB串行执行时冲突操作执行顺序一致。

所以,数据库只要采用2PL就能保证一致性和隔离性了吗?来看一下这个例子:

以上执行顺序是符合2PL的,但T2读到了未提交的数据。如果此时T1回滚,则会引发级联回滚(T1的更改,不能被任何事务看到)。因此,数据库往往使用的是加强版的S(trong)S(trict)2PL,它相较于2PL有一点不同:shrinking阶段,只能在事务结束后再释放锁,完全杜绝了事务未提交的数据被读到。

2.2 死锁处理

并发事务加锁放锁必然绕不开一个问题--死锁:事务1持有A锁等B锁,事务2持有B锁等A锁。目前解决死锁问题有两种方案:

• Deadlock Detection:
数据库系统根据waits-for图记录事务的等待关系,其中点代表事务,有向边代表事务在等待另一个事务放锁。当waits-for图出现环时,代表死锁出现了。系统后台会定时检测waits-for图,如果发现环,则需要选择一个合适的事务abort。

• Deadlock Prevention:
当事务去请求一个已经被持有的锁时,数据库系统为防止死锁,杀死其中一个事务(一般持续越久的事务,保留的优先级越高)。这种防患于未然的方法不需要waits-for图,但提高了事务被杀死的比率。

2.3 意向锁

如果只有行锁,那么事务要更新一亿条记录,需要获取一亿个行锁,将占用大量的内存资源。我们知道锁是用来保护数据库内部访问对象的,这些对象根据大小可能是:属性(Attribute)、记录(Tuple)、页面(Page)、表(Table),相应的锁可分为行锁、页面锁、表锁(没人实现属性锁,对于OLTP数据库,最小的操作单元是行)。对于事务来讲,获得最少量的锁当然是最好的,比如更新一亿条记录,或许加一个表锁就足够了。

层次越高的锁(如表锁),可以有效减少对资源的占用,显著减少锁检查的次数,但会严重限制并发。层次越低的锁(如行锁),有利于并发执行,但在事务请求对象多的情况下,需要大量的锁检查。数据库系统为了解决高层次锁限制并发的问题,引入了意向(Intention)锁的概念:

• Intention-Shared (IS):表明其内部一个或多个对象被S-Lock保护,例如某表加IS,表中至少一行被S-Lock保护。

• Intention-Exclusive (IX):表明其内部一个或多个对象被X-Lock保护。例如某表加IX,表中至少一行被X-Lock保护。

• Shared+Intention-Exclusive (SIX):表明内部至少一个对象被X-Lock保护,并且自身被S-Lock保护。例如某个操作要全表扫描,并更改表中几行,可以给表加SIX。读者可以思考一下,为啥没有XIX或XIS

意向锁和普通锁的兼容关系如下所示:

意向锁的好处在于:当表加了IX,意味着表中有行正在修改。

(1)这时对表发起DDL操作,需要请求表的X锁,那么看到表持有IX就直接等待了,而不用逐个检查表内的行是否持有行锁,有效减少了检查开销。

(2)这时有别的读写事务过来,由于表加的是IX而非X,并不会阻止对行的读写请求(先在表上加IX,再去记录上加S/X),事务如果没有涉及已经加了X锁的行,则可以正常执行,增大了系统的并发度。

3.基于Timing Order(T/O)的并发控制

为每个事务分配timestamp,并以此决定事务执行顺序。当事务1的timestamp小于事务2时,数据库系统要保证事务1先于事务2执行。timestamp分配的方式包括:
(1)物理时钟;
(2)逻辑时钟;
(2)混合时钟。

3.1 Basic T/O

基于T/O的并发控制,读写不需加锁, 每行记录都标记了最后修改和读取它的事务的timestamp。当事务的timestamp小于记录的timestamp时(不能读到”未来的”数据),需要abort后重新执行。假设记录X上标记了读写两个timestamp:WTS(X)和RTS(X),事务的timestamp为TTS,可见性判断如下:

读:
• TTS < WTS(X):该对象对该事务不可见,abort事务,取一个新timestamp重新开始。
• TTS > WTS(X):该对象对事务可见,更新RTS(X) = max(TTS,RTS(X))。为了满足repeatable read,事务复制X的值。
• 为了防止读到脏数据,可以在记录上做特殊标记,读请求需等待事务提交后再去读。

写:
• TTS < WTS(X) || TTS < RTS(X):abort事务,重新开始。
• TTS > WTS(X) && TTS > RTS(X): 事务更新X,WTS(X) = TTS。

这里之所以要求TTS > RTS(X),是为了防止如下情况:读请求的时间戳为rts,已经读过X,时间戳设为RTS(X)=rts,如果新事务的TTS < RTS(X),并且更新成功,则rts读请求再来读一次就看到新的更改了,违反了repeatable read,因此这是为了避免读写冲突。记录上存储了最后的读写时间,可以保证conflict serializable

这种方式也能避免write skew,例如:初始状态,X和Y两条记录,X=-3,Y=5,X+Y >0,RTS(X)=RTS(Y)=WTS(X)=WTS(Y)=0。事务T1的时间戳为TTS1=1,事务T2的时间戳TTS2=2。

它缺陷包括:
• 长事务容易饿死,因为长事务的timestamp偏小,大概率会在执行一段时间后读到更新的数据,导致abort。

• 读操作也会产生写(写RTS)。

4.基于Validation(OCC)的并发控制

执行过程中,每个事务维护自己的写操作(Basic T/O在事务执行过程中写就将数据写入DB)和相应的RTS/WTS,提交时判断自己的更改是否和数据库中已存在的数据冲突,如果不冲突才写入DB。OCC分为三个阶段:

• Read & Write Phase:即读写阶段,事务维护读的结果和即将提交的更改,以及写入记录的RTS和WTS。

• Validation Phase:检查事务是否与数据库中的数据冲突。

• Write Phase:不冲突就写入,冲突就abort,restart。

Read & Write Phase结束,进入Validation Phase相当于事务准备完成,进入提交阶段了,进入Validation Phase的时间被选做记录行的时间戳,来定序。不用事务开始时间是因为:事务执行时间可能较长,导致后开始的事务可能先提交,这会加大事务冲突的概率,较小时间戳的事务后写入数据库,肯定会abort。

Validation过程

假设当前只有两个事务T1和T2,并修改了相同数据行,T1的时间戳 < T2的时间戳(即validation顺序:T1 < T2,对用户而言,T1先发生于T2),则有如下情况:

(1)T1在validate阶段,T2还在Read & Write Phase。此时只要T1和T2已经发生的读写没有冲突,就可以提交。

• 如果WS(T1) ∩ (RS(T2) ∪ WS(T2)) = ∅,说明T2和T1写的记录无冲突,validation通过,可以写入。

• 否则,T2与T1之间存在读写冲突或写写冲突,T1需要回滚。读写冲突:T2读到了T1写之前的版本,T1提交后,它可能读到T1写的版本,不可重复读。写写冲突:T2有可能在旧版本基础上更新,再次写入,造成T1的更新丢失。

(2)T1完成validate阶段,进入write阶段直到提交完成,这已经是不可逆的了。T2在T1进入write phase之前的读写,肯定和T1的操作不冲突(因为T1 validation通过了)。T2之后继续的读写操作,有可能冲突与T1要提交的操作,因此T2进入validate阶段:
• 如果WS(T1) ∩ RS(T2)= ∅,说明T2没读到T1写的记录,validation通过,T2可以写入。(为什么不验证WS(T2)了呢?WS(T1)已经提交了,且它的时间戳小于WS(T2),WS(T2)里之前的一部分肯定没有冲突,之后的一部分,因为没有读过T1的写入的对象,写进去也没问题,不会覆盖WS(T1)的写)

• 否则,T2与T1之间存在读写冲突和写写冲突,T2需要回滚。读写冲突:T2读到了T1写之前的版本,T1提交后,它可能读到T1写的版本,不可重复读。写写冲突:T2有可能在旧版本基础上更新,再次写入,造成T1的更新丢失。

5.基于MVCC的并发控制

数据库维护了一条记录的多个物理版本。事务写入时,创建写入数据的新版本,读请求依据事务/语句开始时的快照信息,获取当时已经存在的最新版本数据。它带来的最直接的好处是:写不阻塞读,读也不阻塞写,读请求永远不会因此冲突失败(例如单版本T/O)或者等待(例如单版本2PL)。对数据库请求来说,读请求往往多于写请求。主流的数据库几乎都采用了这项优化技术。

MVCC是读和写请求的优化技术,没有完全解决数据库并发问题,它需要与前述的几种并发控制技术组合,才能提供完整的并发控制能力。常见的并发控制技术种类包括:MV-2PL,MV-T/O和MV-OCC,它们的特点如下表:

MVCC还有两个关键点需要考虑:多版本数据的存储和多余多版本数据的回收。

多版本数据存储方式,大致可以分为两类:
(1)Append only的方式,新旧版本存储在同一个表空间,例如基于LSM-Tree的存储引擎。
(2)主表空间记录最新版本数据,前镜像记录在其它表空间或数据段,例如InnoDB的多版本信息记录在undo log。多版本数据回收又称为垃圾回收(GC),那些没有机会再被任何读请求获取的旧版本记录,应该被及时删除。

6.总结

本文依据冲突处理的时机(乐观程度),依次介绍了基于锁(在事务开始前预防冲突)、基于T/O(在事务执行中判断冲突)和基于Validation(在事务提交时验证冲突)的事务并发控制机制。不同的实现适用于不同的workload,并发冲突小的workload,当然适合更乐观的并发控制方式。而MVCC可以解决只读事务和读写事务之间相互阻塞的问题,提高了事务的并发读,被大多数主流数据库系统采用。

上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/

并发执行变成串行_一篇讲透如何理解数据库并发控制(纯干货)相关推荐

  1. 并发执行变成串行_一篇讲透如何理解数据库并发控制

    01数据库并发控制的作用 1.1 事务的概念 在介绍并发控制前,首先需要了解事务.数据库提供了增删改查等几种基础操作,用户可以灵活地组合这几种操作,实现复杂的语义.在很多场景下,用户希望一组操作可以做 ...

  2. 一篇讲透如何理解数据库并发控制

    01数据库并发控制的作用 1.1 事务的概念 在介绍并发控制前,首先需要了解事务.数据库提供了增删改查等几种基础操作,用户可以灵活地组合这几种操作,实现复杂的语义.在很多场景下,用户希望一组操作可以做 ...

  3. 并发执行变成串行_大白话Java并发面试问题之Java 8如何优化CAS性能?

    专注于Java领域优质技术,欢迎关注 来自:石杉的架构笔记 一.前言 这篇文章给大家聊一下java并发包下的CAS相关的原子操作,以及Java 8如何改进和优化CAS操作的性能. 因为Atomic系列 ...

  4. 并发执行变成串行_大神浅谈数据库并发控制 锁和 MVCC

    在学习几年编程之后,你会发现所有的问题都没有简单.快捷的解决方案,很多问题都需要权衡和妥协,而本文介绍的就是数据库在并发性能和可串行化之间做的权衡和妥协 - 并发控制机制.  如果数据库中的所有事务 ...

  5. 并发执行变成串行_网易Java研发面试官眼中的Java并发——安全性、活跃性、性能...

    一. 安全性问题 线程安全的本质是正确性,而正确性的含义是程序按照预期执行 理论上线程安全的程序,应该要避免出现可见性问题(CPU缓存).原子性问题(线程切换)和有序性问题(编译优化) 需要分析是否存 ...

  6. 优先级调度算法实现_一篇讲透嵌入式操作系统任务调度

    进互联网公司操作系统和网络库是基础技能,面试过不去的看,这里基于嵌入式操作系统分几章来总结一下任务调度.内存分配和网络协议栈的基础原理和代码实现. 处理器上电时会产生一个复位中断,接下来会执行复位中断 ...

  7. 百度排名批量查询_一篇讲透百度霸屏引流细节思路与操作玩法

    废话不多说,我们今天来聊聊百度霸屏引流这件事: 现在外面所讲的百度霸屏就这几个操作步骤,当然也就这几个步骤,再多也没什么了,简单看下哈: 第一点:选择高权重平台并注册 第二点:挖掘大量长尾词 第三点: ...

  8. 使用 JAVA 队列把高并发转为批量串行降低后端并发压力

    整体思路: 1.多用户并发请求后端接口: 2.后端会启动多个线程处理前端的请求,如果并发很大就会对后端数据库造成很大的并发压力: 3.通过 JAVA 队列把用户的并发请求转换为串行批量操作数据库: 4 ...

  9. 使用celery执行Django串行异步任务

    Django项目有一个耗时较长的update过程,希望在接到请求运行update过程的时候,Django应用仍能正常处理其他的请求,并且update过程要求不能并行,也不能漏掉任何一个请求 使用cel ...

最新文章

  1. 青少年编程竞赛交流群周报(第042周)
  2. flask-第一个flask程序
  3. [Swift]LeetCode1023. 驼峰式匹配 | Camelcase Matching
  4. Spring Bean的加载过程以及一些生命周期
  5. oracle中print,oracle中print_table存储过程实例介绍
  6. web逻辑思维题目_经典的逻辑思维训练题
  7. 计算机ae软件的入门教学视频教程,新手如何入门剪辑?3分钟教会你快速上手(附赠教程)...
  8. VMware复制ubuntu16虚拟机时提示句柄无效解决方法
  9. Android安装教程
  10. 线性回归2020年天猫双十一销量
  11. 怎么快速学会Excel?每周学习5个实用小技巧
  12. NC65 查询信用余额——客户信用联查、销售订单信用联查等
  13. npm install报错 npm ERR 的四种解决办法
  14. linux grep -a命令 grep用法
  15. 联想(Lenovo) 小新M7268W 黑白激光无线WiFi打印多功能一体机 出现:打印机故障:显示扫描单元未找到初始位置 或者 扫描单元马达故障 解决办法
  16. 我的世界联机教程java_我的世界(minecraft)联机教程
  17. iqooz1手机能搭载鸿蒙吗,iQOOZ1有NFC功能吗?iQOO Z1支持NFC刷公交卡与门禁卡吗
  18. 魔道祖师 (作者:墨香铜臭)mobi格式下载
  19. gdb arm linux 下载,月光软件站 - 编程文档 - 其他语言 - 关于ARM Linux下的GDBServer
  20. NOI模拟 五彩斑斓

热门文章

  1. Easy3D:一个轻量级、易用、高效的C++库,用于处理和渲染3D数据
  2. OpenGL如何处理多个纹理
  3. C++随时输出到文件-outfile
  4. 线性连续时间状态空间模型的离散化及实例
  5. 【从零开始的ROS四轴机械臂控制】(二) - ROS与Gazebo连接,Gazebo仿真及urdf文件修改
  6. 绘制你的世界:探索构图和真实的深度感
  7. Linux中的粘滞位
  8. 关于 线程模型中经常使用的 __sync_fetch_and_add 原子操作的性能
  9. Effective C++ 1.0 -- 概述
  10. LeetCode 1021:Remove Outermost Parentheses