MySQL锁机制的原理(2)_两个事务都需要获得对方持有的排他锁才能完成事务 实例-程序员宅基地

关于死锁

上文讲过,MyISAM表锁是deadlock free的,这是因为MyISAM总是一次获得所需的全部锁,要么全部满足,要么等待,因此不会出现死锁。但在InnoDB中,除单个SQL组成的事务外,锁是逐步获得的,这就决定了在InnoDB中发生死锁是可能的。如表20-17所示的就是一个发生死锁的例子。

表20-17                   InnoDB存储引擎中的死锁例子

session_1

session_2

mysql> set autocommit = 0;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from table_1 where where id=1 for update;

...

做一些其他处理...

mysql> set autocommit = 0;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from table_2 where id=1 for update;

...

select * from table_2 where id =1 for update;

因session_2已取得排他锁,等待

做一些其他处理...

 

mysql> select * from table_1 where where id=1 for update;

死锁

在上面的例子中,两个事务都需要获得对方持有的排他锁才能继续完成事务,这种循环锁等待就是典型的死锁。

发生死锁后,InnoDB一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务。但在涉及外部锁,或涉及表锁的情况下,InnoDB并不能完全自动检测到死锁,这需要通过设置锁等待超时参数innodb_lock_wait_timeout来解决。需要说明的是,这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。我们通过设置合适的锁等待超时阈值,可以避免这种情况发生。

通常来说,死锁都是应用设计的问题,通过调整业务流程、数据库对象设计、事务大小,以及访问数据库的SQL语句,绝大部分死锁都可以避免。下面就通过实例来介绍几种避免死锁的常用方法。

(1)在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。在下面的例子中,由于两个session访问两个表的顺序不同,发生死锁的机会就非常高!但如果以相同的顺序来访问,死锁就可以避免。

表20-18        InnoDB存储引擎中表顺序造成的死锁例子

session_1

session_2

mysql> set autocommit=0;

Query OK, 0 rows affected (0.00 sec)

mysql> set autocommit=0;

Query OK, 0 rows affected (0.00 sec)

mysql> select first_name,last_name from actor where actor_id = 1 for update;

+------------+-----------+

| first_name | last_name |

+------------+-----------+

| PENELOPE   | GUINESS   |

+------------+-----------+

1 row in set (0.00 sec)

 
 

mysql> insert into country (country_id,country) values(110,'Test');

Query OK, 1 row affected (0.00 sec)

mysql>  insert into country (country_id,country) values(110,'Test');

等待

 
 

mysql> select first_name,last_name from actor where actor_id = 1 for update;

+------------+-----------+

| first_name | last_name |

+------------+-----------+

| PENELOPE   | GUINESS   |

+------------+-----------+

1 row in set (0.00 sec)

mysql>  insert into country (country_id,country) values(110,'Test');

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

 

(2)在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能。

表20-19        InnoDB存储引擎中表数据操作顺序不一致造成的死锁例子

session_1

session_2

mysql> set autocommit=0;

Query OK, 0 rows affected (0.00 sec)

mysql> set autocommit=0;

Query OK, 0 rows affected (0.00 sec)

mysql> select first_name,last_name from actor where actor_id = 1 for update;

+------------+-----------+

| first_name | last_name |

+------------+-----------+

| PENELOPE   | GUINESS   |

+------------+-----------+

1 row in set (0.00 sec)

 
 

mysql> select first_name,last_name from actor where actor_id = 3 for update;

+------------+-----------+

| first_name | last_name |

+------------+-----------+

| ED         | CHASE     |

+------------+-----------+

1 row in set (0.00 sec)

mysql> select first_name,last_name from actor where actor_id = 3 for update;

等待

 
 

mysql> select first_name,last_name from actor where actor_id = 1 for update;

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

mysql> select first_name,last_name from actor where actor_id = 3 for update;

+------------+-----------+

| first_name | last_name |

+------------+-----------+

| ED         | CHASE     |

+------------+-----------+

1 row in set (4.71 sec)

 

(3)在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时再申请排他锁,因为当用户申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁。具体演示可参见20.3.3小节中的例子。

       (4)前面讲过,在REPEATABLE-READ隔离级别下,如果两个线程同时对相同条件记录用SELECT...FOR UPDATE加排他锁,在没有符合该条件记录情况下,两个线程都会加锁成功。程序发现记录尚不存在,就试图插入一条新记录,如果两个线程都这么做,就会出现死锁。这种情况下,将隔离级别改成READ COMMITTED,就可避免问题,如表20-20所示。

表20-20                     InnoDB存储引擎中隔离级别引起的死锁例子1

session_1

session_2

mysql> select @@tx_isolation;

+-----------------+

| @@tx_isolation  |

+-----------------+

| REPEATABLE-READ |

+-----------------+

1 row in set (0.00 sec)

mysql> set autocommit = 0;

Query OK, 0 rows affected (0.00 sec)

mysql> select @@tx_isolation;

+-----------------+

| @@tx_isolation  |

+-----------------+

| REPEATABLE-READ |

+-----------------+

1 row in set (0.00 sec)

mysql> set autocommit = 0;

Query OK, 0 rows affected (0.00 sec)

当前session对不存在的记录加for update的锁:

mysql> select actor_id,first_name,last_name from actor where actor_id = 201 for update;

Empty set (0.00 sec)

 
 

其他session也可以对不存在的记录加for update的锁:

mysql> select actor_id,first_name,last_name from actor where actor_id = 201 for update;

Empty set (0.00 sec)

因为其他session也对该记录加了锁,所以当前的插入会等待:

mysql> insert into actor (actor_id , first_name , last_name) values(201,'Lisa','Tom');

等待

 
 

因为其他session已经对记录进行了更新,这时候再插入记录就会提示死锁并退出:

mysql> insert into actor (actor_id, first_name , last_name) values(201,'Lisa','Tom');

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

由于其他session已经退出,当前session可以获得锁并成功插入记录:

mysql> insert into actor (actor_id , first_name , last_name) values(201,'Lisa','Tom');

Query OK, 1 row affected (13.35 sec)

 

(5)当隔离级别为READ COMMITTED时,如果两个线程都先执行SELECT...FOR UPDATE,判断是否存在符合条件的记录,如果没有,就插入记录。此时,只有一个线程能插入成功,另一个线程会出现锁等待,当第1个线程提交后,第2个线程会因主键重出错,但虽然这个线程出错了,却会获得一个排他锁!这时如果有第3个线程又来申请排他锁,也会出现死锁。

对于这种情况,可以直接做插入操作,然后再捕获主键重异常,或者在遇到主键重错误时,总是执行ROLLBACK释放获得的排他锁,如表20-21所示。

表20-21                        InnoDB存储引擎中隔离级别引起的死锁例子2

session_1

session_2

session_3

mysql> select @@tx_isolation;

+----------------+

| @@tx_isolation |

+----------------+

| READ-COMMITTED |

+----------------+

1 row in set (0.00 sec)

mysql> set autocommit=0;

Query OK, 0 rows affected (0.01 sec)

mysql> select @@tx_isolation;

+----------------+

| @@tx_isolation |

+----------------+

| READ-COMMITTED |

+----------------+

1 row in set (0.00 sec)

mysql> set autocommit=0;

Query OK, 0 rows affected (0.01 sec)

mysql> select @@tx_isolation;

+----------------+

| @@tx_isolation |

+----------------+

| READ-COMMITTED |

+----------------+

1 row in set (0.00 sec)

mysql> set autocommit=0;

Query OK, 0 rows affected (0.01 sec)

Session_1获得for update的共享锁:

mysql> select actor_id, first_name,last_name from actor where actor_id = 201 for update;

Empty set (0.00 sec)

由于记录不存在,session_2也可以获得for update的共享锁:

mysql> select actor_id, first_name,last_name from actor where actor_id = 201 for update;

Empty set (0.00 sec)

 

Session_1可以成功插入记录:

mysql> insert into actor (actor_id,first_name,last_name) values(201,'Lisa','Tom');

Query OK, 1 row affected (0.00 sec)

   
 

Session_2插入申请等待获得锁:

mysql> insert into actor (actor_id,first_name,last_name) values(201,'Lisa','Tom');

等待

 

Session_1成功提交:

mysql> commit;

Query OK, 0 rows affected (0.04 sec)

   
 

Session_2获得锁,发现插入记录主键重,这个时候抛出了异常,但是并没有释放共享锁:

mysql> insert into actor (actor_id,first_name,last_name) values(201,'Lisa','Tom');

ERROR 1062 (23000): Duplicate entry '201' for key 'PRIMARY'

 
   

Session_3申请获得共享锁,因为session_2已经锁定该记录,所以session_3需要等待:

mysql> select actor_id, first_name,last_name from actor where actor_id = 201 for update;

等待

 

这个时候,如果session_2直接对记录进行更新操作,则会抛出死锁的异常:

mysql> update actor set last_name='Lan' where actor_id = 201;

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

 
   

Session_2释放锁后,session_3获得锁:

mysql> select first_name, last_name from actor where actor_id = 201 for update;

+------------+-----------+

| first_name | last_name |

+------------+-----------+

| Lisa       | Tom       |

+------------+-----------+

1 row in set (31.12 sec)

尽管通过上面介绍的设计和SQL优化等措施,可以大大减少死锁,但死锁很难完全避免。因此,在程序设计中总是捕获并处理死锁异常是一个很好的编程习惯。

如果出现死锁,可以用SHOW INNODB STATUS命令来确定最后一个死锁产生的原因。返回结果中包括死锁相关事务的详细信息,如引发死锁的SQL语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等。据此可以分析死锁产生的原因和改进措施。下面是一段SHOW INNODB STATUS输出的样例:

mysql> show innodb status \G

…….

------------------------

LATEST DETECTED DEADLOCK

------------------------

070710 14:05:16

*** (1) TRANSACTION:

TRANSACTION 0 117470078, ACTIVE 117 sec, process no 1468, OS thread id 1197328736 inserting

mysql tables in use 1, locked 1

LOCK WAIT 5 lock struct(s), heap size 1216

MySQL thread id 7521657, query id 673468054 localhost root update

insert into country (country_id,country) values(110,'Test')

………

*** (2) TRANSACTION:

TRANSACTION 0 117470079, ACTIVE 39 sec, process no 1468, OS thread id 1164048736 starting index read, thread declared inside InnoDB 500

mysql tables in use 1, locked 1

4 lock struct(s), heap size 1216, undo log entries 1

MySQL thread id 7521664, query id 673468058 localhost root statistics

select first_name,last_name from actor where actor_id = 1 for update

*** (2) HOLDS THE LOCK(S):

………

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:

………

*** WE ROLL BACK TRANSACTION (1)

……

小结

本章重点介绍了MySQL中MyISAM表级锁和InnoDB行级锁的实现特点,并讨论了两种存储引擎经常遇到的锁问题和解决办法。

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

(1)共享读锁(S)之间是兼容的,但共享读锁(S)与排他写锁(X)之间,以及排他写锁(X)之间是互斥的,也就是说读和写是串行的。

(2)在一定条件下,MyISAM允许查询和插入并发执行,我们可以利用这一点来解决应用中对同一表查询和插入的锁争用问题。

(3)MyISAM默认的锁调度机制是写优先,这并不一定适合所有应用,用户可以通过设置LOW_PRIORITY_UPDATES参数,或在INSERT、UPDATE、DELETE语句中指定LOW_PRIORITY选项来调节读写锁的争用。

(4)由于表锁的锁定粒度大,读写之间又是串行的,因此,如果更新操作较多,MyISAM表可能会出现严重的锁等待,可以考虑采用InnoDB表来减少锁冲突。

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

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

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

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

l         MySQL的恢复和复制对InnoDB锁机制和一致性读策略也有较大影响。

l         锁冲突甚至死锁很难完全避免。

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

l         尽量使用较低的隔离级别;

l         精心设计索引,并尽量使用索引访问数据,使加锁更精确,从而减少锁冲突的机会;

l         选择合理的事务大小,小事务发生锁冲突的几率也更小;

l         给记录集显示加锁时,最好一次性请求足够级别的锁。比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁;

l         不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会;

l         尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响;

l         不要申请超过实际需要的锁级别;除非必须,查询时不要显示加锁;

l         对于一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_43224306/article/details/82922420

智能推荐

Cadence OrCAD Capture 复制粘贴电路时元器件编号保持问号的方法图文教程_cadence中怎么复制电阻时是问号-程序员宅基地

文章浏览阅读548次,点赞2次,收藏5次。本文简单介绍使用Capture软件复制电路时保持元器件编号保持问号的方法。_cadence中怎么复制电阻时是问号

Python程序员Debug的利器,和Print说再见 | 技术头条-程序员宅基地

文章浏览阅读2.3k次,点赞41次,收藏95次。【导语】程序员每日都在和 debug 相伴。新手程序员需要学习的 debug 手段复杂多样,设置断点、查看变量值……一些网站还专门针对debug撰写了新手教程。老司机们在大型的项目中要 debug 的问题不一样,模块众多、代码超长,面对大型项目的debug之路道阻且长。针对新手和老手程序员会遇到的不同debug问题,本文推荐了两个GitHub上的开源debug工具:PySnooper 和 Be..._python 除了用print检查变量

安装并配置jdk_挂载jdk.iso并安装jdk-程序员宅基地

文章浏览阅读401次。安装jdkjdk下载地址.两种下载方式安装包和压缩包,主要介绍安装包但是也介绍压缩包安装包下载打开此文件进入安装界面,由于已经安装,不在详细演示安装分两个步骤JDK安装打开安装程序后 会有安装路径选择,可自行选择位置安装,但是最好文件夹名带jdk,比如jdk1.8.0_231如过自行选择安装路径,则下一步的jre安装也要选择相同路径示例图如下jre安装上述已经说明环境..._挂载jdk.iso并安装jdk

