热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

认识什么是锁,MySQL中锁如何解决幻读问题

MySQL教程栏目介绍锁如何解决幻读问题。

MySQL教程栏目介绍锁如何解决幻读问题。

前言

今天就为大家介绍一下MySQL中锁相关的知识。

本文在没有特别声明的情况下,均是默认InnoDB引擎,如涉及到其他引擎或者数据库则会特别指出。

什么是锁

锁是一种用于保证在并发场景下每个事务仍能以一致性的方式读取和修改数据的方式,当一个事务对某一条数据上锁之后,其他事务就不能修改或者只能阻塞等待锁的释放,所以锁的粒度大小一定程度上可以影响到访问数据库的性能。

从锁的粒度上来说,我们可以将锁分为表锁和行锁。

表锁

顾名思议,表锁就是直接锁表,在MyISAM引擎中就只有表锁。

表锁的加锁方式为:

LOCK TABLE 表名 READ;--锁定后表只读
UNLOCK TABLE; --解锁 

行锁

行锁,从名字上来看,就是锁住一行数据,然而,行锁的实际实现算法会相对复杂,有时候并不仅仅只是锁住某一条数据,这个后面再展开。

正常的思路是:锁住一行数据之后,其他事务就不能来访问这条数据了,那么我们想象,假如事务A访问了一条数据,只是拿出来读一下,并不想去修改,正好事务B也来访问这条数据,也仅仅只是想拿出来读一下,并不想去修改,这时候如果因此阻塞了,就有点浪费性能了。所以为了优化这种读数据的场景,我们又把行锁分为了两大类型:共享锁和排他锁

共享锁

共享锁,Shared Lock,又称之为读锁,S锁,就是说一条数据被加了S锁之后,其他事务也能来读数据,可以共享一把锁。
我们可以通过如下语句加共享锁:

select * from test where id=1 LOCK IN SHARE MODE; 

加锁之后,直到加锁的事务结束(提交或者回滚)就会释放锁。

排他锁

排他锁,Exclusive Lock,又称之为写锁,X锁。就是说一条数据被加了X锁之后,其他事务想来访问这条数据只能阻塞等待锁的释放,具有排他性。

当我们在修改数据,如:insert,update,delete的时候MySQL就会自动加上排他锁,同样的,我们可以通过如下sql语句手动加上排他锁:

select * from test where id=1 for update; 

在InnoDB引擎中,是允许行锁和表锁共存的。

但是这样就会有一个问题,假如事务A给t表其中一行数据上锁了,这时候事务B想给t表上一个表锁,这时候怎么办呢?事务B怎么知道t表有没有行锁的存在,如果采用全表遍历的情况,当表中的数据很大的话,加锁都要加半天,所以MySQL中就又引入了意向锁

意向锁

意向锁为表锁,分为两种类型,分为:意向共享锁(Intention Shared Lock)和意向排他锁(Intention Exclusive Lock),这两种锁又分别可以简称为IS锁和IX锁。

意向锁是MySQL自己维护的,用户无法手动加意向。

意向锁有两大加锁规则:

  • 当需要给一行数据加上S锁的时候,MySQL会先给这张表加上IS锁。
  • 当需要给一行数据加上X锁的时候,MySQL会先给这张表加上IX锁。

这样的话上面的问题就迎刃而解了,当需要给一张表上表锁的时候,只需要看这张表是否有对应的意向锁就可以了,无需遍历整张表。

各种锁的兼容关系

下面这张图是各种锁的兼容关系,参考自官网:


XIXSIS
X

互斥

互斥

互斥

互斥

IX

互斥

共享

冲突

共享

S

互斥

互斥

共享

共享

IS

互斥

共享

共享

共享

锁到底锁的是什么

建立以下两张表,并初始化5条数据,注意test表有2个索引而test2没有索引:

