文章目录
Lock事务锁
-
在了解数据库锁之前,首先就要区分开lock和latch。在数据库中,lock和latch虽然都是锁,却有着截然不同的含义。
-
latch通常被我们称为闩锁(轻量级锁),因为其要求锁定的时间必须非常短。在InnoDB中,latch可以分为mutex(互斥锁)和rwlock(读写锁),它的作用是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测机制。总之他操作的线程, 保护的是理解资源
-
lock的操作对象则是事务,用来锁定数据库中的对象,如表、页、行等,并且一半lock的对象仅在事务提交或者回滚后释放,并且lock有死锁检测机制,操作的是事务, 保护的是数据库中的数据, 本篇博客介绍的也主要是lock,下面是lock和latch的区别。
InnoDB引擎中的行锁
-
在InnoDB存储引擎中实现了下面两种标准的行级锁
-
共享锁(S Lock) :允许事务读一行数据
-
排他锁(X Lock) :允许事务删除或者更新一行数据。
-
由于共享锁并不涉及到数据的修改,所以即使一个事务已经获得了行1的共享锁,另外的事务也可以立即获得行1的共享锁,这种情况又被称为锁兼容。
-
对于排他锁又是另一种情况,由于排他锁涉及到了数据的修改,为了保证安全,其他的事务想要获得同一行的排他锁时,必须要等到前一个事务释放锁才行,这种情况又被称为锁不兼容
-
由于InnoDB支持多粒度锁定,所以允许事务可以同时存在行锁和表锁,为了支持在不同粒度上进行加锁操作,InnoDB支持一种额外的锁方式,即意向锁(Intention Lock)。意向锁即将锁定的对象分为多个层次,意味着希望事务在更细粒度上进行加锁,如下图。
-
如果我们想对下层的对象如记录上一个X锁,就需要先对粒度更粗的上层对象上锁,需要分别先对数据库、表、页上IX锁,再对记录上X锁。(防止我们对一行上了读锁之后,某个事务申请了一个表级的写锁,此时这个事务就会对我们上锁的数据进行修改,导致出现问题)
-
InnoDB支持意向锁设计比较简练,其意向锁即为表级别的锁,设计目录主要是为了在一个事务中揭示下一行将被请求的锁的类型,两种意向锁分别如下
-
意向共享锁(IS Lock),先到的事务获得了某一张表中某几行的共享锁
-
意向排他锁(IX Lock),先到的事务获得了某一张表中某几行的排他锁
-
由于InnoDB支持的是行级别的锁,因此意向锁不会阻塞除全表扫描外的任何请求,故兼容性如下。
事务中的行锁机制
- 事务的几种性质原子性,持久性,隔离性,一致性,数据库为了维护这些性质,尤其是一致性和隔离性,一般使用加锁这种方式。同时数据库又是个高并发的应用,同一时间会有大量的并发访问,如果加锁过度,会极大的降低并发处理能力。所以对于加锁的处理,可以说就是数据库对于事务处理的精髓所在。
俩段锁
- 在事务开始阶段,数据库并不知道会用到哪些数据。数据库遵循的是两段锁协议,也就是将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)
- 加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得X锁(排它锁,其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
- 解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。
- 这种方式虽然无法避免死锁.死锁的解决办法在后面会讲到,但是两段锁协议可以保证事务的并发调度是串行化(串行化很重要,尤其是在数据恢复和备份的时候)的。
行锁的三个实现算法
- 在InnoDB中由三种行锁的算法
- Record Lock:单个行记录的上锁
- Gap Lock:间歇锁,不包含记录本身的区间锁
- Next-Key Lock:包含记录本身的区间锁, 是前两个算法的结合体
- 为了更好的理解他们的区别,我拿一组数据来进行举例
有一组数据,其索引分别为10、30、60
此时使用SQL语句SELECT * FROM t WHERE id = 10 FOR UPDATE,三种锁的范围如下
Record Lock: 对10单行进行加锁
Gap Lock : (-∞, 10)、(10, 30)、(30, 60)、(60, +∞)
Next-Key Lock : (-∞,10]、(10,30]、(30,60]、(60,+∞)
对于Record Lockl来说,其总是会去锁住索引记录,即使没有设置任何一个索引,它也会使用隐式的索引进行锁定。
Next-Key Lock是结合了前面所说的两种锁算法,既锁住范围,也锁住记录本身,在InnoDB中对于行的查询都会采用这种算法,而设计它的目的正是为了解决幻读问题。
事务的四种隔离级别
- 在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别。我们的数据库行锁,也是为了构建这些隔离级别存在的。
- 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
- 提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
- 可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读
- 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞
实现原理
Read UnCommitted(读未提交)
- Read Uncommitted这种级别,数据库一般都不会用,而且任何操作都不会加锁,这里就不讨论了。
Read Committed(读提交)
- 在RC级别中,数据的读取都是不加锁的,但是数据的写入、修改和删除是需要加锁的。
- 也就是在事务中执行insert update 和delete时会加锁,但是执行select语句的时候是不会加锁的
- 那对于上锁, 在RC级别中使用的锁机制是Record Lock:单个行记录的上锁,
- mysql的innoDB引擎的行锁机制锁的是索引,不是行记录,所以如果没有使用索引,那么会使用表锁,直接锁住整张表
Repeatable Read(可重复读)
- 这是MySQL中InnoDB默认的隔离级别。
- 他主要致力于解决不可重复读问题, 就是在一个事务中执行同一条sql俩次, 但是得到的数据信息是不一样的, 就类似下面情况
- 事务A进行了一次查询id=1, 但是事务并没有提交, 事务B修改id=1的数据提交之后,事务A同样的查询,后一次和前一次的结果不一样,这就是不可重读(重新读取产生的结果不一样)。
- 产生的原因就是没有给读加锁, 在Read Committed(读提交)的隔离机制中,只对update和insert,delete加锁,而对select读操作未进行加锁,所以当A事务读取值x=100,但此时B事务对x修改为x=200,导致A事务再次读x时为200
- 在可重复读的隔离机制中,会对select,update,insert,delete加锁, 但是还是有问题,另外一个事务又在该范围内insert插入了新的记录,当之前的事务再次读取该范围时,会产生幻读(符合条件的记录增多了)
- 在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免.
- 所以当某个事务读取某个范围内的记录时,innodb存储引擎会对符合条件的记录行加上记录行之间的间隙进行上锁
- 需要使用Next-Key + gap Lock锁(行锁与间隙锁的组合锁)解决幻读问题,当InnoDB扫描索引记录的时候,会首先对选中的索引记录加上行锁,再对索引记录两边的间隙上锁,那么这样相当于只要在符合where查询的条件区间,不管是不是存在真实的记录,都会上锁,保证了当前事务,这整个区间是同步的。
- 至于为什么是Next-Key + gap Lock锁是因为,对于主键的聚集索引来说, 使用的是Next-Key Lock机制, 将查询的索引和之间的间隙都锁住, gap lock是用来锁住辅助索引, 将辅助索引的上一条数据的间隙和下一条数据的间隙也锁住
- 所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题, 不可重复读只需要给select加锁即可, 但是幻读问题必须是使用Next-Key + gap Lock锁将整个范围锁住。
- 都知道使用锁自然会降低效率, 但是InnoDB还是想办法使用MVCC多版本并发控制提高了查询的效率, 这个后面讲
丢失更新
- 丢失更新是一个事务的更新操作会被另一个事务的更新操作覆盖,比如下面案例
- 在实现同一个用户在俩台设备上转账的时候, 一台设备启动一个事务A, 事务A的任务是转账9999, 另一台设备启动事务B, 事务B任务的转账1圆, 此时俩台设置同时启动, 事务A首先会查看用户的余额, 发现是10000元, 可以进行转账, 还未时间操作数据实现转账的时候, 由于上的是共享锁所以事务B也查看用户的余额, 还是一万元, 然后事务A的转账操作执行, 然后提交事务释放了排它锁, 事务B的转账一元, 然后提交事务, 此时数据库中的余额是多少?是9999, 因为事务A的转账更新丢失了, 到时数据出现错误
- 要避免更新丢失的发生, 就必须让读与加一个排他锁, 或者最安全的就是让事务的执行完全串行化, 也就是让隔离级别达到串行化Serializable级别
Serializable (串行化)
- 使用的悲观锁的理论给读和写都加表级别锁,实现简单,数据更加安全,但是并发能力非常差。
自增长锁
- 自增长在数据库中很常见, 是主键的首选方式, 在InnoDB引擎中, 对每一个自增长的表都有一个自增长计数器, 当对自增长列进行插入操作的时候, 就会将这个自增长值进行加一, 但是在高并发下, 这个加一操作并不是安全的, 所以InnoDB也对其进行了上锁, 这个锁与X锁和S锁不一样, 不是在事务提交后进行释放,而是当一条插入sql执行完毕后就会释放
死锁
死锁指的是两个或者两个以上的事务在执行过程中,因为争抢所资源而导致的一种互相等待的现象。在死锁的情况下,如果没有外力作用,事务将永远无法推进下去。
在数据库中通常都会使用超时机制来解决死锁。即为事务设置超时时间,即使两个事务互相等待,当其中一方超时后立刻进行回滚,另一个事务就能够继续进行了。
- 虽然超时机制可以解决这个问题,但是我们并不能掌握回滚的事务的量级,倘若事务更新庞大,则回滚就会带来大量的性能损耗,所以我们通常会采用更加主动的策略,即使用等待图来进行死锁检测
等待图主要有俩个信息链表, 一个是锁的信息链表, 一个事务等待链表
-
由图可以发现, 在事务等待链表中有4个事务, 分别是t1~t4, 在row1行中的锁的信息链表表示t2事务上了X锁, t1事务上了S锁, 由上面俩个信息链表就可以构建出等待图
图中每个节点即为一个事务
每条指向其他节点的线则代表着正在等待该节点的资源
当存在回路时,则代表着事务互相等待,此时就意味着存在死锁 -
每当事务请求锁并发生等待时,都会主动判断等待图中是否存在回路,如果存在则代表着有死锁产生,此时就会主动选择undo量最小的事务来打破死锁。在现版本的InnoDB中,通常采用深度优先搜索(老版本使用递归)来检测死锁的存在。发现有死锁就会查看当前事务的版本然后在undo.log中进行查找那个事务的回滚数据量小就会回滚哪个事务