一、并发控制定义

在数据库中,并发控制是指在多个用户/进程/线程同时对数据库进行操作时,保证事务的一致性和隔离性,同时最大程度地并发。

并发控制的目的是保证一个用户的工作不会对另一个用户的工作产生不合理的影响。

在某些情况下,这些措施保证了当用户和其他用户一起操作时,所得的结果和她单独操作时的结果是一样的。

二、并发中存在的冲突状况

1、读-读

不存在任何问题

2、读-写

有隔离性问题,可能遇到脏读,不可重复读等。

脏读:(读未提交)

事务 T1修改某一数据,并将其写回磁盘,事务T2读取同一数据后, T1由于某种原因被撤消,这时 T1已修改过的数据恢复原值, T2读到的数据就与数据库中的数据不一致,则T2读到的数据就为"脏"数据,即不正确的数据。(读取到未提交数据)

不可重复读:(读已提交)

事务T1读取数据后,事务T2执行更新操作,使T1无法再现前一次读取结果。

(包括针对某一数据项和针对一批数据(幻读))

解决办法:

多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是系统为每个事务分配单向增长的时间戳。

为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。

这样在读操作不用阻塞写操作,写操作不用阻塞读操作的同时,避免了脏读和不可重复读。

3、写-写

可能遇到丢失修改。

丢失修改:两个事务T1和T2读入同一数据并修改,T2提交的结果破坏了T1提交的结果,导致T1的修改被丢失。

解决办法:

一种办法是是锁,即基于锁的并发控制,比如2PL,这种方式开销比较高,而且无法避免死锁。

乐观并发控制(OCC)是一种用来解决写-写冲突的无锁并发控制,认为事务间争用没有那么多,所以先进行修改,在提交事务前,检查一下事务开始后,有没有新提交改变,如果没有就提交,如果有就放弃并重试。乐观并发控制类似自选锁。乐观并发控制适用于低数据争用,写冲突比较少的环境。

多版本并发控制可以结合基于锁的并发控制来解决写-写冲突,即MVCC+2PL,也可以结合乐观并发控制来解决写-写冲突。

三、并发控制解决办法

并发控制的主要技术有封锁(locking)、时间戳(timestamp)、乐观控制法(optimistic sheduler)和多版本并发控制(multi-version concurrency control,MVCC)等,各个技术之间是存在交叉配合使用的。商用的DBMS一般都采用封锁方法。

悲观并发控制:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。 
乐观并发控制:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。

(一)封锁:

事务T在对某个数据对象(例如表、记录等)操作之前,先向系统发出请求,对其加锁。加锁后事务T就对该数据对象有了一定的控制权,在事务T释放它的锁之前,其它的事务不能更新此数据对象,属于一种悲观控制法

1.1 封锁的基本类型

基本的封锁类型有两种:排他锁(X锁)和共享锁(S锁)。

(1)排它锁又称写锁X。若事务T对数据对象A加上X锁,只有事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。

(2)共享锁又称读锁S。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

1.2 封锁协议

一级封锁协议:对应事务隔离级别中的读未提交(Read uncommited)

事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。

一级封锁协议可防止丢失修改,并保证事务T是可恢复的,因为防止其他事务进行同时修改。

在一级封锁协议中,如果仅仅是读数据不对其进行修改,是不需要加锁的。所以它不能保证可重复读和不 读"脏"数据。(暂时举例不出脏读的情况!!!!对下图脏读情况的疑问:ROLLBACK才代表事务的结束,那为什么在ROLLBACK前X锁就解开了)


二级封锁协议:对应事务隔离级别中的读已提交(Read Committed)

一级封锁协议加上事务T在读取数据R之前必须先对其加S锁,读完后即可释放S锁。二级封锁协议除防止了丢失修改和读"脏"数据。 

遵从二级封锁协议时发生的“不可重复读”的过程


 三级封锁协议:对应事务隔离级别中的可重复读(Repeatable Read)

一级封锁协议加上事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放。三级封锁协议除防止了丢失修改和不读'脏'数据外,还进一步防止了不可重复读。

1.3 封锁中的死锁与活锁