CREATE TABLE `test` (
  `id` int(11) NOT NULL,
  `name` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `NAME_INDEX` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO test VALUE(1,'张1');
INSERT INTO test VALUE(5,'张5');
INSERT INTO test VALUE(8,'张8');
INSERT INTO test VALUE(10,'张10');
INSERT INTO test VALUE(20,'张20');

CREATE TABLE `test2` (
  `id` varchar(32) NOT NULL,
  `name` varchar(32) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO test2 VALUE(1,'张1');
INSERT INTO test2 VALUE(5,'张5');
INSERT INTO test2 VALUE(8,'张8');
INSERT INTO test2 VALUE(10,'张10');
INSERT INTO test2 VALUE(20,'张20'); 

举例猜测

在行锁中,假如我们对一行记录加锁,那么到底是把什么东西锁住了,我们来看下面两个例子:
举例1(操作test表):

事务A事务B
BEGIN;
SELECT * FROM test WHERE id=1 FOR UPDATE;

SELECT * FROM test WHERE id=1 FOR UPDATE;

阻塞


SELECT * FROM test WHERE id=5 FOR UPDATE;

加锁成功

COMMIT;

(释放锁)



SELECT * FROM test WHERE id=1 FOR UPDATE;

加锁成功

举例2(操作test2表):

事务A事务B
BEGIN;
SELECT * FROM test2 WHERE id=1 FOR UPDATE;

SELECT * FROM test2 WHERE id=1 FOR UPDATE;

阻塞


SELECT * FROM test2 WHERE id=5 FOR UPDATE;

阻塞

COMMIT;

(释放锁)



SELECT * FROM test2 WHERE id=1 FOR UPDATE;

加锁成功

从上面两个例子我们可以发现,test表好像确实是锁住了id=1这一行的记录,而test2表好像不仅仅是锁住了id=1这一行记录,实际上经过尝试我们就知道,test2表是被锁表了,所以其实MySQL中InnoDB锁住的是索引,当没有索引的时候就会锁表

接下来再看一个场景:

事务A事务B
BEGIN;
SELECT * FROM test WHERE name=‘张1’ FOR UPDATE;

SELECT name FROM test WHERE name=‘张1’ FOR UPDATE;

阻塞


SELECT id FROM test WHERE id=1 FOR UPDATE;

阻塞

COMMIT;

(释放锁)



SELECT id FROM test WHERE id=1 FOR UPDATE;

加锁成功

这个例子中我们是把name索引锁住了,然后我们在事务B中通过主键索引只查id,这样就用到name索引了,但是最后发现也被阻塞了。所以我们又可以得出下面的结论,MySQL索引不但锁住了辅助索引,还会把辅助索引对应的主键索引一起锁住

到这里,可能有人会有怀疑,那就是我把辅助索引锁住了,但是假如加锁的时候,只用到了覆盖索引,然后我再去查主键会怎么样呢?

接下来让我们再验证一下:

事务A事务B
BEGIN;
SELECT name FROM test WHERE name=‘张1’ FOR UPDATE;

SELECT name FROM test WHERE name=‘张1’ FOR UPDATE;

阻塞


SELECT * FROM test WHERE id=1 FOR UPDATE;

阻塞


SELECT id FROM test WHERE id=1 FOR UPDATE;

阻塞

COMMIT;

(释放锁)



SELECT id FROM test WHERE id=1 FOR UPDATE;

加锁成功

我们可以看到,就算只是用到了辅助索引加锁,MySQL还是会把主键索引锁住,而主键索引的B+树叶子节点中,又存储了整条数据,所以查询任何字段都会被锁定。

到这里,我们可以明确的给锁到底锁住了什么下结论了:

结论

InnoDB引擎中,锁锁的是索引:

  • 假如一张表没有索引,MySQL会进行锁表(其实锁住的是隐藏列ROWID的主键索引)
  • 假如我们对辅助索引加锁,那么辅助索引所对应的主键索引也会被锁住
  • 主键索引被锁住,实际上就等于是整条记录都被锁住了(主键索引叶子节点存储了整条数据)

行锁的算法

上一篇介绍事务的时候我们提到了,MySQL通过加锁来防止了幻读,但是如果行锁只是锁住一行记录,好像并不能防止幻读,所以行锁锁住一条记录的话只是其中一种情况,实际上行锁有三种算法:记录锁(Record Lock),间隙锁(Gap Lock)和临键锁(Next-Key Lock),而之所以能做到防止幻读,正是临键锁起的作用。

记录锁(Record Lock)

记录锁就是上面介绍的,当我们的查询能命中一条记录的时候,InnoDB就会使用记录锁,锁住所命中的这一行记录。

间隙锁(Gap Lock)

当我们的查询没有命中记录的时候,这时候InnoDB就会加上一个间隙锁。

事务A事务B
BEGIN;
SELECT * FROM test WHERE id=1 FOR UPDATE;

INSERT INTO test VALUE (2,‘张2’);

阻塞


INSERT INTO test VALUE (3,‘张3’);

阻塞


SELECT * FROM test WHERE id=2 FOR UPDATE;

加锁成功

COMMIT;

(释放锁)


从上面的例子中,我们可以得出结论:

  • 间隙锁与间隙锁之间不冲突,也就是事务A加了间隙锁,事务B可以在同一个间隙中加间隙锁。(之所以会用到间隙锁就是没有命中数据的时候,所以并没有必要去阻塞读,也没有必要阻塞其他事务对同一个间隙加锁)
  • 间隙锁主要是会阻塞插入操作

间隙是如何确定的

test表中有5条记录,主键值分别为:1,5,8,10,20。那么就会有如下六个间隙:
(-∞,1),(1,5),(5,8),(8,10),(10,20),(20,+∞)

而假如主键不是int类型,那么就会转化为ASCII码之后再确定间隙。

临键锁(Next-Key Lock)

临键锁就是记录锁和间隙锁的结合。当我们进行一个范围查询,不但命中了一条或者多条记录,且同时包括了间隙,这时候就会使用临键锁,临键锁是InnoDB中行锁的默认算法。

注意了,这里仅针对RR隔离级别,对于RC隔离级除了外键约束和唯一性约束会加间隙锁,没有间隙锁,自然也就没有了临键锁,所以RC级别下加的行锁都是记录锁,没有命中记录则不加锁,所以RC级别是没有解决幻读问题的

临键锁在以下两个条件时会降级成为间隙锁或者记录锁:

  • 当查询未命中任务记录时,会降级为间隙锁。
  • 当使用主键或者唯一索引命中了一条记录时,会降级为记录锁。
事务A事务B
BEGIN;
SELECT * FROM test WHERE id>=2 AND id<=6 FOR UPDATE;



INSERT INTO test VALUE (2,‘张2’);

阻塞


INSERT INTO test VALUE (6,‘张6’);

阻塞


INSERT INTO test VALUE (8,‘张8’);

阻塞


SELECT * FROM test WHERE id=8 FOR UPDATE;

阻塞


INSERT INTO test VALUE (9,‘张9’);

插入成功

COMMIT;

(释放锁)


上面这个例子,事务A加的锁跨越了(1,5)和(5,8)两个间隙,且同时命中了5,然后我们发现我们对id=8这条数据进行操作也阻塞了,但是9这条记录插入成功了。

临键锁加锁规则

临键锁的划分是按照左开右闭的区间来划分的,也就是我们可以把test表中的记录划分出如下区间:(-∞,1],(1,5],(5,8],(8,10],(10,20],(20,+∞)。

那么临键锁到底锁住了哪些范围呢?

**临键锁中锁住的是最后一个命中记录的 key 和其下一个左开右闭的区间**

那么上面的例子中其实锁住了(1,5]和(5,8]这两个区间。

临键锁为何能解决幻读问题

临键锁为什么要锁住命中记录的下一个左开右闭的区间?答案就是为了解决幻读。

我们想一想上面的查询范围id>=2且id<=6,如果我们事务A只锁住了(1,5]这个区间,假如这时候事务B插入一条数据id=6,那么事务A再去查询,就会多出来了一条记录id=6,就会出现了幻读,所以我把你下一个区间5,10]也给锁住,就可以避免了幻读。

当然,其实如果我们执行的查询刚好是id>=2且id<=5,那么就算只锁住了(1,5],同样能避免幻读问题,只是我们要考虑到查询范围的最大值没有命中记录的情况,而锁住了下一个区间,可以确保不论是哪种范围查询,都可以避免幻读的产生。

在我们使用锁的时候,有一个问题是需要注意和避免的,我们知道,排它锁有互斥的特性。一个事务持有锁的时候,会阻止其他的事务获取锁,这个时候会造成阻塞等待,那么假如事务一直等待下去,就会一直占用CPU资源,所以,锁等待会有一个超时时间,在InnoDB引擎中,可以通过参数:innodb_lock_wait_timeout查询:

SHOW VARIABLES LIKE &#39;innodb_lock_wait_timeout&#39;; 

默认超时时间是50s,超时后会自动释放锁回滚事务。但是我们想一下,假如事务A在等待事务B释放锁,而事务B又在等待事务A释放锁,这时候就会产生一个等待环路了,而这种情况是无论等待多久都不可能会获取锁成功的,所以是没有必要去等50s的,这种形成等待环路的现象又叫做死锁。

死锁(Dead Lock)

什么是死锁

死锁是指的两个或者两个以上的事务在执行过程中,因为争夺锁资源而造成的一种互相等待的现象。

事务A事务B
BEGIN;
SELECT * FROM test WHERE id=10 FOR UPDATE;

BEGIN;

SELECT * FROM test WHERE id=20 FOR UPDATE;
SELECT * FROM test WHERE id=20 FOR UPDATE;

SELECT * FROM test WHERE id=10 FOR UPDATE;

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
查询出结果

死锁的避免

  • 尽量将长事务拆分成多个小事务
  • 查询时避免没有where条件语句查询,并尽可能使用索引查询
  • 可以的话尽量使用等值查询

锁信息查询

InnoDB在information_schema库下提供了3张表供我们查询并排查事务和锁相关问题。

INNODB_TRX

记录了当前在InnoDB中执行的每个事务的信息,包括事务是否在等待锁、事务何时启动以及事务正在执行的SQL语句(如果有的话)。

列名含义
trx_idInnoDD引擎中的事务的唯一ID
trx_state事务状态:RUNNING, LOCK WAIT, ROLLING BACK,COMMITTING
trx_started事务的开始时间
trx_requested_lock_id等待会务的锁ID,如果trx_state不为LOCK WAIT时,为null
trx_wait_started事务等待开始的时间
trx_weight事务的权重,反映了一个事务修改和锁住的行数,当发生死锁时候,InnoDB会选择该值最小的事务进行回滚
trx_mysql_thread_idMySQL中的线程ID,可以通过SHOW PROCESSLIST查询
trx_query事务运行的sql语句
trx_operation_state事务的当前操作状态,如果没有则为NULL
trx_tables_in_use当前事务中执行的sql语句用到的表数量
trx_tables_locked已经被锁定表的数量(因为用的是行锁,所以虽然显示一张表被锁了,但是可能只是锁定的其中一行或几行,所以其他行还是可以被其他事务访问)
trx_lock_structs当前事务保留的锁数量
trx_lock_memory_bytes当前事务的索结构在内存中的大小
trx_rows_locked当前事务中锁住的大致行数,包括已经被打上删除标记等物理存在的但是对当前事务不可见的数据
trx_rows_modified当前事务修改或者插入的行数
trx_concurrency_tickets并发数,指的是当前事务未结束前仍然可以执行的并发数,可以通过系统变量innodb_concurrency_tickets设置
trx_isolation_level当前事务隔离级别
trx_unique_checks是否为当前事务打开或者关闭唯一约束:0-否1-是
trx_foreign_key_checks是否为当前事务打开或者关闭外键约束:0-否1-是
trx_last_foreign_key_error最后一个外键错误信息,没有则为空
trx_adaptive_hash_latched自适应哈希索引是否被当前事务锁定。在分区自适应哈希索引搜索系统时,单个事务不会锁定整个自适应哈希索引。自适应哈希索引分区由innodb_adaptive_hash_index_parts控制,默认设置为8。
trx_adaptive_hash_timeout是立即放弃自适应哈希索引的搜索latch,还是在来自MySQL的调用中保留它。当没有自适应哈希索引争用时,这个值将保持为零,并且语句会保留latch直到它们完成。在争用期间,它的计数减少到零,并且语句在每一行查找之后立即释放锁存。当自适应哈希索引搜索系统被分区时(由innodb_adaptive_hash_index_parts控制),该值保持为0。
trx_is_read_only当前事务是否只读:0-否1-是
trx_autocommit_non_locking值为1表示这是一条不包含for update和lock in share model的语句,而且是在开启autocommit情况下执行的有且仅有这一条语句,当这列和TRX_IS_READ_ONLY都为1时,InnoDB会优化事务以减少与更改表数据事务的相关开销。

INNODB_LOCKS

记录了事务请求锁但未获得的每个锁的信息和一个事务持有锁但正在阻塞另一个事务的每个锁的信息。

列名含义
lock_id锁的id(虽然LOCK_ID当前包含TRX_ID,但LOCK_ID中的数据格式随时可能更改,不要编写解析LOCK_ID值的应用程序)
lock_trx_id上一张表的事务ID
lock_mode锁的模式: S, X, IS, IX, GAP, AUTO_INC,UNKNOWN
lock_type锁的类型是表锁还是行锁
lock_table被锁住的表
lock_index被锁住的索引,表锁则为NULL
lock_space锁记录的空间id,表锁则为NULL
lock_page事务锁定页的数量,表锁则为NULL
lock_rec事务锁定行的数量,表锁则为NULL
lock_data事务锁定的主键值,表锁则为NULL

INNODB_LOCK_WAITS

记录了锁等待的信息。每个被阻塞的InnoDB事务包含一个或多个行,表示它所请求的锁以及正在阻塞该请求的任何锁。

列名含义
lock_id锁的id(虽然LOCK_ID当前包含TRX_ID,但LOCK_ID中的数据格式随时可能更改,不要编写解析LOCK_ID值的应用程序)
requesting_trx_id申请锁资源的事务ID
requested_lock_id申请的锁的ID
blocking_trx_id阻塞的事务ID
blocking_lock_id阻塞的锁的ID

更多相关免费学习推荐:mysql教程(视频)

以上就是认识什么是锁,MySQL中锁如何解决幻读问题的详细内容,更多请关注 第一PHP社区 其它相关文章!


推荐阅读
  • PHP 数组逆序排列方法及常用排序函数详解 ... [详细]
  • 在数据库事务处理中,InnoDB 存储引擎提供了多种隔离级别,其中 READ COMMITTED 和 REPEATABLE READ 是两个常用的选项。本文详细对比了这两种隔离级别的特点和差异,不仅从理论角度分析了它们对“脏读”和“幻读”的处理方式,还结合实际应用场景探讨了它们在并发控制和性能表现上的不同。特别关注了行锁机制在不同隔离级别下的行为,为开发者选择合适的隔离级别提供了参考。 ... [详细]
  • 开发日志:在插入数据到一张表的同时更新另一张表的技术细节与最佳实践 ... [详细]
  • 总数 | 小规模算法动态规划第3讲:LeetCode 62 不同路径详解 | 从自顶向下到自底向上的动态规划方法分析
    总数 | 小规模算法动态规划第3讲:LeetCode 62 不同路径详解 | 从自顶向下到自底向上的动态规划方法分析 ... [详细]
  • PHP与MySQL的Web应用开发技术深入解析
    PHP与MySQL的Web应用开发技术深入解析 ... [详细]
  • 在实际开发中,连接池是最常使用的数据库管理技术之一。连接池通过创建和管理一组预初始化的数据库连接,使得这些连接可以被多个线程复用,从而显著提高应用程序的性能和资源利用率。本文将详细介绍如何从零开始构建一个自定义的 MySQL 连接池,并将其与 Spring Data 进行集成,以实现高效、可靠的数据库操作。 ... [详细]
  • 源代码是构建网站的基础,涵盖了网站程序的所有代码、文件和目录结构。掌握源代码意味着完全控制网站的所有权。在传统自助建站平台中,由于采用SAAS模式,源代码通常不对外开放,用户实际上每年支付的费用仅是为了使用这些平台提供的服务,而无法获得真正的代码所有权。相比之下,米拓源代码提供了全面的技术细节和实现方法,使开发者能够深入了解并自主定制网站功能,确保了更高的灵活性和安全性。 ... [详细]
  • 本文详细介绍了 PHP 中 `sprintf` 函数的使用方法,并通过具体示例进行说明。例如,使用 `%%` 作为参数时,`%%` 会被替换为 `%`。通过 `echo sprintf($str)` 可以验证这一行为,返回的结果是“测试一下 % 这个参数,会被替换成什么”。此外,文章还探讨了 `sprintf` 函数在格式化字符串中的多种应用场景,包括数字格式化、日期时间处理等,帮助读者全面掌握该函数的使用技巧。 ... [详细]
  • 本文深入探讨了二叉树路径和问题的算法优化方法。具体而言,给定一棵二叉树,需要找出所有从根节点到叶节点的路径,其中各节点值的总和等于指定的目标值。通过详细分析和优化,提出了一种高效的解决方案,并通过多个样例验证了其有效性和性能。 ... [详细]
  • Node.js 配置文件管理方法详解与最佳实践
    本文详细介绍了 Node.js 中配置文件管理的方法与最佳实践,涵盖常见的配置文件格式及其优缺点,并提供了多种实用技巧和示例代码,帮助开发者高效地管理和维护项目配置,具有较高的参考价值。 ... [详细]
  • 二叉树的直径是指树中任意两个叶节点之间最长路径上的节点数量。本文深入解析了计算二叉树直径的算法,并提出了一种优化方法,以提高计算效率和准确性。通过详细的案例分析和性能对比,展示了该优化算法在实际应用中的优势。 ... [详细]
  • vtkGlyph3D 是一种强大的符号化可视化工具,能够将三维数据集中的每个点用预定义的几何图形(如球体或箭头)进行表示。该工具不仅支持自定义符号的方向和缩放比例,还能够在复杂的数据场中突出显示关键特征,从而提高数据的可解释性和可视化效果。通过这种方式,用户可以更直观地理解和分析三维数据集中的重要信息。 ... [详细]
  • 本文详细解析了九度编程平台上的斐波那契数列高效算法挑战(题目编号:1387)。该挑战要求在1秒的时间限制和32兆的内存限制下,设计出高效的斐波那契数列计算方法。通过多种算法的对比和性能分析,本文提供了优化方案,帮助参赛者在限定资源条件下实现高效计算。 ... [详细]
  • 实现Nginx对ThinkPHP URL重写及PATHINFO支持的详细方法解析【PHP开发】
    在PHP后端开发中,实现Nginx对ThinkPHP的URL重写及PATHINFO支持是一项常见的需求。本文详细解析了经过多次尝试和研究,最终找到的一种有效配置方法,能够确保URL_MODERewrite功能正常运行,并提供稳定的服务。此外,文章还探讨了相关配置项的具体作用及其优化建议,帮助开发者更好地理解和应用这些技术。 ... [详细]
  • 如何在Android设备上通过应用程序创建浏览器书签 ... [详细]
author-avatar
hlfk77136
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有