MySQL 技术内幕(三):锁

本文详细介绍了InnoDB存储引擎的锁机制,包括锁的类型如共享锁、排他锁、意向锁,以及一致性非锁定读和锁定读。还讨论了自增长属性与锁的关系,外键操作中的锁行为。此外,分析了行锁的三种算法(RecordLock、GapLock、Next-KeyLock)及其在解决幻像问题中的作用。最后,阐述了锁问题,如脏读、不可重复读、丢失更新和死锁,以及如何通过设置参数来管理和避免这些问题。
摘要由CSDN通过智能技术生成

开发多用户、数据库驱动的应用时,最大的一个难点是:一方面要最大程度地利用数据库的并发访问,另外一方面还要确保每个用户能以一致的方式读取和修改数据。为此就有了锁(locking)的机制。

从程序员的角度来说,锁分为两类:

  • 悲观锁:顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

  • 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

lock 和 latch 的区别:

  • latch:latch 一般称为闩(shuan ,意为古时候门上的那根木棍)锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差。在 InnoDB存储引擎中,latch又可以分为mutex(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且.通常没有死锁检测的机制。
  • lock:lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commit或rollback后进行释放(不同事务隔离级别释放的时间可能不同)。此外,lock,正如在大多数数据库中一样,是有死锁机制的。
  • 总得来说,latch 操作的是数据库内部的线程对于共享内存的操作,例如维护内部的数据页,对于LRU列表的删除和添加等;而 lock 则是锁对于事务对数据的修改。

1. InnoDB 存储引擎中的锁

1.1 锁的类型

InnoDB存储引擎实现了如下两种标准的行级锁:

  • 共享锁(S Lock),允许事务读一行数据。
  • 排他锁(X Lock),允许事务删除或更新一行数据。

如果一个事务已经获得了某个行的 S 锁,那么另一个事务也可以获得该行的 S 锁,因为S锁对于数据不会产生修改,这种情况称为锁兼容(lock Compatible),而如果一个事务获取了某一行的 X 锁,则该事务独占这一行,不再与其他锁兼容。

为了支持多粒度(granular)的锁定,也就是为了允许事务在表级上的锁和行级上的锁同时存在,InnoDB 存储引擎提供了一种额外的锁方式,称为意向锁(Intention Lock)。

意向锁也支持两种类型,设计目的主要是为了在一个事务中揭示下一层次锁请求的锁类型。

  • 意向共享锁 (IS Lock),事务想要获得一张表中某几行的共享锁;
  • 意向排他锁 (IX Lock),事务想要获得一张表中某几行的排他锁;

在这里插入图片描述
例如上图这样层次结构的一个数据库,想要对某页中的某条记录加上一个 X 锁,那么,就会将该记录上层的页、表加上一个 IX 锁,如果任何一个部分导致了锁的等待则该加锁操作会阻塞,或者,在加完锁之后,某个事务想将该记录对应的数据表加上 X 锁,因为 IX 锁的存在,所以也会阻塞等待。

在这里插入图片描述

1.2 一致性非锁定读

一致性的非锁定读(consistent nonlocking read)是指InnoDB存储引擎通过行多版本控制 ( multi versioning)的方式来读取当前执行时间数据库中行的数据。即,如果读取的行记录正在被其他事务 DELETE 或者 UPDATE,那么不会阻塞等待 X 锁的释放,而是读取一个快照数据

这样可以极大的提高数据库的并发性,这是默认的读取方式,但是不同的隔离级别会有不同的实现方式。另外,快照就是当前行数据的历史版本,每行数据可能会有多个版本,多个版本带来的并发控制,就称之为多版本并发控制 (Multi Version Concurrency Control,MVCC)。多版本并发控制的实现如下

  • InnoDB 的 MVCC,是通过在每行记录后面保存两个隐藏的列来实现的,这两个列一个保存了行的创建时间,一个保存了过期或者删除时间,不是真的时间戳而是系统版本号。
  • 每开启一个新的事务,系统版本号都会自动递增,事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。
  • MVCC 使用到的快照存储在 Undo 日志中,该日志通过回滚指针把一个数据行(Record)的所有快照连接起来。
  • 对于 READ COMMITTED (读已提交)事务隔离级别,总是会非锁定的读取最新的一份快照数据,也就是小于当前系统版本号的最大的一条快照数据。
  • 对于 REPEATABLE READ (可重复度)事务隔离级别下,总是读取该事务开始之前的一份快照数据,也就是小于该事务版本号中最大的一条快照数据。
  • 对于读未提交以及串行化读,则不需要MVCC的快照实现。

1.3 一致性锁定读

在默认配置 REPEATABLE READ (可重复读)的隔离级别下,默认就是通过一致性非锁定来进行SELECT读取,但是,一些特殊情况下,用户想要显式地对数据库读取操作进行加锁来保证一致性。

InnoDB 存储引擎提供了两个命令来进行锁定:

  • SELECT . . . FOR UPDATE:表示加上一个 X 锁。
  • SELECT . . . LOCK IN SHARE MODE:表示加上一个 S 锁,阻塞其他的加 X 锁操作。

1.4 自增长与锁

自增长在数据库中是非常常见的一种属性。在InnoDB存储引擎的内存结构中,对每个含有自增长值的表都有一个自增长计数器(auto-increment counter)。当对含有自增长的计数器的表进行插人操作时,这个计数器执行函数获取计数器的值,然后 + 1 插入,该方式称为 AUTO-INC Locking。其实是采用一种特殊的表锁机制,来保证自增。但是为了提高效率,会在插入后释放锁,而不是事务提交后释放。

MySQL 5.1.2之后,通过轻量级互斥量来实现自增长。

1.5 外键和锁

在InnoDB存储引擎中,对于一个外键列,如果没有显式地对这个列加索引,InnoDB存储引擎自动对其加一个索引,因为这样可以避免表锁。(只有走索引才会使用行锁)。

对于外键值的插人或更新,首先需要查询父表中的记录,即SELECT父表。但是对于父表的SELECT操作,不是使用一致性非锁定读的方式,因为这样会发生数据不一-致的问题,因此这时使用的是SELECT…LOCK IN SHARE MODE方式,即主动对父表加一个S锁。如果这时父表上已经这样加X锁,子表上的操作会被阻塞。

2. 锁的算法

2.1 行锁的三种算法

InnoDB 存储引擎有三种行锁的算法:

  • Record Lock:单个行记录上的锁;
    • Record Lock 总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定
  • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身;
    • 也就是范围从上一个索引到该索引,并且左右都是开区间。
  • Next-Key Lock : Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身;
    • 也就是锁住上一个索引到该索引,左边是开区间而右边是闭区间。

对于唯一索引或者主键索引,如果查询的索引含有唯一属性,InnoDB 存储引擎会对 Next-key Lock 进行优化,将其降级为 Record Lock,锁住索引本身而不是范围来提高并发量。

而对于辅助索引,其加上的是Next-Key Lock,并且会对辅助索引的下一个键值对加上一个Gap Lock,所以总得锁定范围为上一个键值对到下一个键值对,两边都是开区间。

InnoDB存储引擎采用 Next-Key Locking 机制来避免Phantom Problem (幻像问题)。

Phantom Problem是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。

另外,用户可以通过以下两种方式来显式地关闭Gap Lock:

  • 将事务的隔离级别设置为READ COMMITTED;
  • 将参数innodb_locks_unsafe_for_binlog 设置为;

3. 锁问题

  • 脏读
    • 所谓脏数据,是指事务对缓冲池中的行记录的修改,并且还没有提交。
    • 脏读即指的是,在 READ UNCOMMITED 的情况下,一个事务读取到另一个事务修改但是还没有提交的脏数据。
  • 不可重复读
    • 不可重复读指的是,一个事务内多次读取同一个数据集合,因为另一个事务的修改,而变得数据不一样。
    • 会发生在 READ COMMITED 隔离级别下。但是读取的是已经提交的数据,本身不会带来很大的影响。
    • 可以采用 Next-Key Lock 来解决这个问题。
  • 丢失更新
    • 指的是一个事务的更新操作会被另一个事务的更新操作覆盖,这种情况在所有隔离级别下都不会发生,因为有行锁的存在。
    • 但是可能发生的情况时,一个线程从数据库读取到数据,去进行修改,这是另一个线程也来读取了原先的数据,进行了修改,然后第一个线程更新数据,然后第二个线程更新数据,这就发生了丢失更新。
    • 这种情况就需要在读取的时候就加上X Lock 排他锁,等所有操作完成,再更新之后,再commit 提交事务释放锁。
  • 阻塞
    • 阻塞指的是,一个事务中的锁需要等待另一个事务中的锁释放它锁占用的资源。
    • InnoDB 存储引擎可以通过设置一些参数来对阻塞进行控制:
      • 参数 innodb_lock_wait_timeout 用来控制等待的时间(默认为 50 s);
      • 参数 innodb_rollback_on_timeout 用来设定是否在等待超时时对进行中的事务进行回滚。(默认为不回滚)。
  • 死锁
    • 死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。
    • 数据库普遍采用 wait-for graph (等待图)的方式来检测死锁,就是通过保存锁的信息链表,以及事务等待锁的链表来检测是否有环。
  • 两阶段锁
    • 锁分为加锁的阶段以及解锁的阶段,锁会在需要的时候加上,但是不会在不需要的时候释放,只会在事务提交的时候释放锁。
    • 所以要尽量把冲突多的操作放在后面。减少占有锁的时间。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值