活锁

存在现象:如果事务T1封锁了数据R,事务T2又请求封锁数据R,于是T2等待。事务T3也请求封锁R,当事务T1释放了数据R上的封锁之后系统首先批准了事务T3的封锁请求,T2仍然等待。然后T4又申请封锁R,当T3释放了R的封锁之后系统又批准了T4的封锁请求。T2有可能一直等待下去,这就是活锁。

解决办法:避免活锁的方法就是先来先服务的策略。当多个事务请求对同一数据对象封锁时,封锁子系统按照请求的先后对事务排队。数据对象上的锁一旦释放就批准申请队列中的第一个事务获得锁。

死锁

存在现象:如果事务T1封锁了数据R1,事务T2封锁了数据R2,然后T1又请求封锁数据R2,因为T2已经封锁了数据R2,于是T1等待T2释放R2上的锁。接着T2又申请封锁R1,因为因为T1已经封锁了数据R1,T2也只能等待T1释放R1上的锁。这样就出现了T1在等待T2,T2也在等待T1的局面,T1和T2两个事务永远不能结束,形成死锁。

解决办法

1) 死锁的预防

①一次封锁法

一次封锁法要求事务必须一次将所有要使用的数据全部加锁,否则不能继续执行。例如上图中的事务T1将数据R1和R2一次加锁,T1就能执行下去,而T2等待。T1执行完成之后释放R1,R2上的锁,T2继续执行。这样就不会产生死锁。

一次封锁法虽然能防止死锁的发生,但是缺点却很明显。一次性将以后要用到的数据加锁,势必扩大了封锁的范围 ,从而降低了系统的并发度。

②顺序封锁法

顺序封锁法是预先对数据对象规定一个封锁顺序,所有的事务都按照这个顺序实行封锁。

顺序封锁法虽然可以有效避免死锁,但是问题也很明显。第一,数据库系统封锁的数据对象极多,并且随着数据的插入、删除等操作不断变化,要维护这样的资源的封锁顺序非常困难,成本很高。第二,事务的封锁请求可以随着事务的执行动态的确定,因此很难按照规定的顺序实行封锁。

可见,预防死锁的产生并不是很适合数据库的特点,所以在解决死锁的问题上普遍采用的是诊断并且解除死锁。

2) 死锁的诊断与解除

①超时法

如果一个事务的等待时间超过了默认的时间,就认为是产生了死锁。

②等待图法

一旦检测到系统中存在死锁就要设法解除。通常的解决方法是选择一个处理死锁代价最小的事务,将其撤销,释放此事务持有的所有的锁,恢复其所执行的数据修改操作,使得其他事务得以运行下去。

1.4 两段锁协议2PL

所谓的二段锁协议是指所有事务必须分两个阶段对数据进行加锁和解锁操作。

  • 在对任何数据进行读、写操作之前,首先要申请并获得该数据的封锁。

  • 在释放一个封锁之后,事务不在申请和获得其他封锁。

也就是说事务分为两个阶段。(事务只有一次申请锁的机会)

  • 第一个阶段是获得封锁,也称为扩展阶段。在这个阶段,事务可以申请获得任何数据项任何类型的锁,但是不能释放任何锁。
  • 第二阶段是释放封锁,也称为收缩阶段。在这个阶段,事务可以释放任何数据项上任何类型的封锁,但是不能再申请任何锁。

事务遵守两段锁协议是可串行化调度的充分条件,而不是必要条件。也就是说遵守两段锁协议一定是可串行化调度的,而可串行化调度的不一定是遵守两段锁协议的。

左侧T1、T2遵循两段锁协议,右侧T1、T2并不遵循两段锁协议

两段锁协议和一次封锁法的异同

一次封锁法要求事务必须将要使用的数据全部加锁,否则不能继续执行。因此一次封锁法遵守两段锁协议。

但是两段锁协议并不要求事务将要使用的数据一次全部加锁,因此两段锁协议可能发生死锁。如图:

(二)时间戳

给每个事务分配一个全局惟一的时间戳。时间截的值产生了一个精确的顺序,事务按照该顺序提交。