戴尔INSPIRON 14-7472 DDH40 DDH50 LA-F251P r1.0笔记本图纸_ddh40 la-f251p 图纸-程序员宅基地

文章浏览阅读991次。戴尔INSPIRON 14-7472 DDH40 DDH50 LA-F251P r1.0笔记本图纸 品牌 戴尔 型号 INSPIRON 14-7472 主板版号 DDH40 DDH50 LA-F251P 图纸类型 笔记本电路图 图纸格式 PDF 其他信息 戴尔INSPIRON 14-7472 DDH40 DDH50 LA-F251P r1.0笔记本图纸.pdf Compal Confidential DDH._ddh40 la-f251p 图纸

phpredis手册_php redis 手册-程序员宅基地

文章浏览阅读1.3k次。Skip to contentPull requestsIssuesMarketplaceExploreSign out Watch 513 Star6,474 Fork1,680phpredis/phpredis Code Issues 87 Pull requests 17 InsightsA PHP extension for Redisphpredisredis-clustercclust..._php redis 手册

rtl8821cu wifi调试记录_8821cu wlan-程序员宅基地

文章浏览阅读1.5k次。3188 android5 rtl8821cu wifi调试记录开始遇到的问题:1.drivers/net/wireless/rockchip_wlan/rtl8821cu/os_dep/linux/usb_intf.c:1631:31: fatal error: linux/rfkill-wlan.h: No such file or directory//#include <linux/rfkill-wlan.h>2.drivers/built-in.o: In function_8821cu wlan

随便推点

关于在IDEA中使用Hibernate框架,以及出现的错误总结_idea hibernate插件-程序员宅基地

文章浏览阅读2.1k次,点赞3次,收藏5次。在IDEA中初次创建是没有Hibernate框架的,所以需要引入Hibernate插件,在setting–>plugins中搜索此插件安装即可;安装好插件以后具体的配置信息可以看这篇文章:https://www.cnblogs.com/solverpeng/p/5915823.html关于一些基本的Hibernate配置:<?xml version='1.0' encoding='utf-8'?><!DOCTYPE hibernate-configuration PU_idea hibernate插件

解答c语言的app,你学c语言用的什么app?-程序员宅基地

文章浏览阅读4.2k次。这里简单介绍2个可以直接编译运行C语言的手机APP,分别是C语言编译器和C++编译器,对于大部分简单的程序来说,这2个软件都可以轻松编译并运行,感兴趣的朋友可以尝试一下:01C语言编译器这是一个非常精简的手机C语言编程软件,主要为C语言初学者提供核心的功能,能够随手验证一些小程序,免费、小巧、灵活,下面我简单介绍一下这个软件的安装和使用:1.首先,安装C语言编译器,这个直接在手机应用商店中搜索就行..._有教学c语言的软件吗

darknet环境安装_darkenet 环境安装-程序员宅基地

文章浏览阅读619次。参考:https://www.cnblogs.com/pprp/p/9525508.html下载库文件git clone https://github.com/pjreddie/darknetcd darknet修改MakefileGPU=1 #0或1CUDNN=1 #0或1OPENCV=0 #0或1OPENMP=0DEBUG=0编译make编译时候报错:include/darknet.h:16:23: fatal error: cudnn.h: No such _darkenet 环境安装

vue {{}} 数据渲染出现 undefined问题_vue{{}}undefind-程序员宅基地

文章浏览阅读4.4k次。这应该就是数据异步请求导致的问题吧~例如user:{"id":1,"name":"yrm","des":{"sex":"girl" "age":"19"}}{{user.des.sex}} //报错undefined{{user.des ? use..._vue{{}}undefind

python魔法方法学不懂_Python--魔法方法学习-程序员宅基地

文章浏览阅读436次。1、什么叫魔法方法?魔法方法:Python解释器自动给出默认的,是可以给你的类增加魔力的特殊方法。如果你的对象实现(重载)了这些方法中的某一个,那么这个方法就会在特殊的情况下被 Python 所调用(不重载则会使用默认的)。你可以根据需求,重写这些方法去定义自己想要的行为,而这一切都是自动发生的。魔法方法经常是两个下划线包围来命名的(比如__init__,__del__)函数与方法的区别:魔法方法..._python中typeerror:missing 33 required positional arguments

太棒啦!PyCharm与Jupyter完美融合,Jupytext来啦!-程序员宅基地

文章浏览阅读561次。文末送书!项目作者/Marc Wouts转自/机器之心Jupyter Notebook 真的是让人又爱又失望,在有的场景下它极其便利,但是在很多大模型或复杂项目上,它又无能为力。在 Jup..._pyzmq与jupyter兼容

推荐文章

热门文章

相关标签