• MySQL中的锁机制


    目录

    提出背景

    什么是锁

    锁是基于什么实现的

    锁的分类

    悲观锁

    特点:

    使用场景:

    乐观锁

    特点:

    与悲观锁的对比

    表锁 

    特征

    ​编辑 使用 

    锁表的命令

    查看数据库表锁的命令

    给mylock表上读锁,给book表上写锁

    释放表锁

    表锁分析

    MyISAM表锁

    读锁案例

    写锁案例

    行锁

    特征

    使用

    环境准备

    开启手动提交

    执行不同会话修改操作

    两个SESSION同时,SESSION1 写,SESSION2查看结果

    行锁两个SESSION同时対一条记录进行写操作

    行锁两个SESSION同时对不同记录进行写操作

    行锁分析

    InnoDB锁

    数据库的事务隔离

    InnoDB的行锁模式及加锁方法 

    InnoDB行锁实现方式

    间隙锁(Next-Key锁)


    提出背景

    我们知道,数据也是一种供许多用户共享访问的资源。如何保证数据并发访问的一致性、有效性,是所有数据库必须解决的一个问题,锁的冲突也是影响数据库并发访问性能的一个重要因素。从这一角度来说,锁对于数据库而言就显得尤为重要。

    数据库的锁是为了解决事务的隔离性问题,为了让事务之间相互不影响,每个事务进行操作的时候都会对数据加上一把特有的锁,防止其他事务同时操作数据。如果你想一个人静一静,不被别人打扰,那么请在你的房门上加上一把锁。
    实例:

    在数据库的操作中,有可能会出现数据不一致的问题,一个常见的例子如下:

    A 给 B 转账 100 元
    A 的账户减少 100 元
    B 的账户增加 100 元

    若在 2 后的一瞬间进行查看,可以发现 A 的账户减少了 100 元但是 B 的账户余额却没有任何变化(当然,这种情况只发生在对应课本的对应章节里),数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性,例如: 

    现有两处火车票售票点,同时读取某一趟列车车票数据库中车票余额为 X。两处售票点同时卖出一张车票,同时修改余额为 X-1,写回数据库,这样就造成了实际卖出两张火车票而数据库中的记录却只少了一张。

    产生这种情况的原因是因为两个事务读入同一数据并同时修改,其中一个事务提交的结果破坏了另一个事务提交的结果,导致其数据的修改被丢失,破坏了事务的隔离性,其他类似的问题还有:

    • 丢失或覆盖更新
    • 读脏数据
    • 非重复读

    并发控制要解决的就是这类。

    什么是锁

    为了面对由于并发引来的一些问题,在数据库中有「锁」的概念,即当并发事务同时访问一个资源时,有可能导致数据不一致,因此需要一种机制来将数据访问顺序化,以保证数据库数据的一致性,为了通俗地理解锁的概念,以写作作为比喻:

    在一个博客平台上写作和发布时,对于已经发布的文章可以允许所有人同时阅读,而对于正在修改的文章,我们并不希望读者看到我们修改的过程,同时也不希望其他的编辑对我们正在修改的文章有任何的修改,所以我们将文章暂时地下线进行修改,修改完成后再次上线到页面上。

    数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性需要遵从 ACID 特性:

    • 原子性(Atomicity)

    一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。

    • 一致性(Consistency)

    在事务开始之前和事务结束以后,数据库的完整性没有被破坏。

    • 隔离性(Isolation)

    数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。

    • 持久性(Durability)

    事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

    锁是基于什么实现的

    你现实生活中家里的锁是基于门来实现的,那么数据库的锁又是基于什么实现的呢? 那么我在这里可以告诉你,数据库中的锁时基于索引来实现的。

    • 在Innodb中我们的锁都是作用在索引上面的,当我们的SQL命中索引时,那么锁住的就是命中条件内的索引节点(行锁),
    • 如果没有命中索引的话,那我们锁的就是整个索引树(表锁),如下图一下锁住的是整棵树还是某几个节点,完全取决于你的条件是否有命中到对应的索引节点。

    innodb索引结构图(B+ Tree)

    锁的分类

    • 按锁的粒度划分(即,每次上锁的对象是表,行还是页):表级锁,行级锁,页级锁
    • 按锁的级别划分:共享锁、排他锁
    • 按加锁方式分:自动锁(存储引擎自行根据需要施加的锁)、显式锁(用户手动请求的锁)
    • 按操作划分:DML锁(对数据进行操作的锁)、DDL锁(对表结构进行变更的锁)
    • 最后按使用方式划分:悲观锁、乐观锁

    悲观锁

    悲观锁是基于一种悲观的态度类来防止一切数据冲突,

    • 以一种预防的姿态在修改数据之前把数据锁住;
    • 然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作
    • 直到前面一个人把锁释放后下一个人数据加锁才可对数据进行加锁,然后才可以对数据进行操作,一般数据库本身锁的机制都是基于悲观锁的机制实现的;

    特点:

    可以完全保证数据的独占性和正确性,因为每次请求都会先对数据进行加锁, 然后进行数据操作,最后再解锁,而加锁释放锁的过程会造成消耗,所以性能不高;

    使用场景:

    悲观锁更适用于多写少读的情况。

    互斥锁是被悲观锁的一种

    乐观锁

    乐观锁顾名思义比较乐观,只有在更新数据的时候才会检查这条数据是否被其他线程更新了(这点与悲观锁一样,悲观锁是在读取数据的时候就加锁了)。

    • 如果更新数据时,发现这条数据被其他线程更新了,则此次更新失败。
    • 如果数据未被其他线程更新,则更新成功。
    • 由于乐观锁没有了锁等待,提高了吞吐量,所以乐观锁适合多读少写的场景。

    常见的乐观锁实现方式是:版本号version和CAS(compare and swap)。 

    特点:

    乐观锁是一种并发类型的锁,其本身不对数据进行加锁通而是通过业务实现锁的功能,不对数据进行加锁就意味着允许多个请求同时访问数据,同时也省掉了对数据加锁和解锁的过程,这种方式大大的提高了数据操作的性能; 

    与悲观锁的对比

    • 悲观锁:读取时加锁,更新完释放锁,再此过程中会造成其他线程阻塞,导致吞吐量低,适用于多写场景。
    • 乐观锁:不加锁,只有在更新时验证数据是否被其他线程更新,吞吐量较高,适用于多读场景。

    表锁 

    特征

    表级锁是mysql锁中粒度最大的一种锁,表示当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。

    • 开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
    • 该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。
    • 表锁被大部分的mysql引擎支持,MyISAM和InnoDB都支持表级锁。
    • MyISAM只是支持表锁,因此性能相对Innodb来说相对降低,而Innodb也支持表锁,但是默认的行锁,而且只有在查询或者其他SQL语句通过索引才会使用行锁。
       

     使用 

    1. # 1、创建表
    2. CREATE TABLE `mylock`(
    3. `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
    4. `name` VARCHAR(20)
    5. )ENGINE=MYISAM DEFAULT CHARSET=utf8 COMMENT='测试表锁';
    6. # 2、插入数据
    7. INSERT INTO `mylock`(`name`) VALUES('ZhangSan');
    8. INSERT INTO `mylock`(`name`) VALUES('LiSi');
    9. INSERT INTO `mylock`(`name`) VALUES('WangWu');
    10. INSERT INTO `mylock`(`name`) VALUES('ZhaoLiu');

    锁表的命令

    查看数据库表锁的命令
    1. # 查看数据库表锁的命令
    2. SHOW OPEN TABLES;
    给mylock表上读锁,给book表上写锁
    1. # 给mylock表上读锁,给book表上写锁
    2. LOCK TABLE `mylock` READ, `book` WRITE;
    1. # 查看当前表的状态
    2. mysql> SHOW OPEN TABLES;
    3. +--------------------+------------------------------------------------------+--------+-------------+
    4. | Database | Table | In_use | Name_locked |
    5. +--------------------+------------------------------------------------------+--------+-------------+
    6. | sql_analysis | book | 1 | 0 |
    7. | sql_analysis | mylock | 1 | 0 |
    8. +--------------------+------------------------------------------------------+--------+-------------+
    释放表锁
    1. # 释放给表添加的锁
    2. UNLOCK TABLES;
    3. # 查看当前表的状态
    4. mysql> SHOW OPEN TABLES;
    5. +--------------------+------------------------------------------------------+--------+-------------+
    6. | Database | Table | In_use | Name_locked |
    7. +--------------------+------------------------------------------------------+--------+-------------+
    8. | sql_analysis | book | 0 | 0 |
    9. | sql_analysis | mylock | 0 | 0 |
    10. +--------------------+------------------------------------------------------+--------+-------------+
    表锁分析
    1. mysql> SHOW STATUS LIKE 'table%';
    2. +----------------------------+-------+
    3. | Variable_name | Value |
    4. +----------------------------+-------+
    5. | Table_locks_immediate | 173 |
    6. | Table_locks_waited | 0 |
    7. | Table_open_cache_hits | 5 |
    8. | Table_open_cache_misses | 8 |
    9. | Table_open_cache_overflows | 0 |
    10. +----------------------------+-------+
    11. 5 rows in set (0.00 sec)

    可以通过Table_locks_immediate和Table_locks_waited状态变量来分析系统上的表锁定。具体说明如下:

    • Table_locks_immediate:产生表级锁定的次数,表示可以立即获取锁的查询次数,每立即获取锁值加1。
    • Table_locks_waited:出现表级锁定争用而发生等待的次数(不能立即获取锁的次数,每等待一次锁值加1),此值高则说明存在较严重的表级锁争用情况。

    此外,MyISAM的读写锁调度是写优先,这也是MyISAM不适合作为主表的引擎。因为写锁后,其他线程不能进行任何操作,大量的写操作会使查询很难得到锁,从而造成永远阻塞。

    表锁使用的是一次性锁技术,也就是说,在会话开始的地方使用 lock 命令将后续需要用到的表都加上锁,在表释放前,只能访问这些加锁的表,不能访问其他表,直到最后通过 unlock tables 释放所有表锁。

    除了使用 unlock tables 显示释放锁之外,会话持有其他表锁时执行lock table 语句会释放会话之前持有的锁;会话持有其他表锁时执行 start transaction 或者 begin 开启事务时,也会释放之前持有的锁。

    MyISAM表锁

    MyISAM引擎在执行查询语句SELECT之前,会自动给涉及到的所有表加读锁,在执行增删改之前,会自动给涉及的表加写锁。

    MySQL的表级锁有两种模式:

    • 表共享读锁(Table Read Lock)。
    • 表独占写锁(Table Write Lock)。

    対MyISAM表进行操作,会有以下情况:

    • 対MyISAM表的读操作(加读锁),不会阻塞其他线程対同一表的读操作,但是会阻塞其他线程対同一表的写操作。只有当读锁释放之后,才会执行其他线程的写操作。
    • 対MyISAM表的写操作(加写锁),会阻塞其他线程対同一表的读和写操作,只有当写锁释放之后,才会执行其他线程的读写操作。
       
    读锁案例
    1. 1、打开两个会话,SESSION1为mylock表添加读锁。
    2. # 为mylock表添加读锁
    3. LOCK TABLE `mylock` READ;
    4. 2、打开两个会话,SESSION1是否可以读自己锁的表?是否可以修改自己锁的表?是否可以读其他的表?那么SESSION2呢?
    5. # SESSION1
    6. # 问题1:SESSION1为mylock表加了读锁,可以读mylock表!
    7. mysql> SELECT * FROM `mylock`;
    8. +----+----------+
    9. | id | name |
    10. +----+----------+
    11. | 1 | ZhangSan |
    12. | 2 | LiSi |
    13. | 3 | WangWu |
    14. | 4 | ZhaoLiu |
    15. +----+----------+
    16. 4 rows in set (0.00 sec)
    17. # 问题2:SESSION1为mylock表加了读锁,不可以修改mylock表!
    18. mysql> UPDATE `mylock` SET `name` = 'abc' WHERE `id` = 1;
    19. ERROR 1099 (HY000): Table 'mylock' was locked with a READ lock and can't be updated
    20. # 问题3:SESSION1为mylock表加了读锁,不可以读其他的表!
    21. mysql> SELECT * FROM `book`;
    22. ERROR 1100 (HY000): Table 'book' was not locked with LOCK TABLES
    23. # SESSION2
    24. # 问题1:SESSION1为mylock表加了读锁,SESSION2可以读mylock表!
    25. mysql> SELECT * FROM `mylock`;
    26. +----+----------+
    27. | id | name |
    28. +----+----------+
    29. | 1 | ZhangSan |
    30. | 2 | LiSi |
    31. | 3 | WangWu |
    32. | 4 | ZhaoLiu |
    33. +----+----------+
    34. 4 rows in set (0.00 sec)
    35. # 问题2:SESSION1为mylock表加了读锁,SESSION2修改mylock表会被阻塞,需要等待SESSION1释放mylock表!
    36. mysql> UPDATE `mylock` SET `name` = 'abc' WHERE `id` = 1;
    37. ^C^C -- query aborted
    38. ERROR 1317 (70100): Query execution was interrupted
    39. # 问题3:SESSION1为mylock表加了读锁,SESSION2可以读其他表!
    40. mysql> SELECT * FROM `book`;
    41. +--------+------+
    42. | bookid | card |
    43. +--------+------+
    44. | 1 | 1 |
    45. | 7 | 4 |
    46. | 8 | 4 |
    47. | 9 | 5 |
    48. | 5 | 6 |
    49. | 17 | 6 |
    50. | 15 | 8 |
    51. +--------+------+
    52. 24 rows in set (0.00 sec)
    写锁案例
    1. 1、打开两个会话,SESSION1为mylock表添加写锁。
    2. # 为mylock表添加写锁
    3. LOCK TABLE `mylock` WRITE;
    4. 2、打开两个会话,SESSION1是否可以读自己锁的表?是否可以修改自己锁的表?是否可以读其他的表?那么SESSION2呢?
    5. # SESSION1
    6. # 问题1:SESSION1为mylock表加了写锁,可以读mylock的表!
    7. mysql> SELECT * FROM `mylock`;
    8. +----+----------+
    9. | id | name |
    10. +----+----------+
    11. | 1 | ZhangSan |
    12. | 2 | LiSi |
    13. | 3 | WangWu |
    14. | 4 | ZhaoLiu |
    15. +----+----------+
    16. 4 rows in set (0.00 sec)
    17. # 问题2:SESSION1为mylock表加了写锁,可以修改mylock表!
    18. mysql> UPDATE `mylock` SET `name` = 'abc' WHERE `id` = 1;
    19. Query OK, 1 row affected (0.00 sec)
    20. Rows matched: 1 Changed: 1 Warnings: 0
    21. # 问题3:SESSION1为mylock表加了写锁,不能读其他表!
    22. mysql> SELECT * FROM `book`;
    23. ERROR 1100 (HY000): Table 'book' was not locked with LOCK TABLES
    24. # SESSION2
    25. # 问题1:SESSION1为mylock表加了写锁,SESSION2读mylock表会阻塞,等待SESSION1释放!
    26. mysql> SELECT * FROM `mylock`;
    27. ^C^C -- query aborted
    28. ERROR 1317 (70100): Query execution was interrupted
    29. # 问题2:SESSION1为mylock表加了写锁,SESSION2读mylock表会阻塞,等待SESSION1释放!
    30. mysql> UPDATE `mylock` SET `name` = 'abc' WHERE `id` = 1;
    31. ^C^C -- query aborted
    32. ERROR 1317 (70100): Query execution was interrupted
    33. # 问题3:SESSION1为mylock表加了写锁,SESSION2可以读其他表!
    34. mysql> SELECT * FROM `book`;
    35. +--------+------+
    36. | bookid | card |
    37. +--------+------+
    38. | 1 | 1 |
    39. | 7 | 4 |
    40. | 8 | 4 |
    41. | 9 | 5 |
    42. | 5 | 6 |
    43. | 17 | 6 |
    44. | 15 | 8 |
    45. +--------+------+
    46. 24 rows in set (0.00 sec)

    行锁

    特征

    行锁的是mysql锁中粒度最小的一种锁,因为锁的粒度很小,所以发生资源争抢的概率也最小,并发性能最大,但是也会造成死锁,每次加锁和释放锁的开销也会变大。目前主要是Innodb使用行锁,Innodb也是mysql在5.5.5版本之后默认使用的存储引擎。

    行锁按照使用方式也氛围共享锁(S锁或者读锁)和排它锁(X锁或者写锁)

    行锁按照使用方式也氛围共享锁(S锁或者读锁)和排它锁(X锁或者写锁)

    使用

    环境准备
    1. # 建表语句
    2. CREATE TABLE `test_innodb_lock`(
    3. `a` INT,
    4. `b` VARCHAR(16)
    5. )ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='测试行锁';
    6. # 插入数据
    7. INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(1, 'b2');
    8. INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(2, '3');
    9. INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(3, '4000');
    10. INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(4, '5000');
    11. INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(5, '6000');
    12. INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(6, '7000');
    13. INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(7, '8000');
    14. INSERT INTO `test_innodb_lock`(`a`, `b`) VALUES(8, '9000');
    15. # 创建索引
    16. CREATE INDEX idx_test_a ON `test_innodb_lock`(a);
    17. CREATE INDEX idx_test_b ON `test_innodb_lock`(b);
    开启手动提交
    1. # 开启MySQL数据库的手动提交
    2. mysql> SET autocommit=0;
    3. Query OK, 0 rows affected (0.00 sec)
    执行不同会话修改操作
    两个SESSION同时,SESSION1 写,SESSION2查看结果
    1. # SESSION1
    2. # SESSION1対test_innodb_lock表做写操作,但是没有commit。
    3. # 执行修改SQL之后,查询一下test_innodb_lock表,发现数据被修改了。
    4. mysql> UPDATE `test_innodb_lock` SET `b` = '88' WHERE `a` = 1;
    5. Query OK, 1 row affected (0.00 sec)
    6. Rows matched: 1 Changed: 1 Warnings: 0
    7. mysql> SELECT * FROM `test_innodb_lock`;
    8. +------+------+
    9. | a | b |
    10. +------+------+
    11. | 1 | 88 |
    12. | 2 | 3 |
    13. | 3 | 4000 |
    14. | 4 | 5000 |
    15. | 5 | 6000 |
    16. | 6 | 7000 |
    17. | 7 | 8000 |
    18. | 8 | 9000 |
    19. +------+------+
    20. 8 rows in set (0.00 sec)
    21. # SESSION2
    22. # SESSION2这时候来查询test_innodb_lock表。
    23. # 发现SESSION2是读不到SESSION1未提交的数据的。
    24. mysql> SELECT * FROM `test_innodb_lock`;
    25. +------+------+
    26. | a | b |
    27. +------+------+
    28. | 1 | b2 |
    29. | 2 | 3 |
    30. | 3 | 4000 |
    31. | 4 | 5000 |
    32. | 5 | 6000 |
    33. | 6 | 7000 |
    34. | 7 | 8000 |
    35. | 8 | 9000 |
    36. +------+------+
    37. 8 rows in set (0.00 se
    行锁两个SESSION同时対一条记录进行写操作
    1. # SESSION1 対test_innodb_lock表的`a`=1这一行进行写操作,但是没有commit
    2. mysql> UPDATE `test_innodb_lock` SET `b` = '99' WHERE `a` = 1;
    3. Query OK, 1 row affected (0.00 sec)
    4. Rows matched: 1 Changed: 1 Warnings: 0
    5. # SESSION2 也对test_innodb_lock表的`a`=1这一行进行写操作,但是发现阻塞了!!!
    6. # 等SESSION1执行commit语句之后,SESSION2的SQL就会执行了
    7. mysql> UPDATE `test_innodb_lock` SET `b` = 'asdasd' WHERE `a` = 1;
    8. ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
    行锁两个SESSION同时对不同记录进行写操作
    1. # SESSION1 対test_innodb_lock表的`a`=6这一行进行写操作,但是没有commit
    2. mysql> UPDATE `test_innodb_lock` SET `b` = '8976' WHERE `a` = 6;
    3. Query OK, 1 row affected (0.00 sec)
    4. Rows matched: 1 Changed: 1 Warnings: 0
    5. # SESSION2 対test_innodb_lock表的`a`=4这一行进行写操作,没有阻塞!!!
    6. # SESSION1和SESSION2同时对不同的行进行写操作互不影响
    7. mysql> UPDATE `test_innodb_lock` SET `b` = 'Ringo' WHERE `a` = 4;
    8. Query OK, 1 row affected (0.00 sec)
    9. Rows matched: 1 Changed: 1 Warnings: 0
    行锁分析
    1. mysql> SHOW STATUS LIKE 'innodb_row_lock%';
    2. +-------------------------------+--------+
    3. | Variable_name | Value |
    4. +-------------------------------+--------+
    5. | Innodb_row_lock_current_waits | 0 |
    6. | Innodb_row_lock_time | 124150 |
    7. | Innodb_row_lock_time_avg | 31037 |
    8. | Innodb_row_lock_time_max | 51004 |
    9. | Innodb_row_lock_waits | 4 |
    10. +-------------------------------+--------+
    11. 5 rows in set (0.00 sec)

    対各个状态量的说明如下:

    • Innodb_row_lock_current_waits:当前正在等待锁定的数量。
    • Innodb_row_lock_time:从系统启动到现在锁定总时间长度(重要)。
    • Innodb_row_lock_time_avg:每次等待所花的平均时间(重要)。
    • Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间。
    • Innodb_row_lock_waits:系统启动后到现在总共等待的次数(重要)。
       

    尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手制定优化策略。 

    InnoDB锁

    InnoDB与MyISAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁。行级锁与表级锁本来就有许多不同之处,另外,事务的引入也带来了一些新问题。

    数据库的事务隔离

    在并发事务处理带来的问题中,“更新丢失”通常应该是完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。

    “脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。数据库实现事务隔离的方式,基本可以分为以下两种。

    • 一种是在读取数据前,对其加锁,阻止其他事务对数据进行修改。
    • 另一种是不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度,好像是数据库可以提供同一数据的多个版本,因此,这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC或MCC),也经常称为多版本数据库。

    为了解决“隔离”与“并发”的矛盾,ISO/ANSI SQL92定义了4个事务隔离级别,每个级别的隔离程度不同,允许出现的副作用也不同,应用可以根据自己的业务逻辑要求,通过选择不同的隔离级别来平衡 “隔离”与“并发”的矛盾。下表很好地概括了这4个隔离级别的特性。

    InnoDB的行锁模式及加锁方法 

    • 共享锁(s):又称读锁。允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
    • 排他锁(X):又称写锁。允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。

    对于共享锁大家可能很好理解,就是多个事务只能读数据不能改数据。

    对于排他锁大家的理解可能就有些差别,我当初就犯了一个错误,以为排他锁锁住一行数据后,其他事务就不能读取和修改该行数据,其实不是这样的。排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在其上加其他的锁。mysql InnoDB引擎默认的修改数据语句:update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select …for update语句,加共享锁可以使用select … lock in share mode语句。所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制

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

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

     

    如果一个事务请求的锁模式与当前的锁兼容,InnoDB就请求的锁授予该事务;反之,如果两者两者不兼容,该事务就要等待锁释放。

    意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁。

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

    1. 共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。
    2. 排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE。

     用SELECT … IN SHARE MODE获得共享锁,主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操作。但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用SELECT… FOR UPDATE方式获得排他锁

    InnoDB行锁实现方式

    InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!

    (1)在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁。

    1. mysql> create table tab_no_index(id int,name varchar(10)) engine=innodb;
    2. Query OK, 0 rows affected (0.15 sec)
    3. mysql> insert into tab_no_index values(1,'1'),(2,'2'),(3,'3'),(4,'4');
    4. Query OK, 4 rows affected (0.00 sec)
    5. Records: 4 Duplicates: 0 Warnings: 0

    在上面的例子中,看起来session_1只给一行加了排他锁,但session_2在请求其他行的排他锁时,却出现了锁等待!。当我们给其增加一个索引后,InnoDB就只锁定了符合条件的行,如下例所示:

    创建tab_with_index表,id字段有普通索引:

    1. mysql> create table tab_with_index(id int,name varchar(10)) engine=innodb;
    2. mysql> alter table tab_with_index add index id(id);

     

    (2)由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。应用设计的时候要注意这一点。

    在下面的例子中,表tab_with_index的id字段有索引,name字段没有索引:

    1. mysql> alter table tab_with_index drop index name;
    2. Query OK, 4 rows affected (0.22 sec) Records: 4 Duplicates: 0
    3. Warnings: 0
    4. mysql> insert into tab_with_index values(1,'4');
    5. Query OK, 1 row affected (0.00 sec)
    6. mysql> select * from tab_with_index where id = 1;

     

    InnoDB存储引擎使用相同索引键的阻塞例子 

     

    (3)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。

    在下面的例子中,表tab_with_index的id字段有主键索引,name字段有普通索引:

    1. mysql> alter table tab_with_index add index name(name);
    2. Query OK, 5 rows affected (0.23 sec) Records: 5 Duplicates: 0
    3. Warnings: 0

    InnoDB存储引擎的表使用不同索引的阻塞例子

    (4)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决 定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突 时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。

    比如,在tab_with_index表里的name字段有索引,但是name字段是varchar类型的,检索值的数据类型与索引字段不同,虽然MySQL能够进行数据类型转换,但却不会使用索引,从而导致InnoDB使用表锁。通过用explain检查两条SQL的执行计划,我们可以清楚地看到了这一点。

    1. mysql> explain select * from tab_with_index where name = 1 \G
    2. mysql> explain select * from tab_with_index where name = '1' \G

    间隙锁(Next-Key锁)

    当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁 (Next-Key锁)。
    举例来说,假如emp表中只有101条记录,其empid的值分别是 1,2,…,100,101,下面的SQL:

    Select * from  emp where empid > 100 for update;
    

    是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。

    InnoDB使用间隙锁的目的,一方面是为了防止幻读,以满足相关隔离级别的要求,对于上面的例子,要是不使 用间隙锁,如果其他事务插入了empid大于100的任何记录,那么本事务如果再次执行上述语句,就会发生幻读;另外一方面,是为了满足其恢复和复制的需 要。

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

    还要特别说明的是,InnoDB除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁!下面这个例子假设emp表中只有101条记录,其empid的值分别是1,2,……,100,101。

    InnoDB存储引擎的间隙锁阻塞例子

    对于MyISAM的表锁,主要讨论了以下几点:

    • (1)共享读锁(S)之间是兼容的,但共享读锁(S)与排他写锁(X)之间,以及排他写锁(X)之间是互斥的,也就是说读和写是串行的。
    • (2)在一定条件下,MyISAM允许查询和插入并发执行,我们可以利用这一点来解决应用中对同一表查询和插入的锁争用问题。
    • (3)MyISAM默认的锁调度机制是写优先,这并不一定适合所有应用,用户可以通过设置LOW_PRIORITY_UPDATES参数,或在INSERT、UPDATE、DELETE语句中指定LOW_PRIORITY选项来调节读写锁的争用。
    • (4)由于表锁的锁定粒度大,读写之间又是串行的,因此,如果更新操作较多,MyISAM表可能会出现严重的锁等待,可以考虑采用InnoDB表来减少锁冲突。

    对于InnoDB表,本文主要讨论了以下几项内容:

    • (1)InnoDB的行锁是基于索引实现的,如果不通过索引访问数据,InnoDB会使用表锁。

    • (2)介绍了InnoDB间隙锁(Next-key)机制,以及InnoDB使用间隙锁的原因。

    在不同的隔离级别下,InnoDB的锁机制和一致性读策略不同。

    在了解InnoDB锁特性后,用户可以通过设计和SQL调整等措施减少锁冲突和死锁,包括:

    • 尽量使用较低的隔离级别; 精心设计索引,并尽量使用索引访问数据,使加锁更精确,从而减少锁冲突的机会;
    • 选择合理的事务大小,小事务发生锁冲突的几率也更小;
    • 给记录集显式加锁时,最好一次性请求足够级别的锁。比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁;
    • 不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会;
    • 尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响; 不要申请超过实际需要的锁级别;除非必须,查询时不要显示加锁;
    • 对于一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能。

     

  • 相关阅读:
    modesim verilog仿真验证基本流程(新建工程方式)
    算法竞赛入门【码蹄集新手村600题】(MT1151-1200)
    威胁的数量、复杂程度和扩散程度不断上升
    本地上传文件到hadoop的hdfs文件系统里
    19、Flink 的Table API 和 SQL 中的自定义函数及示例(3)
    常见面试题之计算机网络
    新一轮SocialFi浪潮来袭,Atem Network 再次打响注意力争夺战
    SpringBoot2.7.3 动态数据数据源以及多数据源自动配置
    terminal下环境不统一导致的程序报错(powersell改cmd)
    好看好玩的韩剧电视- 厄运的恋人
  • 原文地址:https://blog.csdn.net/m0_62807361/article/details/132798842