时间戳必须有两个特性:惟一性和单调性,惟一性保证不存在相等的时间戳值,单调性保证时间戳的值是一直增长的。

同一事务中所有的数据库操作(读和写)都必须有相同的时间戳。

DBMS按照时间戳顺序执行冲突的事务,因此保证了事务的可串行化。如果两个事务冲突,通常终止其中一个,将其回滚并重新调度,赋予新的时间戳。

用时间戳实现并发控制,需要为数据库中每个值附加两个字段

读时间戳:用于保存所有访问该记录的事务中的最大时间戳(最后读取时间)

写时间戳:用于保存将记录改到当前值的事务的时间戳(最后修改时间)

因此时间戳增加了内存需求和数据库的处理开销,因为有可能导致许多事务被终止,重新调度和重新赋予时间戳,时间戳方法一般需要大量的系统资源.

这样的事务在并行执行时,用的是乐观控制,先任由事务对数据进行修改,在写回去的时候在判断记录的时间戳有没有修改,如果没有被修改,就写入,否则,就生成一个新的时间戳并再次尝试更新数据。

(三)乐观控制法

乐观方法基于这样的假设,数据库操作的大部分都不会发生冲突,乐观方法不要求锁定,作为替换,事务不受限制地被执行,直到它被提交,便用乐观方法,每个事务经过两个或者三个阶段,它们是读、确认、写。

(1)读阶段,事务读取数据库,执行需要的计算,并对一个私有的数据库值的副本进行更新,事务的所有更新操作都记录在一个临时更新文件中,该文件将不 ,会被剩下的其他事务访问.

(2)确认阶段,对事务进行确认以保证所做的修改不会影响数据库的完整性和一致性,如果确认检查是肯定的,事务进入写阶段;如果确认检查是否定的,则事务回滚,重新启动,所做的修改被抛弃

(3)写阶段,所做的修改被永久地写入到数据库中。

乐观方法对于大多数只有较少更新事务的查询数据库系统来说是可以接受的,

(四)多版本并发控制

MVCC (Multiversion Concurrency Control),即多版本并发控制技术,是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。

在MVCC中,每个事务操作的是数据的一个快照,写操作造成的变化在写操作完成之前(或者数据库事务提交之前)对于其他的事务来说是不可见的。

当一个 MVCC 数据库需要更一个一条数据记录的时候,它不会直接用新数据覆盖旧数据,而是将旧数据标记为过时(obsolete)并在别处增加新版本的数据。这样就会有存储多个版本的数据,但是只有一个是最新的。

这种方式允许事务读取在他读之前已经存在的数据,即使这些在读的过程中半路被别人修改、删除了,也对先前正在读的用户没有影响。这种多版本的方式避免了填充删除操作在内存和磁盘存储结构造成的空洞的开销,但是需要系统周期性整理(sweep through)以真实删除老的、过时的数据。

MVCC 并发控制下的读事务一般使用时间戳或者事务ID去标记当前读的数据库的状态(版本),读取这个版本的数据。读、写事务相互隔离,不需要加锁。

读写并存的时候,写操作会根据目前数据库的状态,创建一个新版本,并发的读则依旧访问旧版本的数据。

一句话讲,MVCC就是用 同一份数据临时保留多版本的方式 的方式,实现并发控制。

这里留意到 MVCC 关键的两个点:

  1. 在读写并发的过程中如何实现多版本;
  2. 在读写并发之后,如何实现旧版本的删除(毕竟很多时候只需要一份最新版的数据就够了);

4.1 MVCC的实现(还没看懂)

MVCC 使用时间戳(TS)、递增的事务 ID(T)实现事务一致性。

MVCC 通过维护多版本数据,保证一个读事务永远不会被阻塞。对象 P 维护有多个版本,每个版本会有一个读时间戳(Read TimeStamp, RTS)和 写时间戳(Write TimeStamp, WTS),事务 Ti 读对象 P 的最新版本,该版本早于事务 Ti 的读时间戳 RTS(Ti)。

