关于死锁
1、问题
应用经常出现如下日志
2、死锁定义
指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
死锁是事务系统中客观存在的事实,你的应该在设计上必须应该考虑处理死锁。一些业务系统可以从头重试事务。
2.1 关于共享锁和排它锁
Innodb存储引擎实现了如下2种标准的行级锁:
共享锁(S lock),允许事务读取一行数据。
排它锁(X lock),允许事务删除或者更新一行数据。若事务T获取了数据对象r的排它锁,则只允许T读取和修改r
锁兼容:当一个事务获取了行r的共享锁,那么另外一个事务也可以立即获取行r的共享锁,因为读取并未改变行r的数据,这种情况就是锁兼容。
锁不兼容:当一个事务获取了行r的共享锁或者排它锁,其他事务想获得行r的排它锁,则它必须等待事务释放行r上的共享锁或者排它锁释放,这种情况就是锁不兼容
二者兼容性如下表格所示:
排它锁和共享锁的兼容性 | X排它锁 | S共享锁 |
X排它锁 | 冲突 | 冲突 |
S共享锁 | 冲突 | 兼容 |
简单的说,只有共享锁之间是兼容的
- 什么情况下应用排它锁?
在更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁。或者使用select ...for update语句
- 什么情况下应用共享锁?
加共享锁可以使用select ... lock in share mode语句
注:select不会加任何锁
3、排它锁锁产生过程测试
测试之前,先简单说明下如下三个表的用途
innodb_trx ## 当前运行的所有事务
innodb_locks ## 当前出现的锁
innodb_lock_waits ## 锁等待的对应关系
3.1 建立测试数据
创建表
create table tx1 (id int primary key , c1 varchar(20), c2 varchar(30)) engine=innodb default charset = utf8;
插入数据
insert into tx1 values(1,'aaaa','aaaaa2'); insert into tx1 values(2,'bbbb','bbbbb2');
3.2 模拟
第一步
start transaction;
update tx1 set c1='heyf',c2='heyf' where id=1;
这时候使用如下命令,可以看到有一个事务在执行---这个表有事务不代表一定是锁表,只是代表有一个事务在执行。
SELECT * FROM information_schema.INNODB_TRX\G;
由于没有锁等待,所以使用如下两条命令都查不出数据
SELECT * FROM information_schema.INNODB_locks; SELECT * FROM information_schema.INNODB_lock_waits;
第二步
打开另一个session,执行
update tx1 set c1='yyyyy',c2='yyyyy' where id=1;
输入命令的界面一直这样:
然后过了一会,就显示超时
3.3 分析
在显示超时之前,看INNODB_TRX表,如下
注:(显示超时之后,这个事务就消失了,对,如果没有开启事务控制,即在前面执行start transaction命令,那么超时后就会消失)
每个字段含义具体解释如下:
mysql> SELECT * FROM information_schema.INNODB_TRX\G; *************************** 1. row *************************** trx_id: 2014137 ##第2个事务 trx_state: LOCK WAIT ## 处于等待状态,事务执行的状态, 允许的值:RUNNING, LOCK WAIT, ROLLING BACK, and COMMITTING. trx_started: 2016-11-08 16:47:48 ##事务开始时间 trx_requested_lock_id: 2014137:30721:3:2 ##请求的锁ID,事务当前等待的,如果TRX_STATE 是lock_wait;否则就是NULL. 得到信息关于lock,使用LOCK_ID和INNODB_LOCKS表关联 trx_wait_started: 2016-11-08 16:52:05 ##当事务开始等待锁的时间, 如果TRX_STATE is LOCK WAIT; 否则为空 trx_weight: 2 trx_mysql_thread_id: 118544 ##线程 ID,MySQL thread ID,得到细节关于thread, 使用这个列和NFORMATION_SCHEMA PROCESSLIST table的ID进行关联 trx_query: update tx1 set c1='yyyyy',c2='yyyyy' where id=1 ##事务执行的语句 trx_operation_state: starting index read #事务的当前操作 如果有的话 否则为NULL trx_tables_in_use: 1 ##需要用到1个表 trx_tables_locked: 1 ##事务拥有多少个锁 trx_lock_structs: 2 trx_lock_memory_bytes: 360 #事务锁住的内存大小(B) trx_rows_locked: 1 ##事务锁住的行数 trx_rows_modified: 0 ##事务更改的行数 trx_concurrency_tickets: 0 trx_isolation_level: REPEATABLE READ #事务隔离级别 trx_unique_checks: 1 #是否唯一性检查 trx_foreign_key_checks: 1 #是否外键检查 trx_last_foreign_key_error: NULL #最后的外键错误 trx_adaptive_hash_latched: 0 trx_adaptive_hash_timeout: 10000 trx_is_read_only: 0 trx_autocommit_non_locking: 0 |
再看看INNODB_locks
此表具体解释如下:
a) lock_id:锁的id以及被锁住的空间id编号、页数量、行数量 b) lock_trx_id:锁的事务id。 c) lock_mode:锁的模式。 d) lock_type:锁的类型,表锁还是行锁,行锁值是RECORD,表锁值是TABLE e) lock_table:要加锁的表。 f) lock_index:锁的索引。 g) lock_space:innodb存储引擎表空间的id号码 h) lock_page:被锁住的页的数量,如果是表锁,则为null值。 i) lock_rec:被锁住的行的数量,如果表锁,则为null值。 j) lock_data:被锁住的行的主键值,如果表锁,则为null值。 |
由上面的信息,可以得出如下:
- 这里的2014137和2014136是两个事务的ID,2014136修改了1记录未提交,2014137再修改1记录导致死锁
- 通过lock_page、lock_rec、lock_data不为null,我们知道两个事务都是行锁
- 通过 lock_mode值,锁的模式是x,两个事务申请的都是排它锁
- 看相同的数据lock_space、lock_page:、lock_rec,可以得出两个事务都访问了相同的innodb数据块,通过lock_data:1,
看到锁定的数据行都是主键为1的数据记录,可见两个事务都申请了相同的资源,因此会被锁住,事务在等待
再看看INNODB_lock_waits表
此表具体解释如下:
requesting_trx_id ## 请求锁的事务 requested_lock_id: ## 请求锁的锁ID blocking_trx_id: ## 拥有锁的事务 blocking_lock_id: ## 拥有锁的锁ID |
3.4 总结
- 支持行级锁,id为1的记录锁住的时候,不影响id为2的记录的修改
注:支持行级锁是针对有索引的表,每个表创建的时候会自动对主键生成一个索引,具体看下面行级锁的实现方式
- 假设A事务对1进行操作未commit,B事务再操作1,这时B事务处于锁等待状态(LOCK WAIT),如果A事务提交,那么B事务也能修改成功,以B事务提交的数据为准。
- 假设A事务对1进行操作未commit,B事务再操作1,这是B事务处于锁等待状态(LOCK WAIT),锁等待超时后,A事务提交,B事务如果未提交,且这个事务未关闭(此种情况是指B事务开启事务控制的情况下,超时后这个事务不会消失的,会一直存在),相当于开启了事务控制,但是超时了,未能commit,这个事务一直存在,可通过innodb_trx表中查看到。
- 还有一个,关掉对应的crt窗口,相当于断掉了改对话,该事务就会在事务表中消失。
4、共享锁的产生过程测试
4.1 建立测试数据
创建表
create table tx1 (id int primary key , c1 varchar(20), c2 varchar(30)) engine=innodb default charset = utf8;
插入数据
insert into tx1 values(1,'aaaa','aaaaa2'); insert into tx1 values(2,'bbbb','bbbbb2');
4.2 模拟
第一步
start transaction;
update tx1 set c1='heyf',c2='heyf' where id=1;
还未提交之前,有这样一个事务一直在执行:
第二步
更改tx2表,条件中会查询到tx1表id为1的索引,这时候申请的是S锁,注:这时候不代表已经获得了锁,只是代表在申请,怎么查看
申请还是已获得,这就需要看innodb日志了。
update tx1 set c1='yy' where id=(select id from tx1 where id=1)
这时候查看innodb_locks表,发现出现死锁了,这个事务申请的是S锁
上面的是update,再尝试使用delete(如下命令),也会产生申请S锁,而导致死锁
delete from tx1 where id=(select id from tx1 where id=1);
注意的是:我单独地执行查询“select id from tx1 where id=1”语句是不会产生S锁的。
4.3 总结
对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);
对于普通SELECT语句,InnoDB不会加任何锁;事务可以通过以下语句显示给记录集加共享锁或排他锁。
·共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。
·排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE。
对于共享锁,有如下
- insert语句在普通情况下是会申请排他锁,也就是X锁,但是这里出现了S锁。这是因为id字段是一个唯一索引,所以insert语句会在插入前进行一次duplicate key的检查,为了使这次检查成功,需要申请S锁防止其他事务对id字段进行修改。
- update的时候,id字段是唯一索引,此种场景:UPDATE tbl_name SET column=value WHERE unique_key_col=key_value,产生S锁,防止其他事务对id字段进行修改
- delete的时候,id字段是唯一索引,DELETE FROM tbl_name WHERE unique_key_col=key_value,产生S锁,防止其他事务对id字段进行修改
5、其他
5.1 关于行锁
InnoDB行锁实现方式
InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!会把所有扫描过的行都锁定!
在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。下面通过一些实际例子来加以说明。
(1)在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁。
(2)由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。应用设计的时候要注意这一点。
(3)当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁
(4)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁
(5)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。
5.2 关于数据库服务器出现死锁的同时,磁盘利用率达到了100%的问题
出现死锁的时候同时磁盘利用率达到了100%,那么有两种情况
1、磁盘达到了100%利用率导致的死锁,
2、死锁导致了磁盘利用率达到了100%
网上找到解释:
当事务开始时,它将缓冲区语句分配一个binlog_cache_size大小的缓冲区(我这里设置的是16777216bytes,即16MB)。 如果一个语句大于此,线程将打开一个临时文件来存储事务(默认是存放在/tmp/目录下)。 当线程结束时,临时文件会自动被删除。
上面就是因为事务里面的临时文件超过16MB了,被放到/tmp目录下了,但是这个临时文件实在太大了,导致磁盘空间不足告警了。
参考: