一、锁的概述

  • 锁是计算机协调多个进程或线程并发访问某一资源的机制。
  • 锁保证数据并发访问的一致性、有效性。
  • 锁冲突也是影响数据库并发访问性能的一个重要因素。
  • 锁是Mysql在服务器层和存储引擎层的并发控制。

一句话总结就是:锁机制用于管理对共享资源的并发访问。

二、锁的分类

从操作粒度区分:

  • 表级锁:每次操作锁住整张表。开销小,加锁快,粒度大,不会出现死锁,触发锁冲突的概率高,并发度低。
  • 行级锁:每次操作锁住一行数据。开销大,加锁慢,粒度小,会出现死锁,触发锁冲突的概率低,并发度高。
  • 页级锁:每次锁定相邻的一组记录。锁定粒度、开销、加锁时间介于行级锁和表级锁之间,会出现死锁,并发度一般。

三种锁在不同存储引擎中的应用:

表锁

行锁

页锁

MyISAM


InnoDB



BDB



从操作类型区分:

  • 读锁(S):共享锁,针对同一份数据,多个读操作可以同时进行而不会互相影响(其他事务可以读,但不能写)
  • 写锁(X):排它锁,当前的写操作没有完成时,会阻塞其他的读和写操作(其他事务既不能读取,也不能写)

为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意向锁(Intention Locks):

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

这两种意向锁都属于表级锁,S和X主要针对行级锁。在对表记录添加S或X锁之前,会先对表添加IS和IX锁,表明某个事务正在持有某些行的锁、或该事务准备去持有锁。意向锁存在是为了协调锁之间的关系,支持多粒度锁共存。

★ 为什么意向锁是表级锁呢?

答:为了减少确认次数,提升性能。如果意向锁是行锁,需要遍历每一行去确认数据是否已经加锁;而如果是表锁的话,就只需要判断一次就知道有没有数据行被锁定。

从操作性能区分:

  • 乐观锁:一般采用的方式是对数据记录版本进行对比,在数据更新提交时才会进行冲突检测,如果发现冲突了,则提示错误信息。
  • 悲观锁:在对一条记录进行修改时,为了避免被其他人修改,在修改数据之前先锁定再修改的方式。读锁和写锁是悲观锁的不同实现。

三、表级锁

偏向 MyISAM 存储引擎,MyISAM 在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行增删改操作前,会自动给涉及的表加写锁。在自动加锁的情况下,MyISAM 总是一次性获得 SQL 语句所需要的全部锁,这也正是 MyISAM 表不会出现死锁的原因。

MySQL 的表级锁有两种模式,分别是共享锁(读锁)和排他锁(写锁),它们的兼容关系如下:

读锁

写锁

读锁

兼容

不兼容

写锁

不兼容

不兼容

结合上表,对 MyISAM 表进行操作,会出现以下情况:

  1. 对 MyISAM 表的进行读操作(加读锁),不会阻塞其他进程和本线程对同一表的读请求,但会阻塞对同一表的写请求。只有当读锁释放后,才会执行其它进程和本线程的写操作。
  2. 对 MyISAM 表的写操作〈加写锁),会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进程的读和写操作。

简而言之,就是读锁会阻塞写,但不会堵塞读。而写锁则会堵塞读和写,下面来看例子:MySQL好玩的读写锁案例(表级锁)

四、事务

4.1 什么是事务

事务是由一步或几步数据库操作序列组成的逻辑执行单元,要么全部执行,要么全部不执行。事务具有以下4个属性:

  • 原子性(Atomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
  • 一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性。事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。
  • 隔离性(lsolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的独立环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
  • 持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。

4.2 并发事务处理带来的问题

  • 更新丢失(Lost Update):当两个或多个事务选择同一数据进行修改,由于每个事务都不知道其他事务的存在,就会导致最后一个事务所做的更新覆盖了其他事务所做的更新。
  • 脏读(Dirty Reads):当⼀个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外⼀个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,所以另外⼀个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
  • 不可重复读(Non-Repeatable Reads):指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第⼀个事务中的两次读数据之间,由于第二个事务的修改导致第⼀个事务两次读取的数据不⼀样。这就出现了在⼀个事务中两次读取的数据是不⼀样的情况,因此称为不可重复读。
  • 幻读(Phantom Reads):幻读与不可重复读类似。它发生在第一个事务读取了几行数据,接着另一个并发事务插入或删除了一些数据。随后第一个事务接之前相同的查询条件重新查询,发现多了或者少了一些原本不存在的记录,就好像发生了幻觉⼀样,所以称为幻读。

4.3 事务的隔离级别

脏读、不可重复读和幻读,其实都是数据库读一致性的问题,必须由数据库提供一定的事务隔离机制来解决。SQL 标准定义了四个隔离级别:

  • READ-UNCOMMITTED(读取未提交):最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、不可重复读或幻读。
  • READ-COMMITTED(读取已提交):允许读取并发事务已经提交的数据,可以防止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读):对同一数据的多次读取结果都是⼀致的,除非数据是被本身事务所修改,可以防止脏读和不可重复读,但幻读仍有可能发生。
  • SERIALIZABLE(可串行化):最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

脏读

不可重复读

幻读

读取未提交




读取已提交

×



可重复读

×

×


可串行化

×

×

×

MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重复读),我们可以通过 show variables like 'tx_isolation' 来查看当前数据库的事务隔离级别。