事务 Ti 要对 P 执行写操作,如果有其他事务 Tk 同时对 P 操作,则 RTS(Ti)必须要早于 RTS(Tk),即有 RTS(Ti) < RTS(Tk),这样对 Ti 对 P 的写操作才能完成。一般地,如果其他事务拥有 P 的一个更早的读时间戳的情况下,写操作是不能完成的。打个比方就是在存储前面有一道线,只有等你前面的人的完成了他们的事务,你的修改事务才可以提交完成。

重复说一下:每个对象 P 有一个时间戳 TS,如果事务 Ti 想要对 P 执行写操作,(写要先读)事务的读时间戳是 RTS(Ti),如果有其他事务拥有一个比较早的时间戳,有 TS(P) < RTS(Ti),这时事务 Ti 会退出并重新开始。否则,事务 Ti 创建一个 P 的新版本,并设置新版本 P 的时间戳,似的 TS = TS(Ti)。

MVCC 系统明显的缺点是会存储多个版本数据的冗余开销。但同时,读操作永不会被阻塞,这对那些以读操作为主的数据库来说非常重要。MVCC 实现了真的快照隔离(snapshot isolation),然后其他的并发控制方法要么是不完整的快照隔离方式,要么需要较高的性能损耗。

4.2 示例(还没看懂)

Time Object1 Object2
0 "Foo" by T0 "Bar" by T0
1 "Hello" by T1  

Time=1的时候数据库的状态如上:

T0 写 Object1 为 "Foo",写 Object2 为 "Bar";之后 T1 写 Object1 为 "Hello",保留 Object2 为原始值。 Object1 的新值将取代 Time=0 时刻的旧值,并提供给 T1提交之后的发生的所有事务。Object1的版本号为0的旧数据会被 GC 掉。

如果有一个长事务 T2,在 T1之后对 Object1和 Object2 进行读操作,同时并行地,有事务 T3 做更新:删除 Object2、增加 Object3="Foo-Bar",在 Time=2 数据的状态如下所示:

Time Object1 Object2 Object3
0 "Foo" by T0 "Bar" by T0  
1 "Hello" by T1    
2   (delete)by T3 "Foo-Bar" by T3

在 Time=2 Object2有一个新版本:标记删除,同时增加了新对象 Object3 。T2 和 T3 并发执行,T2 看到的是数据在 Time=2 且 T3提交前的版本,这样 T2读到了 Object2="Bar""Object1="Hello"

以上就是 MVCC 在不加锁的情况下实现的快照隔离的读的原理。

4.3 innodb中的MVCC

下面转自:【mysql】关于innodb中MVCC的一些理解

innodb MVCC主要是为Repeatable-Read事务隔离级别做的。在此隔离级别下,A、B客户端所示的数据相互隔离,互相更新不可见

了解innodb的行结构、Read-View的结构对于理解innodb mvcc的实现由重要意义

innodb存储的最基本row中包含一些额外的存储信息 DATA_TRX_ID,DATA_ROLL_PTR,DB_ROW_ID,DELETE BIT

  • 6字节的DATA_TRX_ID 标记了最新更新这条行记录的transaction id,每处理一个事务,其值自动+1

  • 7字节的DATA_ROLL_PTR 指向当前记录项的rollback segment的undo log记录,找之前版本的数据就是通过这个指针

  • 6字节的DB_ROW_ID,当由innodb自动产生聚集索引时,聚集索引包括这个DB_ROW_ID的值,否则聚集索引中不包括这个值.,这个用于索引当中

  • DELETE BIT位用于标识该记录是否被删除,这里的不是真正的删除数据,而是标志出来的删除。真正意义的删除是在commit的时候

具体的执行过程

begin->用排他锁锁定该行->记录redo log->记录undo log->修改当前行的值,写事务编号,回滚指针指向undo log中的修改前的行

上述过程确切地说是描述了UPDATE的事务过程,其实undo log分insert和update undo log,因为insert时,原始的数据并不存在,所以回滚时把insert undo log丢弃即可,而update undo log则必须遵守上述过程

下面分别以select、delete、 insert、 update语句来说明

SELECT

Innodb检查每行数据,确保他们符合两个标准:

