MySQL锁机制

Posted siweiyidu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL锁机制相关的知识,希望对你有一定的参考价值。

mysql锁机制

MySQL都有什么锁

MySQL有三种锁的级别:页级、表级、行级。

  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

一. get_lock 锁机制的分析

这个锁是应用程序级别的,在不同的mysql会话之间使用。它只是个名字锁

  • 语句

开启一个以key为名的锁

select  GET_LOCK(‘key‘, timeOut) from tableName; 	# key:自定义字符串 timeout:超时时间

关闭该Key的锁

select RELEASE_LOCK(‘key‘) from tableName;
  • 概述
  1. get_lock会按照key来加锁,别的客户端再以同样的key加锁时就加不了了,处于等待状态。
  2. 当调用release_lock来释放上面加的锁或客户端断线了,上面的锁才会释放,其它的客户端才能进来。
  • 例子
  1. # 加锁
  2. select GET_LOCK(‘key‘, 100) from user;
  3. # 解锁
  4. select RELEASE_LOCK(‘key‘) from user;

二.行级锁

由于InnoDB预设是Row-Level Lock(行级锁),所以只有「明确」的指定主键

共享锁:

SELECT * FROM tableName  LOCK IN SHARE MODE;

排它锁:

SELECT * FROM tableName FOR UPDATE;

三.死锁

所谓死锁<DeadLock>: 是指两个或两个以上的进程在执行过程中,

因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.

此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等竺的进程称为死锁进程.

表级锁不会产生死锁.所以解决死锁主要还是针对于最常用的InnoDB.

死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。

那么对应的解决死锁问题的关键就是:让不同的session加锁有次序

在information_schema下面有三张表:INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS,通过这三张表,可以更简单地监控当前的事务并分析可能存在的问题。

  1. trx_id:InnoDB存储引擎内部唯一的事物ID
  2. trx_status:当前事务的状态
  3. trx_status:事务的开始时间
  4. trx_requested_lock_id:等待事务的锁ID
  5. trx_wait_started:事务等待的开始时间
  6. trx_weight:事务的权重,反应一个事务修改和锁定的行数,当发现死锁需要回滚时,权重越小的值被回滚
  7. trx_mysql_thread_id:MySQL中的进程ID,与show processlist中的ID值相对应
  8. trx_query:事务运行的SQL语句

没有提交事务的解决办法

  1.  
  2. 查看当前连接Id(线程Id)
  3. select connection_id();
  4.  
  5. 查询 正在执行(未提交)的事务:
  6. SELECT trx_started,trx_mysql_thread_id FROM information_schema.INNODB_TRX;--这里只查了线程ID和开始时间
  7. SELECT * FROM information_schema.INNODB_TRXG
  8.  
  9. 通过查看events_statements_current表可看到每一个session正在执行的sql,哪怕它依旧执行完成了,只是没有提交。
  10. 这里可以看到执行的SQL语句
  11. select* from performance_schema.events_statements_currentG
  12.  
  13. 查看正在锁的事务
  14. SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
  15.  
  16. 查看等待锁的事务
  17. SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
  18.  
  19. 获取所有处理线程
  20. show processlist;
  21. show full processlist;
  22.  
  23. 杀死指定线程的Id
  24. kill Id
  25.  

经过我好几天反复的测试发现

php(测试版本PHP 7.2.9)的pdo中连接MySQL时发生死锁时后面执行的进程返回Null,事务自动回滚, 不会造成事务僵死.

但是在MySQL-Client(MySQL测试版本5.6.41)中执行,死锁会造成事务僵死.

比如在PHP中我用PDO操作

  1. 我发送一个请求A首先开启事务,查询锁定到了UID1用户的资产,然后sleep(5),去所动UID2的资产
  2. 我发送一个请求B开启事务,查询锁定了UID2用户的资产,然后sleep(5),去所动UID1的资产
  3. 这时候A正确返回,而B的第二个查询查询则返回了一个null
  4. 然后我继续请求,开启一个事务无查询锁定UID1和UID2的资产,没有任何问题.

但是在MySQL-Client中,像上面操作会提示Deadlock found when trying to get lock; try restarting transaction

并且在随后的操作中,在其他窗口包括PHP中取尝试查询锁定该用户,都会一直卡住很久然后提示Deadlock found when trying to get lock; try restarting transaction

不论隔了多久,再次尝试开启事务锁定UID1和UID2都会这个样子.

当然以上测试我都没有commit或rollback过.当然这是因为我一直没有关闭事务,一直占用着这两个用户.实际我们用的时候不管成功还是失败我们都要提交一下事务.

如果一个连接中开启事务切未显示提交或回滚,在不考虑其他因素的前提下,确实应该是只有在连接断开的时候才会回滚。

如果出现锁等待,则是根据innodb_rollback_on_timeout参数进行下一步动作,如果此参数是off,则会回滚上一条语句,不会提交或回滚,直到显示提交或回滚(事务一直存在,直到连接断开)。

如果此参数是on,在超时后会回滚整个事务。

例如:

将MySQL-client开两个窗口

