zoukankan      html  css  js  c++  java
  • MySQL锁篇

    一、一条update语句

      

       https://zhuanlan.zhihu.com/p/146968292

    二、MySQL锁介绍

      锁粒度分类:全局锁、表锁和行锁。MySQL Server支持全局锁和表锁,行锁是存储引擎实现的。

      锁功能分类:共享锁、排他锁。加了共享锁的记录,其他事务可以继续加共享锁,但不允许加排他锁 select ... lock in share mode。加了排他锁的记录,不允许加共享锁和排他锁 select ... for update。

      锁实现分类:悲观锁、乐观锁

    三、表级锁

      1. 读/写锁

    #加读锁
    lock table table_name read;
    #加写锁
    lock table table_name write;
    #释放锁
    unlock tables;

       读锁是共享锁,写锁是排他锁

      2. 元数据锁

      事务开始后,执行第一条select语句时,对表结构加锁,在当前事务没有提交之前,除了本事务外,其他事务不允许修改表结构。

      3. 意向锁

      对表中的行数据添加锁时,会额外对整张表添加意向锁。根据原有加锁的类型分别添加意向共享锁意向排他锁

      当有其他事务想要锁表时,只需要判断整张表上是否有意向锁即可知道表内数据是否已被锁定。

      意向锁之间互不影响加锁。当意向锁和普通锁比较时,当成普通锁。

      4. 自增锁

      当列设置了AUTO_INCREMENT后,插入记录会自动添加,插入完成后自动释放,不需要等到事务提交

    四、行级锁(InnoDB)

      InnoDB加行锁是加在索引列上的,如果查询没有用到索引,那么会对全部数据在主键索引上加锁。InnoDB每个页有一个锁对象,对象内部有位图,对某行记录加锁则在位图对应位置标记。

      假设一个表有3百万数据页,每页100条记录。对全表进行更新

      如果对行加锁,则每行产生一个锁对象,假设大小为10字节,则花费3G内存管理锁。

      如果对页加锁,每页的锁信息占用30字节,总花费90M的内存。

      InnoDB行锁按照锁定范围分成:记录锁间隙锁临键锁、插入意向锁

      4.1 记录锁

      锁住一条记录的索引,可以是共享/排他锁。锁是加在索引上的,并且必须是主键索引或者唯一索引否则会转化成临键锁

      如果一条sql没有用到索引,那么会将所有记录的索引都加上锁,相当于锁表。记录锁会阻碍当前读

    #加共享锁
    select ... lock in share mode;
    #加排他锁
    select ... for update;

      4.2 间隙锁

      锁索引区间,不包括端点。间隙锁阻碍其他事务对区间的插入/修改操作但不会阻碍另一个事务对锁区间的查询。包括lock in share mode和 for update。跟记录锁是不同的

      4.3 临键锁(Next-key Locks)

      锁间隙和索引节点,左开右闭。解决幻读问题。比如索引值有3、5、8、10。对应的next-key lock区间为 (最小值,3]、(3,5]、(5,8]、(8,10]、 (10,最大值]。相当于记录锁和间隙锁的结合。

      4.4 测试(本地测试环境 MySQL5.7.26版本RR隔离级别):

      如果唯一索引是组合索引,除非查询条件用到了组合索引的所有列,否则,依然用间隙锁锁定,因为无法确定具体的记录,属于范围查询。

      1. 唯一索引,等值查询,命中记录。譬如select a from test where a = 5 for update;锁住记录5,即对5加了记录锁,阻塞5的插入和删除。测试结果一致

      2. 唯一索引,等值查询,没有命中。譬如select a from test where a = 4 for update;锁住了(3,5)的区间。测试结果一致

      4. 唯一索引,范围查询。譬如select a from test where a between 5 and 8 for update;锁住了(3,10]的区间。测试结果一致,包括删除和插入的测试

      3. 非唯一索引,等值查询,命中记录。譬如select a from test where a = 5 for update;锁住了(3,8)区间,即对5加了临键锁锁住(3,5],对7加间隙锁锁住(5,8)。

        在测试中,阻塞了[3,8)的插入,锁定了记录5的删除。

      4. 非唯一索引,等值查询,没有命中。譬如select a from test where a = 4 for update;锁住了(3,5)区间,即对5加了间隙锁锁住(3,5)。

        在测试中,阻塞了[3,5)的插入,未锁定删除。

      5. 非唯一索引,范围查询。譬如select a from test where a between 5 and 8 for update;锁住了(3,10]区间,即对5、8、10加了临键锁锁住(3,5]、(5,8]、(8,10]。

        在测试中,阻塞了[3,10)的插入,锁定了记录5,8,10的删除。

        示例数据和索引如下:

    mysql> select * from test;
    +------+
    | a    |
    +------+
    |    3 |
    |    5 |
    |    8 |
    |   10 |
    +------+
    4 rows in set (0.00 sec)
    
    mysql> show index from test;
    +-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
    | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
    +-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
    | test  |          1 | idx_a    |            1 | a           | A         |           3 |     NULL | NULL   | YES  | BTREE      |         |               |
    +-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
    1 row in set (0.00 sec)

        事务A的操作:

    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from test where a between 5 and 8 for update;
    +------+
    | a    |
    +------+
    |    5 |
    |    8 |
    +------+
    2 rows in set (0.00 sec)

        事务B插入和删除操作:

    mysql> insert into test values(2);
    Query OK, 1 row affected (0.02 sec)
    mysql> insert into test values(3);
    ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
    mysql> insert into test values(10);
    Query OK, 1 row affected (0.01 sec)
    mysql> delete from test where a = 10;
    ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
    mysql> delete from test where a = 3;
    Query OK, 1 row affected (0.00 sec)

       原博客地址:https://blog.csdn.net/qingsong3333/article/details/78024931。比较奇怪的是非唯一索引锁定区间是左闭右开?还请大神解惑。

      4.5 RR隔离级别下加锁总结

    1. 唯一索引:
      1. 等值查询
        1. 命中记录,加记录锁,然后在主键上也加记录锁
        2. 没有命中,加间隙锁
      2. 范围查询
        1. 没有命中,加临键锁
        2. 命中记录,包含where条件的区间加临键锁,然后在主键上也加记录锁
    2. 非唯一索引:
      1. 等值查询
        1. 命中记录,加间隙锁和临键锁,然后在主键上也加记录锁
        2. 没有命中,加间隙锁
      2. 范围查询
        1. 没有命中,加临键锁
        2. 命中记录,包含where条件的区间加临键锁,然后在主键上也加记录锁
    3. 没用到索引:
      1. 全表主键上加临键锁,如果设置了innodb_locks_unsafe_for_binlog参数,那么MySQL Server层会优化,释放查询条件不包含的锁

      4.6 RC隔离级别下加锁总结

    1. 唯一索引:
      1. 等值查询
        1. 命中记录,加记录锁,然后在主键上也加记录锁
        2. 没有命中,不加锁
      2. 范围查询
        1. 没有命中,不加锁
        2. 命中记录,对命中的记录的索引加记录锁,然后在主键上也加记录锁
    2. 非唯一索引:
      1. 等值查询
        1. 命中记录,加记录锁,然后在主键上也加记录锁
        2. 没有命中,不加锁
      2. 范围查询
        1. 没有命中,不加锁
        2. 命中记录,对命中的记录的索引加记录锁,然后在主键上也加记录锁
    3. 未用到索引:
      1. 在全表主键上加记录锁,返回给MySQL Server层,MySQL Server层做优化,释放查询条件不包含的锁

      RC隔离级别下只使用记录锁。

      4.7 插入意向锁  

      跟表级的意向锁是不同的概念。在索引区间插入数据,会在这个区间产生插入意向锁。插入完成后,会持有插入记录的记录锁。设计的目的是考虑避免在相同索引区间插入时需要加区间锁。

      如果包含唯一索引,会阻塞其他事务相同索引值的插入和删除。如果是非唯一索引, 那么只会阻塞删除。

      一个有趣的现象

      事务A插入数据,获取了插入的记录锁。

    begin;
    Query OK, 0 rows affected (0.00 sec)
    mysql> insert into test values(6);
    Query OK, 1 row affected (0.03 sec)

      事务B同样的操作,同样获得了记录锁,当然这是两条记录,锁是不一样的。

    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    mysql> insert into test values(6);
    Query OK, 1 row affected (0.00 sec)

      事务A继续执行删除操作,因为有两条数据,事务A会尝试获取事务B持有的记录锁,此时事务A阻塞了。

    mysql> delete from test where a = 6;

      如果此时事务B也执行删除操作,同样的,会等待事务A释放记录锁。

    #事务B
    mysql> delete from test where a = 6;
    ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
    
    #事务A
    mysql> delete from test where a = 6;
    Query OK, 1 row affected (4.18 sec)

      产生了死锁。MySQL内部处理,事务B回滚释放了记录锁,事务A执行成功,事务A删除的记录只有自己插入的那一条。

      另外一个产生死锁的例子

      数据和索引如下:

    mysql> select * from t;
    +---+------+------+
    | a | b    | c    |
    +---+------+------+
    | 1 |    3 |    5 |
    | 2 |    4 |    6 |
    | 3 |    5 |    7 |
    +---+------+------+
    3 rows in set (0.00 sec)
    
    mysql> show index from t;
    +-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
    | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
    +-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
    | t     |          0 | PRIMARY  |            1 | a           | A         |           3 |     NULL | NULL   |      | BTREE      |         |               |
    | t     |          1 | b        |            1 | b           | A         |           3 |     NULL | NULL   | YES  | BTREE      |         |               |
    | t     |          1 | c        |            1 | c           | A         |           3 |     NULL | NULL   | YES  | BTREE      |         |               |
    +-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
    3 rows in set (0.07 sec)

       事务A如下查询,除了在辅助索引上加锁之外,还需要到主键索引加锁,加锁顺序为1,2,3。因为每次只处理一条数据,并交由MySQL Service层缓存到结果集,并不是一次加全部锁。

    mysql> select * from t where b > 2 for update;

       事务B如下查询,主键的加锁顺序为3,2,1。

    mysql> select * from test where c < 8 for update;

       因为相反的加锁顺序,此时就会产生死锁。

    五、死锁解决方法

      1. 超时回滚。默认的情况下,InnoDB不会回滚超时引发的错误异常(1205错误码)。通过innodb_rollback_on_timeout设置,默认为OFF即只回滚加锁超时的SQL,设置为ON之后,会回滚整个事务。

      2. 等待图检测。每个事务在请求锁并发生等待时,都会判断等待图中是否存在回路,如果有回路,InnoDB会选择undo log最小的事务进行回滚。

    六、避免死锁

      1. 一致的加锁顺序

      2. 保持事务的轻量

      3. 优化事务执行顺序 

      4. 尽快提交事务释放锁

    人生就像蒲公英,看似自由,其实身不由己。
  • 相关阅读:
    使用 Spring data redis 结合 Spring cache 缓存数据配置
    Spring Web Flow 笔记
    Linux 定时实行一次任务命令
    css js 优化工具
    arch Failed to load module "intel"
    go 冒泡排序
    go (break goto continue)
    VirtualBox,Kernel driver not installed (rc=-1908)
    go运算符
    go iota
  • 原文地址:https://www.cnblogs.com/walker993/p/14538604.html
Copyright © 2011-2022 走看看