1、该行的创建版本号小于等于当前事务版本号,这确保当前事务读取的行都是事务之前已经存在的,或者是由当前事务创建或修改的行

2、该行的删除版本号大于当前版本或者为空,确定了当前事务开始之前,行没有被删除

符合了以上两点则返回查询结果。

INSERT

InnoDB为每个新增行记录当前系统版本号作为创建ID。

DELETE

InnoDB为每个删除行的记录当前系统版本号作为行的删除ID。

UPDATE

InnoDB复制了一行,不执行原地update,而是转换成insert + delete,系统版本号作为新行的创建版本,把系统版本号作为了旧行的删除版本。

其中,写操作(insert、delete和update)执行时,需要将系统版本号递增。

由于旧数据并不真正的删除,所以必须对这些数据进行清理,innodb会开启一个后台线程执行清理工作,具体的规则是将删除版本号小于当前系统版本的行删除,这个过程叫做purge。

通过MVCC很好的实现了事务的隔离性,可以达到repeated read级别,要实现serializable还必须加锁。

说明

insert操作时 “创建时间”=DB_ROW_ID,这时,“删除时间 ”是未定义的;

update时,复制新增行的“创建时间”=DB_ROW_ID,删除时间未定义,旧数据行“创建时间”不变,删除时间=该事务的DB_ROW_ID;

delete操作,相应数据行的“创建时间”不变,删除时间=该事务的DB_ROW_ID;

select操作对两者都不修改,只读相应的数据

innodb中的MVCC的总结

上述更新前建立undo log,根据各种策略读取时非阻塞就是MVCC,undo log中的行就是MVCC中的多版本,这个可能与我们所理解的MVCC有较大的出入,一般我们认为MVCC有下面几个特点:

  • 每行数据都存在一个版本,每次数据更新时都更新该版本
  • 修改时Copy出当前版本随意修改,各个事务之间无干扰
  • 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)

就是每行都有版本号,保存时根据版本号决定是否成功,听起来含有乐观锁的味道,而Innodb的实现方式是:

  • 事务以排他锁的形式修改原始数据
  • 把修改前的数据存放于undo log,通过回滚指针与主数据关联
  • 修改成功(commit)啥都不做,失败则恢复undo log中的数据(rollback)

二者最本质的区别是,当修改数据时是否要排他锁定,如果锁定了还算不算是MVCC?

Innodb的实现真算不上MVCC,因为并没有实现核心的多版本共存,undo log中的内容只是串行化的结果,记录了多个事务的过程,不属于多版本共存。但理想的MVCC是难以实现的,当事务仅修改一行记录使用理想的MVCC模式是没有问题的,可以通过比较版本号进行回滚;但当事务影响到多行数据时,理想的MVCC据无能为力了。

比如,如果Transaciton1执行理想的MVCC,修改Row1成功,而修改Row2失败,此时需要回滚Row1,但因为Row1没有被锁定,其数据可能又被Transaction2所修改,如果此时回滚Row1的内容,则会破坏Transaction2的修改结果,导致Transaction2违反ACID。

理想MVCC难以实现的根本原因在于企图通过乐观锁代替二段提交。修改两行数据,但为了保证其一致性,与修改两个分布式系统中的数据并无区别,而二提交是目前这种场景保证一致性的唯一手段。二段提交的本质是锁定,乐观锁的本质是消除锁定,二者矛盾,故理想的MVCC难以真正在实际中被应用,Innodb只是借了MVCC这个名字,提供了读的非阻塞而已。

https://blog.csdn.net/qq_28328381/article/details/82787709

