数据库——锁的一点事
仅作为笔记,码字不易,转载请标明出处。
文章目录
前言
仅作为笔记
一、什么是锁
- 关于锁的具体概念不再赘述,和高并发里面的定义是一样的,数据库使用锁是数据库区别于文件系统的关键特性。
- InnoDB存储引擎会在行级别上加锁,当然也会在数据库的其他多个地方加锁,数据库使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。
二、lock与latch
- latch:一般称为闩锁,一种轻量级锁,要求锁定资源的时间非常短,在InnoDB中,latch可分为mutex(互斥量)和rwlock(读写锁),并且没有死锁检测机制。
- lock:服务对象是事务,锁定的内容是数据库中的对象,如表、页、行。一般lock的对象仅在事务commit或者rollback后释放(不同事务隔离级别释放的时间可能不同)。有死锁机制。
latch和lock具体差别看下图:
三、InnoDB存储引擎中的锁
3.1 锁的类型
- 共享锁和排他锁两个行级锁(读写锁):
1)共享锁,简写为S,也就是读锁,与写锁和意向写锁互斥。
2)排他锁,简写为X,也就是写锁,与任何锁互斥。
缺点:这两个都是行级锁,意味着在事务中,如果要对其中一个加锁需要判断这个行有没有加锁,这需要遍历整个表,很浪费时间。 - 意向锁:意向锁是可以解决上述的读写锁的缺点。
1)意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁,只与写锁(排他锁)互斥。锁住的是表。
2)意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁,只与读写锁(S,X)互斥。锁住的是表。
有了意向锁可以避免遍历全表看是否所要加锁的行被加锁没有,只需要检擦表就可以了。这四种表的兼容性如下图:
- 悲观锁和乐观锁:定义方式和高并发内容一样,不再赘述。简述数据库中的实现方式。
1)悲观锁:在数据库中依靠的是数据库的锁机制。
2)乐观锁:在数据库中依靠的是版本号,实际上就是CAS理论。
3.2 一致性非锁定读
-
概念:通过行多版本控制,使用undo段完成,一致性非锁定读的意思就是,要读取的数据就算是加了X锁,也是可以读取的(不等待锁释放),只不过读取的是这个行数据的以前的版本(也就是快照),在不同的隔离模式下读取的数据版本可能不一样。 如下图是读取时遇到锁的操作:
-
不同隔离级别下的非锁定读:
1)READ COMMITTED(RC)模式:该模式下读取的快照是最近版本的,该方式违背了ACID中的I的特性。论其原因,该模式是只能读取到事务提交以后的数据的,意味着他能读到的数据不可能是事务修改成功的第一版数据的事务开始数据。
2)REPEATABLE READ(RR)模式:该模式下读取的快照版本是事务最开始的行数据。 原因是此隔离级别下可以读取到事务内部数据。
3.3 一致性锁定读
-
除了非锁定读,在某些场合是需要使用锁定读的,那么就需要对SELECT操作加锁,有两种加锁方式。
1)SELECT…FOR UPDATE,加X锁。
2)SELECT…LOCK IN SHARE MODE,加S锁。 -
需要放进事务:无论是SELECT…FOR UPDATE,加X锁。SELECT…LOCK IN SHARE MODE,加S锁。都是加锁行为,所以必须放进事务中(不然没法没法释放锁),在事务提交后就会释放锁。
-
不耽误非锁定读:加了锁也不耽误非锁定读,加了就读快照。
3.4 自增长与锁
- 自增长使用的是表锁(老版本InnoDB),并且不是事务的,但是在高并发环境下还是会出现并发插入效率问题。详细的本文未将其纳入重点,不做分析。
3.5 外键和锁
- 在InnoDB存储引擎中,如果没有显示的对外键加索引,那么存储引擎会自动给外键加索引,这样可以避免表锁。
四、锁的算法
4.1 行锁的3种算法
- Record Lock:单个行记录上的锁,在InnoDB上如果隔离级别是READ COMMITTED就使用这个。 也叫单个锁
- Gap Lock:间隙锁,锁定一个范围,但是不包含记录本身。 范围锁
- Next-Key Lock:Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法,锁定一个范围,并且锁定记录本身,InnoDB对于行的查询都是采用这种锁定算法。 范围锁
设计目的:是为了解决Phantom Problem(幻象/幻读)
降级:当查询的索引含有唯一属性的时候,Next-Key Lock会降级称为Record Lock仅锁住索引本身。
4.2 解决Phantom Problem
- 在默认的REPEATABLE READ下,InnoDB存储引擎采用Next-Key Locking机制来避免Phantom Problem(幻读)问题。
- 幻读:连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能返回之前不存在的行。 实际上是因为当前事务能看到其他事务的执行结果引起的,就像幻觉一样凭空产生了一些值。
- InnoDB存储引擎的Next-Key Locking机制还可以在应用层实现唯一性检查,如使用:SELECT * FROM TABLE WHERE col=xxx LOCK IN SHARE MODE;这个操作,即使查询的值不在,它锁定的还是一个范围,若没有返回行,那么新插入的值一定是唯一的,不存在在并发条件下会有多个事务插入,会导致死锁,只有一个事务会操作成功。
五、锁问题
锁定机制可以实现事务的隔离性,但是也会带来些问题。如下三种,也仅此三种,不要将幻读也当成锁的问题,幻读是被Next-Key Lock锁很好解决的问题:
5.1 脏读
- 脏数据:和脏页不同,和脏页不同,和脏页不同,脏页指的是在缓冲中已经被修改的页,只是还没有被刷新到磁盘中而已,但是终究是会被刷新进去的,指的是缓冲池中的页和磁盘中的页数据不一致。而脏数据指的是事务对缓冲池中的行记录进行了修改,还没有被提交。
- 读取脏页是正常的事,是由于数据库实例内存和磁盘的异步造成的,不影响数据的一致性(终究会被刷新到磁盘的)。读取脏数据就不一样了,是一个事务竟然读取到了另一个事务还没提交的数据,这违反了数据库的隔离性。
- 发生的条件:脏读发生的条件是事务的隔离界别为READ UNCOMMITTED(InnoDB的默认隔离级别是RR)。
5.2 不可重复读
- 定义:在一个事务内,多次读取同一个数据集合,在这个事务还没结束时另外一个事务也访问了同一数据集,并且做了DML操作,由于事务的修改第一二次看到的数据会不一样,这样的就是不可重复读。是不是觉得不可重复读和Phantom Problem也就是幻读很像?实际上这有点微妙,在MySql官方文档中的确将不可重复读问题定义为Phantom Problem,并且在InnoDB中采用Next-Key Lock算法解决,但是。。。。。看下面:
注意:“幻读”是指读的过程中,某些元组被增加或删除,这样进行一些集合操作,比如算总数,平均值等等,就会每次算出不一样的数。所以“不可重复读”和“幻读”都是读的过程中数据前后不一致,只是前者(不可重复读)侧重于修改,后者(幻读)侧重于增删。个人认为,严格来讲“幻读”可以被称为“不可重复读”的一种特殊情况,没错的。但是从数据库管理的角度来看二者是有区别的。解决“不可重复读”只要加行级锁就可以了。而解决“幻读”则需要加表级锁,或者采用其他更复杂的技术,总之代价要大许多。这是搞数据库的那帮家伙非要把这两者区分开的动机吧。禁止写时读,避免了“脏读”,对应隔离级别read committed。禁止读时写,避免了“不可重复读”,对应隔离级别repeatable read。而为了避免“幻读”,干脆把整个表给锁住了,Innodb采取的方法是加间隙锁,或者是serialize隔离级别了。
注意中的内容来自于知乎linganmm用户对于问题《在数据库中不可重复读和幻读到底应该怎么分?》的回答,链接地址附上。https://www.zhihu.com/question/392569386
5.3 丢失更新(有问题,理解不到位)
- 实际上这不是指数据库理论意义上的丢失更新,数据库可以在任何隔离级别下阻止数据的丢失,这指的是一个生产实践中逻辑意义上的丢失更新问题,导致该问题的并不是数据库本身。解决方式是:让事务的操作串行化而不是并行的(在需要记录每一步的应用中),并且使用排他锁。具体的例子参考MySql技术内幕第275~276页。算是个遗留问题吧,我对这个理解不到位,标记一下。
六、阻塞
- 与高并发中的定义是一样的,不赘述。
- 有一些InnoDB中自己的性质:可以用innodb_lock_wait_timeout来控制阻塞时间,可以使用innodb_rollback_on_timeout来在超时后控制是否回滚(默认情况下不回滚)。
七、 死锁
- 定义和高并发内容一样,不再赘述。
- InnoDB采用了wait-for graph图来判断事务之间是否死锁,如果wait-for graph图存在回路,那么构成回路的节点(事务)就是构成死锁的。如下图:
事务t1和t2构成死锁。
八、 锁升级
- 定义:将行锁升级为页锁,将页锁升级为表锁这种将锁粒度小的升级为粒度大的。
- 目的:保护系统资源,减少不必要的浪费。
- 锁升级的情况:
1)由一句单独的SQL语句在一个对象上持有的锁的数量超过了阈值,默认这个阈值为5000。如果是不同的对象那么不会发生锁升级。
2)锁资源占用的内存超过了激活内存的40%就会发生锁升级。 - 副作用:锁粒度增大会导致并发性能下降,原因不赘述,参考高并发内容。