mysql 行锁最大并发 TPS mysql行锁并发量_数据库


数据库的事务隔离级别越高,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读”和“幻读”并不敏感,它们更关心数据并发访问的能力。

五、行级锁

偏向InnoDB存储引擎。InnoDB 与 MyISAM 最大不同之处在于:InnoDB 支持事务和默认采用行级锁。

5.1 InnoDB 加锁规则

  1. 对于普通 select 语句,InnoDB 不会加任何锁。
  2. 对于 insert、update、和 delete 语句,InnoDB 会自动给涉及数据集加排他锁。
  3. 意向锁是 InnoDB 自动加的, 不需用户干预。

事务可以通过以下语句显式给记录集加共享锁或排他锁:

select ... lock in share mode  // 加共享锁

其他事务可以查询这些记录集,并且也可以对该记录集加 S 锁,但不能对该记录集进行 DML 操作。

使用场景:为了确保自己查到的数据没有被其他的事务正在修改,也就是说确保查到的数据是最新的数据,并且不允许其他人来修改数据。但是自己不一定能够修改数据,因为有可能其他的事务也对这些数据加了 S 锁,所以此时如果当前事务要对该数据进行更新操作,则很有可能造成死锁。

select ... for update  // 加排他锁

其他事务可以查询该记录集,但是不能对该记录集加 S 锁或 X 锁,而是等待获得锁。

使用场景:为了确保让自己查到的数据是最新数据,并且查到后的数据只允许自己修改。

5.2 InnoDB 行锁实现细节

  • InnoDB 行锁是通过给索引上的索引项加锁来实现的,所以 InnoDB 的这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB 才使用行锁;否则,InnoDB 将使用表锁!
  • 不论是使用主键索引、唯一索引或普通索引,InnoDB 都会使用行锁来对数据加锁。
  • 只有执行计划真正使用了索引,才能使用行锁。即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查 SQL 的执行计划(可以通过 explain 检查 SQL 的执行计划),以确认是否真正使用了索引。
  • 由于 MySQL 的行锁是针对索引加的锁,而不是针对记录加的锁,所以即使多个session访问不同行的记录, 但是如果是使用相同的索引键, 还是会出现锁冲突的,后使用这些索引的session需要等待先使用索引的session释放锁后,才能获取锁。

5.3 InnoDB 的间隙锁

当我们使用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据记录的索引项加锁,对于键值在条件范围内但并不存在的记录,叫做 间隙(GAP),InnoDB也会对这个“间隙”加锁,这种锁机制就是间隙锁(Next-Key锁)。

很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内的键值的并发插入,这往往会造成严重的锁等待。因此,在实际应用开发中,尤其是并发插入比较多的应用,要尽量优化业务逻辑,尽可能地使用相等条件来访问更新数据,避免使用范围条件。

InnoDB使用间隙锁的目的:

  1. 防止幻读,以满足相关隔离级别的要求
    主要通过两个方面实现:
    (1)防止间隙内有新数据被插入
    (2)防止已存在的数据,更新成间隙内的数据
  2. 满足恢复和复制的需要
    MySQL 通过 BINLOG 录入执行成功的 INSERT、UPDATE、DELETE 等更新数据的 SQL 语句,并由此实现 MySQL 数据库的恢复和主从复制。MySQL 的恢复机制(复制其实就是在从数据库中不断做基于 BINLOG 的恢复)有以下特点:
    (1)MySQL 的恢复是 SQL 语句级的,也就是重新执行 BINLOG 中的 SQL 语句。
    (2)MySQL 的 Binlog 是按照事务提交的先后顺序记录的,恢复也是按这个顺序进行的。
    由此可见,MySQL 的恢复机制要求在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,其实也就是不允许出现幻读。