并发控制中存在问题及解决方案相关推荐

  1. 计算机视觉:Bag of words算法实现过程中出现错误及解决方案

    Bag of words算法实现过程中出现错误及解决方案 出现的问题 IndexError: list index out of range OSError:x.sift not found sqli ...

  2. VS2013中使用git发布解决方案master分支的时候出现错误

    VS2013中使用git发布解决方案master分支的时候出现错误 参考文章: (1)VS2013中使用git发布解决方案master分支的时候出现错误 (2)https://www.cnblogs. ...

  3. QT 中 界面中消息的停留时间解决方案 以及 label 中字体大小和换行设置

    QT 中 界面中消息的停留时间解决方案 以及 label 中字体大小和换行设置 参考文章: (1)QT 中 界面中消息的停留时间解决方案 以及 label 中字体大小和换行设置 (2)https:// ...

  4. RabbitMQ系列之【启动过程中遇到问题及解决方案】

    RabbitMQ系列之[启动过程中遇到问题及解决方案] 参考文章: (1)RabbitMQ系列之[启动过程中遇到问题及解决方案] (2)https://www.cnblogs.com/feixiabl ...

  5. 如何修改wampserver中mysql中字符编码的解决方案

    如何修改wampserver中mysql中字符编码的解决方案 参考文章: (1)如何修改wampserver中mysql中字符编码的解决方案 (2)https://www.cnblogs.com/wa ...

  6. Redis的中并发问题的解决方案小结

    Redis的中并发问题的解决方案小结 参考文章: (1)Redis的中并发问题的解决方案小结 (2)https://www.cnblogs.com/lys_013/p/10207609.html (3 ...

  7. iOS开发之#iPhone6与iPhone6Plus适配#Xcode6.0/Xcode6.1上传应用过程中一些变动以及#解决方案#

    iOS开发之#iPhone6与iPhone6Plus适配#Xcode6.0/Xcode6.1上传应用过程中一些变动以及#解决方案# 参考文章: (1)iOS开发之#iPhone6与iPhone6Plu ...

  8. 自动化测试中自动化切换网络----解决方案

    自动化测试中自动化切换网络----解决方案 思路 通过安装一个控制网络的APP,在测试脚本中,通过shell命令来控制实现 使用方式 关闭WIFI,切换到4G网络(测试手机默认流量是开着的) adb ...

  9. thinkphp5部署于Linux中nginx多站点解决方案

    2019独角兽企业重金招聘Python工程师标准>>> 问题简述:thinkphp5怎么在url访问中去掉public. 实验环境: Linux.nginx.mysql.php(ln ...

  10. 另一个SqlParameterCollection 中已包含 SqlParameter[解决方案]

    另一个SqlParameterCollection 中已包含 SqlParameter[解决方案] 参考文章: (1)另一个SqlParameterCollection 中已包含 SqlParamet ...

最新文章

  1. pjax php,ZBlogPHP简单实现pjax的一种方法
  2. 在Ubuntu上源码安装MySQL+安装问题解决+安全优化
  3. echart(2),模拟数据导入篇
  4. 第七章 shell学习之退出、测试、判断及操作
  5. ROS探索总结(十)(十一)(十二)——语音控制 机器视觉 坐标系统
  6. KOL:Key Opinion Leader
  7. 前端学习(3120):react-hello-react的setstate的使用
  8. 【kafka】已解决 kafka No current assignment for partition
  9. 北理在线作业答案c语言,北理乐学C语言答案,最新.doc
  10. Firefox浏览器购物比价插件:惠惠购物助手
  11. weiPe系统启动盘还原
  12. 外链建设的22种方法
  13. 自定义circleindicator
  14. pytorch训练Class-Balanced Loss
  15. 【题解】【循环】幂级数求和
  16. 关于Twinmotion可视化渲染软件初识
  17. Essential C++读书笔记
  18. 递归实现在级联选择器中选择部门下的人员
  19. GaussDB表设计最佳实践
  20. 爱思助手不能通过wifi连接的方法

热门文章

  1. 管理例程——数据库管理员
  2. Windows Phone 7开发人员向导已经发布
  3. 高并发 问题怎么解决
  4. spring源码学习(1)- bean
  5. Android7.0无需FileProvide搞定URI拍照、应用安装问题
  6. Unity中一个安卓设备拆装项目,从使用到放弃ab包过程记录
  7. CVPR2013感兴趣的文章整理
  8. eclipse关闭mysql数据库_Eclipse 连接 Mysql 数据库操作总结
  9. 拓端tecdat|R语言用igraph绘制网络图可视化
  10. 拓端tecdat|数据观察“双十一”网购新常态