执行顺序

  1. 执行下面两个两个部分的begin
  2. 执行下面两个第一部分和第二部分的第一个SQL和第二个SQL(按顺序)即两者的第一个for udpate 和update操作.
  3. 执行SELECT trx_started,trx_mysql_thread_id FROM information_schema.INNODB_TRX;可以看到有两个事务
  4. 执行下面两个第一部分和第二部分的第二个SQL和第二个SQL(按顺序)即两者的第二个for update
  5. 执行SELECT trx_started,trx_mysql_thread_id FROM information_schema.INNODB_TRX;可以看到只有一个事务存活了.
  6. 执行最后一句,commit.
  7. 执行SELECT trx_started,trx_mysql_thread_id FROM information_schema.INNODB_TRX;发现没有任何事务了.

这时候会发现,MySQL返回错误信息

Deadlock found when trying to get lock; try restarting transaction

执行第一部分SQL

  1. begin
  2.  
  3. select `bac_balance` from user_finance where uid=1 for update
  4. update `user_finance` set `bac_balance`=`bac_balance`+100 where uid=1;
  5.  
  6. select `bac_balance` from user_finance where uid=2 for update
  7.  
  8. commit
  9.  

第二部分SQL

  1. begin
  2.  
  3. select `bac_balance` from user_finance where uid=2 for update
  4. update `user_finance` set `bac_balance`=`bac_balance`+100 where uid=2;
  5.  
  6.  
  7. select `bac_balance` from user_finance where uid=1 for update
  8.  
  9. commit

有人说, 并不会导致死锁等待, 而是innodb会将执行自动回滚.

InnoDB会在死锁的时候自动回滚影响小的事务(我的测试结果是回滚后面执行的事务).

但是我们在开发的时候肯定要避免死锁这种情况,

上面的运行结果 第一部分运行成功.而第二部分提示死锁且事务回滚.

这是因为,在第一部分的最后一句执行锁UID2的时候会一直等待,等到innodb_lock_wait_timeout设置的时间超时,

而这时候立刻执行第二部分的最后一句,去锁UID2,会马上返回死锁提示,并且回滚事务.所以第一部分成功了.

如果成功的这个事务不提交的话,连接关闭的时候会回滚事务.

工作中遇到的案例

实际上有上百个撮合引擎, 这里假设有两个A&B错误引擎

假设平台对UID 1,2,3 用户进行进行撮合.两个撮合引擎同时运行.

  • A撮合引擎: 从UID为1的用户扣除100USDT,分别为UID2和UID3的用户加20USDT.
  • B撮合引擎: 从UID为3的用户扣除10BTC,分别为UID1和UID2的用户各加5BTC.

理论上,瞬间就会形成死锁(同时开始资产操作是有可能的);

  1. A撮合首先对UID1用户的资产进行select *from finance where uid=1 for update加锁
  2. 同时B撮合也对UID3用户的资产进行select *from finance where uid=3 for update加锁
  3. 这时候A撮合继续开始加锁UID2和UID3,B撮合继续加锁UID1和UID2
  4. 这时候A撮合发现UID3已被锁定,所以会等待B执行完;B撮合发现UID1被锁定,等待A运行完.
  5. 这时候撮合A和撮合B就会进入锁等待状态, 相互等待对方的锁执行完毕.形成死锁

上面这个情况是比较常见的,当时我们想到了两种方案.

    1. 在指定for update 加锁时,对加锁用户进行排序.

从大到小或从小到大都可以.但是要所有撮合都保持顺序一致.这样在加锁时就不会出现反方向加锁导致冲突的情况.

比如,我从1-10加锁,你从10-1加锁,必然会死锁.而我们都从1-10 加锁的话.必然不会有这种情况

    1. 在指定for update 加锁时使用in()将所有要加锁的用户,一次性加锁.

Select * from finance where id in (1,2,3) for update

在in里面的列表值mysql是会自动从小到大排序,加锁也是一条条从小到大加的锁

百度来的案例

泽锦

https://www.cnblogs.com/zejin2008/p/5262751.html

