面试笔记 | MySQL数据库—写锁、读锁、表锁、行锁、页锁、自旋锁、互斥锁、间隙锁等

MySQL数据库—锁

  • 当并发事务同时访问一个资源时,有可能导致数据不一致,因此需要一种机制来将数据访问顺序化,以保证数据库数据的一致性。 锁就是其中的一种机制。
    面试笔记 | MySQL数据库—写锁、读锁、表锁、行锁、页锁、自旋锁、互斥锁、间隙锁等_第1张图片

基于锁的并发控制流程

  • 事务根据自己对数据项进行的操作类型申请相应的锁(读申请共享锁,写申请排他锁)。
  • 申请锁的请求被发送给锁管理器。锁管理器根据当前数据项是否已经有锁以及申请的和持有的锁是否冲突决定是否为该请求授予锁。
  • 若锁被授予,则申请锁的事务可以继续执行;
  • 若被拒绝,则申请锁的事务将进行等待,直到锁被其他事务释放。

可能出现的问题

  • 死锁:多个事务持有锁并互相循环等待其他事务的锁导致所有事务都无法继续执行。
  • 饥饿:数据项A一直被加共享锁,导致事务一直无法获取A的排他锁。
    对于可能发生冲突的并发操作,锁使它们由并行变为串行执行,是一种悲观的并发控制。

悲观锁与乐观锁

乐观并发控制: 对于并发执行可能冲突的操作,假定其不会真的冲突,允许并发执行,直到真正发生冲突时才去解决冲突,比如让事务回滚。

  • 顾名思义,很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

悲观并发控制: 对于并发执行可能冲突的操作,假定其必定发生冲突,通过让事务等待(锁)或者中止(时间戳排序)的方式使并行的操作串行执行。

  • 总会假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观的。

1、悲观锁

在关系型数据库中,悲观并发控制(悲观锁,Pessimistic Concurrency Control,PCC) 是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作对某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。

悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度(悲观),因此,在整个数据处理过程中,将数据处于锁定状态。 悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

在数据库中,悲观锁的流程如下:

  1. 在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
  2. 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。 具体响应方式由开发者根据实际需要决定。
  3. 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
  4. 其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。
优点与不足:
  • 悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。
  • 但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;
  • 另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数

2、乐观锁

在关系型数据库中,乐观并发控制(乐观锁,Optimistic Concurrency Control,OCC)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。

  • 乐观锁相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。

一般的,实现乐观锁的方式有两种:版本号和时间戳

对于版本号实现,当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。

优点与不足:
  • 乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。但如果直接简单这么做,还是有可能会遇到不可预期的结果,例如两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就遇到了问题。

行级锁、页级锁、表级锁

行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最高。会发生在:InnoDB 存储引擎。

页级锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。会发生在:BDB 存储引擎。

表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。 会发生在:MyISAM、memory、InnoDB、BDB 等存储引擎中。

面试笔记 | MySQL数据库—写锁、读锁、表锁、行锁、页锁、自旋锁、互斥锁、间隙锁等_第2张图片
在 MySQL InnoDB 存储引擎中,锁分为行锁和表锁。其中行锁包括两种锁。

  • 行锁就是给索引上的索引项加锁。

01 共享锁 S锁 读锁

  • 可以查看但无法修改和删除的一种数据锁。
  • 如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获得共享锁的事务只能读数据,不能修改数据。共享锁下其他用户可以并发读取,查询数据。但不能修改、增加、删除数据。资源共享。

02 排他锁 X锁 写锁 独占锁

  • 若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。

另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。表锁又分为三种。

  • 意向共享锁(IS):事务计划给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。

  • 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。

  • 自增锁(AUTO-INC Locks):特殊表锁,自增长计数器通过该“锁”来获得子增长计数器最大的计数值。

