MySQL系列(五) 锁
-
13 锁
锁机制用于管理对共享资源的并发访问。
InnoDB 锁的实现与 Oracle 数据库非常类似。提供一致性的非锁定读、行级锁支持。行级锁没有额外的开销,并可以同时得到并发行和一致性。
-
lock 和 latch
lock 和 latch 都可以成为锁,但是两者有截然不同的意义。
latch 一般称为闩锁,轻量级的锁,要求锁定的时间必须非常短,其目的是用来保证并发线程操作临界资源的正确性,没有死锁的检测。
lock 的对象是事务锁,用来锁定的是数据库的如表、页、行等。并且一般lock 的对象仅在事务提交或回滚之后被释放。lock 有死锁机制。
-
锁的 类型
- 共享锁: 允许事务读一行数据 S锁
- 排他锁:允许事务删除或更新一行数据 X锁
- 将事务串行化,必须等待锁资源的释放。
- 数据库会对DML操作自动加 X锁。
共享锁和共享锁兼容,其他任何组合都不兼容
- 意向锁: 将锁定的对象分为多个层次,意味着事务希望在更细的粒度上加锁。
-
一致性非锁定读
一致性的非锁定读是指 InnoDB 存储引擎通过行多版本控制的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行 DELETE 或 UPDATE 操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB存储引擎会去读取行的一个快照数据。
快照数据是指该行的之前版本的数据,该实现是通过 undo(重做日志) 段来完成。而undo 用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。
非锁定读机制极大地提高了数据库的并发性。在InnoDB存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。
但是在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用一致性非锁定读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也各不相同。
REPEATABLE READ InnoDB存储引擎的默认事务隔离级
-
一致性锁定读
在默认配置下,InnoDB存储引擎的SELECT操作使用一致性非锁定读。但是在某些情况下,用户需要显式地对数据库读取操作进行加锁以保证数据逻辑的一致性。而这要求数据库支持加锁语句,即使是对于SELECT的只读操作。
支持的锁定读操作:
- SELECT…FOR UPDATE 加X锁
- SELECT…LOCK IN SHARE MODE 加S锁
-
行锁的三种算法
- 单行锁,作用域单行记录的锁
- 间隙锁,锁定一个范围,但不包括本身
- 为了阻止多个事务将记录插入到同一范围中,为了防止幻读
- 间隙锁+单行锁,Next-Key lock
- 设计的目的主要是为了解决幻读问题
- 锁定的不是一个值,而是一个范围
- 当查询的索引含有唯一属性时,会降级为单行锁
-
幻读问题
幻读是指在同一事物下,连续执行两次同样的SQL语句,会产生不同的结果。
InnoDB 采用的是行锁的第三种算法: 间隙锁+单行锁 来解决幻读问题。
假设表中有(1,2,5)三个id,有两个事务:
事务1: ... where id>2;
事务2: ... insert id=4;
如果事务1先于事务2,但没有提交;此时执行事务2,并且数据库允许,那么事务1提交时的结果就被改变了。即当前事务可以看到其他事务的结果,违反了事务的隔离性
事务默认的隔离级别为 REPEATABLE READ 不可重读,其采用 Next-Key lock 的方式来加锁,即对大于2的范围全部加锁,因此事务2的插入便是不被允许的,只能处于阻塞状态,等待事务1锁的释放。
而在隔离级别为 READ COMMITTED 不可重复读下,其仅采用单行锁的方式。
另外,Next-Key Locking机制在应用层面实现唯一性的检查。假设用户查询一个索引,如果值不存在则插入一行数据。这不会存在任何问题。
但是如果并发的执行,就会导致死锁,只有一个事务的插入操作会成功,其余的都会抛出死锁的错误。
-
并发时锁带来的三个问题
通过锁机制可以实现事务的隔离性要求,使得事务可以并发地工作。锁提高了并发性,但是却会带来潜在的问题。不过也正是因为事务隔离性的要求,锁一般只会带来三种问题。
-
1 脏读
脏数据是事务对缓冲池中行记录的修改,并且还没有被提交的数据。
脏读指不同的事务间,当前事务可以读到另外事务的脏数据。
脏读发生的条件需要事务的隔离级别为 READ UNCOUNCOMITTED 读未提交。等级最低的隔离级别。
-
2 不可重复读
不可重复读是指在一个事务内多次读取同一数据集合。在这个事务还没有结束时,另外一个事务也访问该同一数据集合,并做了一些DML操作。因此,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的情况,这种情况称为不可重复读,也称之为幻读。
不可重复读和脏读的区别是:脏读是读到未提交的数据,而不可重复读读到的却是已经提交的数据,其违反了数据库事务一致性的要求。
-
3 丢失更新
丢失更新是另一个锁导致的问题,简单来说其就是一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。
但是,在当前数据库隔离级别下,对于行的DML(数据操作语言),需要对行或更粗粒度对象的加锁,因此第二个事务不能对同一个记录进行DML操作,其会被阻塞,直到一个事务提交。
另一个典型的问题,在多用户计算机系统下:
1)事务T1查询一行数据,放入本地内存,并显示给一个终端用户User1。
2)事务T2也查询该行数据,并将取得的数据显示给终端用户User2。
3)User1修改这行记录,更新数据库并提交。
4)User2修改这行记录,更新数据库并提交。
User2提交的数据可能会覆盖User1提交的数据。
要避免这种问题,就要让事务这种情况下的操作变为可串行化,而不是并行的发生,即加上一个排他锁。
-
-
悲观锁和乐观锁
主要是为了解决丢失更新的问题
- 悲观锁
描述- 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。
- 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
- 它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。
- 悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
- 定义
- 属于 X 锁,排他锁
- 隔离级别属于可重复读级别,不会产生幻读等问题
- 悲观锁一般是用于并发不是很高,并且不允许脏读等情况。但是对数据库资源消耗较大。
- 使用
- 关闭MySQL的自动提交
- 使用一致性锁定读的X锁
- 乐观锁
-
描述- 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁
- 但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
- 乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
-
定义
- 不属于数据库内部的锁,人为定义的锁
- 隔离级别对应不可重复读,否则数据库对DML操作自动加排他锁
- 因此会出现幻读的情况,解决的方式是编写代码实现的版本控制
- 性能好,支持的并发也更多
- 乐观锁是首先假设数据冲突很少,只有在数据提交修改的时候才进行校验,如果冲突了则不会进行更新。
-
使用
通常的实现方式增加一个version字段,为每一条数据加上版本。每次更新的时候version+1,并且更新时候带上版本号。
更新完成时,需要从数据库获取当前version的值,如果和最开始获取的值一样,则表示这段时间没有执行DML操作,可以进行当前事务的执行。否则不行。
数据库会对DML操作自动加X锁,即排他锁。因此这段更新的期间,保证只有当前一个事务在执行。因此执行完成后,其他同样version的事务自动变为死锁被kill掉。
-
- 悲观锁
-
死锁
-
死锁的描述和解决
死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。若无外力作用,事务将无法推进下去。
解决死锁问题最简单的方式是不要有等待,将任何的等待都转化为回滚,并且事务重新开始。但是,这会导致并发性能的大幅下降,而且是无法察觉的,是比死锁更加严重的问题。
解决死锁问题较简单的一种方法是超时,当两个事务互相等待时,其中一个等待时间超过设置的某一阈值时,那么这个事务就进行回滚。因此另一个等待的事务就能进行下去。在InnoDB存储引擎中,参数innodb_lock_wait_timeout用来设置超时的时间。
超时机制虽然简单,但是其仅通过超时后对事务进行回滚的方式来处理,或者说其是根据FIFO的顺序选择回滚对象。但若超时的事务所占权重比较大,如事务操作更新了很多行,占用了较多的重做日志记录,这时采用FIFO的方式,就显得不合适了,因为回滚这个事务的时间相对另一个事务所占用的时间可能会很多。
较之超时机制,当前数据库普遍采用
等待图
的方式来主动检测死锁。等待图要求数据库保存两项信息:- 锁的信息链表
- 事务等待链表
通过上述链表可以构造出一张
图
,其采用深度优先的算法。如果图中存在回路,就代表死锁的存在。若存在死锁,InnoDB会立即回滚一个事务,通常选择回滚undo量最小的事务。
-
死锁的示例
AB-BA 死锁,两者互相等待资源的释放
Oracle 常见的死锁是没有对外键添加索引,而MySQL会对其自动添加。
此外还存在一种死锁,当前事务持有了带插入记录的下一个记录的排他锁,但是在等待队列还有一个共享锁的请求,则可能会发生死锁。
会话A中已经对记录4持有了X锁,但是会话A中插入记录3时会导致死锁发生。这个问题的产生是由于会话B中请求记录4的S锁而发生等待,但之前请求的锁对于主键值记录1、2都已经成功,若在事件点5能插入记录,那么会话B在获得记录4持有的S锁后,还需要向后获得记录3的记录,这样就显得有点不合理。因此InnoDB存储引擎在这里主动选择了死锁,而回滚的是undo log记录大的事务,这与AB-BA死锁的处理方式又有所不同。
-
-
锁升级
-
概念
锁升级是指将当前锁的粒度降低。举例来说,数据库可以把一个表的1000个行锁升级为一个页锁,或者将页锁升级为表锁。如果在数据库的设计中认为锁是一种稀有资源,而且想避免锁的开销,那数据库中会频繁出现锁升级现象。
-
SQL Service 对锁升级的处理
Microsoft SQL Server数据库的设计认为锁是一种稀有的资源,在适合的时候会自动地将行、键或分页锁升级为更粗粒度的表级锁。这种升级保护了系统资源,防止系统使用太多的内存来维护锁,在一定程度上提高了效率。
-
InnoDB 对锁升级的处理
InnoDB存储引擎不存在锁升级的问题。因为其不是根据每个记录来产生行锁的,相反,其根据每个事务访问的
每个页对锁进行管理的,采用的是位图的方式
。因此不管一个事务锁住页中一个记录还是多个记录,其开销通常都是一致的。
-
-