mysql(二)事务 & 事务的实现 & 锁

参考文档:

mvcc:https://www.cnblogs.com/chenpingzhao/p/5065316.html

事务的实现:https://www.linuxidc.com/Linux/2018-01/150610.htm

MySQL-InnoDB-MVCC多版本并发控制

事务是指一组逻辑操作单元,使数据从一种状态变换到另一种状态,对数据库的增删改查都是事务操作

事务的4个特性(ACID):

原子性(Atomic):整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样
一致性(Consistency):应用系统从一个正确的状态到另一个正确的状态.而ACID就是说事务能够通过AID来保证这个C的过程.C是目的,AID都是手段.
隔离性(Isolation):隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行 相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请 求,使得在同一时间仅有一个请求用于同一数据
持久性(Durabiliy):当事务处理成功执行到结束的时候,其效果在数据库中被永久纪录下来

对于同时运行的多个事务, 当这些事务访问数据库中相同的数据时, 如果没有采取必要的隔离机制, 就会导致各种并发问题: 

脏读: 对于两个事务 T1, T2, T1 读取了已经被 T2 更新但还没有被提交的字段. 之后, 若 T2 回滚, T1读取的内容就是临时且无效的.
不可重复读: 对于两个事务T1, T2, T1 读取了一个字段, 然后 T2 更新了该字段. 之后, T1再次读取同一个字段, 值就不同了. (主要提现在修改上)
幻读: 对于两个事务 T1, T2, T1 从一个表中读取了一个字段, 然后 T2 在该表中插入了一些新的行. 之后, 如果 T1 再次读取同一个表, 就会多出几行.(主要提现在增加和删除上)

隔离级别

数据库事务的隔离级别有4个,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题

√: 可能出现    ×: 不会出现

  脏读 不可重复读 幻读
Read uncommitted未提交读
Read committed已提交读 ×
Repeatable read可重复读 × ×
Serializable可串行化 × × ×

Record Locks(记录锁):在索引记录上加锁。
Gap Locks(间隙锁):在索引记录之间加锁,或者在第一个索引记录之前加锁,或者在最后一个索引记录之后加锁。
Next-Key Locks:在索引记录上加锁,并且在索引记录之前的间隙加锁。它相当于是Record Locks与Gap Locks的一个结合。
假设一个索引包含以下几个值:10,11,13,20。那么这个索引的next-key锁将会覆盖以下区间:
(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)
多版本机制
锁是针对集中式数据管理设计的,缺点是降低了事务的并发,并且锁本身有开销。在分布式系统,尤其是读多写少的系统中,采用多版本机制更合适。每个数据项都有多个副本,每个副本都有一个时间戳,根据多版本并发控制协议(MVCC)维护各个版本
MVCC又称为乐观锁,它在读取数据项时,不加锁;在更新数据项时,直到最后要提交时,才会加锁。这与CAS(Compare and Swap)的机制很类似,为了提高并发度,它更新数据前,会将数据拷贝一份,进行一系列修改,并且拷贝的同时,会记录当前的版本号(时间戳),当修改完毕,即将提交时,再检查此时的版本号是否与刚才记录的一致,如果不一致,则表明数据项被其他事务修改,当前事务的修改被取消。否则,正式提交修改,并增加版本号
基于锁的并发控制机制称为悲观锁,因为它认为其他事务修改自己正在使用的数据项的概率很高,因此对数据项加锁以阻塞其他事务的读和写
事务的实现
一致性锁定读
  在一个事务中,标准的SELECT语句是不会加锁,但是有两种情况例外。SELECT ... LOCK IN SHARE MODE 和 SELECT ... FOR UPDATE。
  SELECT ... LOCK IN SHARE MODE
  给记录假设共享锁,这样一来的话,其它事务只能读不能修改,直到当前事务提交
  SELECT ... FOR UPDATE
  给索引记录加锁,这种情况下跟UPDATE的加锁情况是一样的
一致性非锁定读
  consistent read (一致性读),InnoDB用多版本来提供查询数据库在某个时间点的快照。如果隔离级别是REPEATABLE READ,那么在同一个事务中的所有一致性读都读的是事务中第一个这样的读读到的快照;如果是READ COMMITTED,那么一个事务中的每一个一致性读都会读到它自己刷新的快照版本。Consistent read(一致性读)是READ COMMITTED和REPEATABLE READ隔离级别下普通SELECT语句默认的模式。一致性读不会给它所访问的表加任何形式的锁,因此其它事务可以同时并发的修改它们