在加行锁之前必须先获得表级意向锁,否则等待 innodb_lock_wait_timeout超时后根据innodb_rollback_on_timeout决定是否回滚事务。

  • 在自增锁的使用过程中,有一个核心参数,需要关注,即 innodb_autoinc_lock_mode,它有0、1、2 三个值。
    面试笔记 | MySQL数据库—写锁、读锁、表锁、行锁、页锁、自旋锁、互斥锁、间隙锁等_第3张图片
    InnoDB 锁关系矩阵如下图所示,其中:+ 表示兼容,- 表示不兼容。
    面试笔记 | MySQL数据库—写锁、读锁、表锁、行锁、页锁、自旋锁、互斥锁、间隙锁等_第4张图片

InnoDB 行锁

  • InnoDB 行锁是通过对索引数据页上的记录(record)加锁实现的。

主要实现算法有 3 种:Record Lock、Gap Lock 和 Next-key Lock。

  • Record Lock 锁:记录锁,单个行记录的锁(锁数据,不锁 Gap)。
  • Gap Lock 锁间隙锁,锁定一个范围,不包括记录本身(不锁数据,仅仅锁数据前面的Gap)。范围没有结果,叶子结点之间加排他锁,基于索引。
  • Next-key Lock 锁:同时锁住数据,并且锁住数据前面的 Gap。范围有结果,前开后闭加排他锁,基于索引。

脏读,使用排他锁解决。
不可重复读,用共享锁解决。
幻读,用临键锁解决。

排查 InnoDB 锁问题

排查 InnoDB 锁问题通常有 2 种方法。

  • 打开 innodb_lock_monitor 表,注意使用后记得关闭,否则会影响性能。
  • 在 MySQL 5.5 版本之后,可以通过查看 information_schema 库下面的 innodb_locksinnodb_lock_waitsinnodb_trx 三个视图排查 InnoDB 的锁问题。
InnoDB 加锁行为

下面举一些例子分析 InnoDB 不同索引的加锁行为。分析锁时需要跟隔离级别联系起来,我们以 RR 为例,主要是从四个场景分析。

  • 主键 + RR。
  • 唯一键 + RR。
  • 非唯一键 + RR。
  • 无索引 + RR。

互斥锁(mutex)机制,以及互斥锁和读写锁的区别

互斥锁

  • 在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为互斥锁的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

互斥锁和读写锁的区别:

1)读写锁区分读者和写者,而互斥锁不区分。
2)互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。

2、Linux的4种锁机制:

  • 互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒
  • 读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。
  • 自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源。
  • RCU:即read-copy-update,在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改。修改完成后,再将老数据update成新的数据。使用RCU时,读者几乎不需要同步开销,既不需要获得锁,也不使用原子指令,不会导致锁竞争,因此就不用考虑死锁问题了。而对于写者的同步开销较大,它需要复制被修改的数据,还必须使用锁机制同步并行其它写者的修改操作。在有大量读操作,少量写操作的情况下效率非常高。

避免死锁的方法:

当两个事务同时执行,一个事务锁住了主键索引,在等待其他相关索引;而另一个事务锁定了非主键索引,在等待主键索引。这样就发生了死锁

在发生死锁时,InnoDB 存储引擎会自动检测,并且会自动回滚代价较小的事务来解决死锁问题。但很多时候一旦发生死锁,InnoDB 存储引擎的处理的效率是很低下的或者有时候根本解决不了问题,需要人为手动去解决。

  • 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
  • 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率。
  • 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定粒度,通过表级锁定来减少死锁产生的概率。
  • 更新 SQL 的 where 条件时尽量用索引;
  • 加锁索引准确,缩小锁定范围;
  • 减少范围更新,尤其非主键/非唯一索引上的范围更新。
  • 控制事务大小,减少锁定数据量和锁定时间长(innodb_row_lock_time_avg)。
  • 加锁顺序一致,尽可能一次性锁定所有所需的数据行。
  • 尽量基于 primary 或 unique key 更新数据。
  • 单次操作数据量不宜过多,涉及表尽量少。
  • 减少表上索引,减少锁定资源。
  • 相关工具:pt-deadlock-logger

你可能感兴趣的:(#,数据库,SQL,锁,数据库)