案例一:

  1. 需求:将投资的钱拆成几份随机分配给借款人。
  2.  
  3. 起初业务程序思路是这样的:
  4.  
  5. 投资人投资后,将金额随机分为几份,然后随机从借款人表里面选几个,然后通过一条条select for update 去更新借款人表里面的余额等。
  6.  
  7.  
  8.  
  9. 抽象出来就是一个session通过for循环会有几条如下的语句:
  10.  
  11. Select * from xxx where id=‘随机id‘ for update
  12.  
  13.  
  14.  
  15. 基本来说,程序开启后不一会就死锁。
  16.  
  17. 这可以是说最经典的死锁情形了。
  18.  
  19.  
  20.  
  21. 例如两个用户同时投资,A用户金额随机分为2份,分给借款人1,2
  22.  
  23. B用户金额随机分为2份,分给借款人2,1
  24.  
  25. 由于加锁的顺序不一样,死锁当然很快就出现了。
  26.  
  27.  
  28.  
  29. 对于这个问题的改进很简单,直接把所有分配到的借款人直接一次锁住就行了。
  30.  
  31. Select * from xxx where id in (xx,xx,xx) for update
  32.  
  33. in里面的列表值mysql是会自动从小到大排序,加锁也是一条条从小到大加的锁
  34.  
  35.  
  36. 例如(以下会话id为主键):
  37.  
  38. Session1:
  39.  
  40. mysql> select * from t3 where id in (8,9) for update;
  41.  
  42. +----+--------+------+---------------------+
  43.  
  44. | id | course | name | ctime |
  45.  
  46. +----+--------+------+---------------------+
  47.  
  48. | 8 | WA | f | 2016-03-02 11:36:30 |
  49.  
  50. | 9 | JX | f | 2016-03-01 11:36:30 |
  51.  
  52. +----+--------+------+---------------------+
  53. rows in set (0.04 sec)
  54.  
  55.  
  56.  
  57.  
  58.  
  59. Session2:
  60.  
  61. select * from t3 where id in (10,8,5) for update;
  62.  
  63. 锁等待中……
  64.  
  65. 其实这个时候id=10这条记录没有被锁住的,但id=5的记录已经被锁住了,锁的等待在id=8的这里。
  66.  
  67.  
  68.  
  69. 不信请看
  70.  
  71. Session3:
  72.  
  73. mysql> select * from t3 where id=5 for update;
  74.  
  75. 锁等待中
  76.  
  77.  
  78.  
  79. Session4:
  80.  
  81. mysql> select * from t3 where id=10 for update;
  82.  
  83. +----+--------+------+---------------------+
  84.  
  85. | id | course | name | ctime |
  86.  
  87. +----+--------+------+---------------------+
  88.  
  89. | 10 | JB | g | 2016-03-10 11:45:05 |
  90.  
  91. +----+--------+------+---------------------+
  92. row in set (0.00 sec)
  93.  
  94.  
  95.  
  96. 在其它session中id=5是加不了锁的,但是id=10是可以加上锁的。

案例2:

在开发中,经常会做这类的判断需求:根据字段值查询(有索引),如果不存在,则插入;否则更新。

  1.  
  2. 以id为主键为例,目前还没有id=22的行
  3.  
  4. Session1:
  5.  
  6. select * from t3 where id=22 for update;
  7.  
  8. Empty set (0.00 sec)
  9.  
  10.  
  11.  
  12. session2:
  13.  
  14. select * from t3 where id=23 for update;
  15.  
  16. Empty set (0.00 sec)
  17.  
  18.  
  19.  
  20. Session1:
  21.  
  22. insert into t3 values(22,‘ac‘,‘a‘,now());
  23.  
  24. 锁等待中……
  25.  
  26.  
  27.  
  28. Session2:
  29.  
  30. insert into t3 values(23,‘bc‘,‘b‘,now());
  31.  
  32. ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
  33.  
  34.  
  35. 当对存在的行进行锁的时候(主键),mysql就只有行锁。
  36.  
  37. 当对未存在的行进行锁的时候(即使条件为主键),mysql是会锁住一段范围(有gap锁)
  38.  
  39.  
  40.  
  41.  
  42.  
  43. 锁住的范围为:
  44.  
  45. (无穷小或小于表中锁住id的最大值,无穷大或大于表中锁住id的最小值)
  46.  
  47.  
  48.  
  49. 如:如果表中目前有已有的id为(11 , 12)
  50.  
  51. 那么就锁住(12,无穷大)
  52.  
  53. 如果表中目前已有的id为(11 , 30)
  54.  
  55. 那么就锁住(11,30)
  56.  
  57.  
  58.  
  59. 对于这种死锁的解决办法是:
  60.  
  61. insert into t3(xx,xx) on duplicate key update `xx`=‘XX‘;
  62.  
  63.  
  64.  
  65. 用mysql特有的语法来解决此问题。因为insert语句对于主键来说,插入的行不管有没有存在,都会只有行锁。
  66.  
  67.  
  68.  
  69.  

案例3:

直接上情景:

  1. mysql> select * from t3 where id=9 for update;
  2.  
  3. +----+--------+------+---------------------+
  4.  
  5. | id | course | name | ctime |
  6.  
  7. +----+--------+------+---------------------+
  8.  
  9. | 9 | JX | f | 2016-03-01 11:36:30 |
  10.  
  11. +----+--------+------+---------------------+
  12. row in set (0.00 sec)
  13.  
  14.  
  15.  
  16. Session2:
  17.  
  18. mysql> select * from t3 where id<20 for update;
  19.  
  20. 锁等待中
  21.  
  22.  
  23.  
  24. Session1:
  25.  
  26. mysql> insert into t3 values(7,‘ae‘,‘a‘,now());
  27.  
  28. ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

转载于:https://my.oschina.net/chinaliuhan/blog/3065290

以上是关于MySQL锁机制的主要内容,如果未能解决你的问题,请参考以下文章

Mysql的锁机制与PHP文件锁处理高并发简单思路

Mysql的锁机制与PHP文件锁处理高并发简单思路

mysql 锁机制

mysql锁机制详解

MySQL锁机制

MySQL InnoDB锁机制