在默认的隔离级别中,普通的SELECT用的是一致性读不加锁。而对于锁定读、UPDATE和DELETE,则需要加锁,至于加什么锁视情况而定。如果你对一个唯一索引使用了唯一的检索条件,那么只需锁定索引记录即可;如果你没有使用唯一索引作为检索条件,或者用到了索引范围扫描,那么将会使用间隙锁或者next-key锁以此来阻塞其它会话向这个范围内的间隙插入数据
作者曾经有一个误区,认为按照前面说MVCC下的增删查改的行为就不会出现任何问题,也不会出现不可重复读和幻读。但其实是大错特错。
举个很简单的例子,假设事务A更新表中id=1。的记录,而事务B也更新这条记录,并且B先提交,如果按照前面MVVC说的,事务A读取id=1的快照版本,那么它看不到B所提交的修改,此时如果直接更新的话就会覆盖B之前的修改,这就不对了,可能B和A修改的不是一个字段,但是这样一来,B的修改就丢失了,这是不允许的所以,在修改的时候一定不是快照读,而是当前读而且,前面也讲过只有普通的SELECT才是快照读,其它诸如UPDATE、删除都是当前读。修改的时候加锁这是必然的,同时为了防止幻读的出现还需要加间隙锁
1、利用MVCC实现一致性非锁定读,这就有保证在同一个事务中多次读取相同的数据返回的结果是一样的,解决了不可重复读的问题
2、利用Gap Locks和Next-Key可以阻止其它事务在锁定区间内插入数据,因此解决了幻读问题
综上所述,默认隔离级别的实现依赖于MVCC和锁,再具体一点是一致性读和锁
事务的加锁方式:
一次性锁协议
事务开始时,即一次性申请所有的锁,之后不会再申请任何锁,如果其中某个锁不可用,则整个申请就不成功,事务就不会执行,在事务尾端,一次性释放所有的锁。
一次性锁协议不会产生死锁的问题,但事务的并发度不高
两阶段锁协议
整个事务分为两个阶段,前一个阶段为加锁,后一个阶段为解锁。在加锁阶段,事务只能加锁,也可以操作数据,但不能解锁,直到事务释放第一个锁,就进入解锁阶段,此过程中事务只能解锁,也可以操作数据,不能再加锁
两阶段锁协议使得事务具有较高的并发度,因为解锁不必发生在事务结尾。它的不足是没有解决死锁的问题,因为它在加锁阶段没有顺序要求。如两个事务分别申请了A, B锁,接着又申请对方的锁,此时进入死锁状态
树形协议
假设数据项的集合满足一个偏序关系,访问数据项必须按此偏序关系的先后进行。如di->dj,则要想访问dj,必须先访问di。这种偏序关系导出一个有向无环图(DAG),因此称为树形协议。树形协议的规则有:
树形协议只有独占锁;
事务T第一次加锁可以对任何数据项进行;
此后,事务T对数据项Q的加锁前提是持有Q的父亲数据项的锁;
对数据项的解锁可以随时进行;
数据项被事务T加锁并解锁之后,就不能再被事务T加锁
树形协议的优点是并发度好,因为可以较早地解锁。并且没有死锁,因为其加锁都是顺序进行的
缺点是对不需要访问的数据进行不必要的加锁
时间戳排序协议
每个事务都有一个唯一的时间戳,也就是其进入系统的时间。时间戳有大小之分,如果事务Ti比Tj先进入系统,则TS(Ti)<TS(Tj)。对于每个数据项Q,有两个时间戳与其绑定:一个是W-TS(Q),表示最近一次写数据项Q的事务的时间戳;一个是R-TS(Q),表示最近一次读数据项Q的事务的时间戳。Thomas协议是对时间戳排序协议的改进,具体内容如下:
若事务Ti发起一个write(Q),则
如果TS(Ti)<R-TS(Q),则表明Ti准备写的值还没来得及写入,Q就提前被读取了,所以Ti的write(Q)操作被拒绝,并且事务Ti被回滚
如果TS(Ti)<W-TS(Q),表明Ti写的值已过期,比它更新的值已经写到Q上,所以Ti的write(Q)操作被拒绝
剩下的情况,write(Q)操作被允许
原文地址:https://www.cnblogs.com/amei0/p/4512573.html