前言

生产环境碰到了死锁的问题,经过本地还原测试,确定了是单表并发量大时,同步插入更新操作导致的死锁产生,之后进行拆表操作,一个表只负责insert操作,拆分的表进行update操作,目前线上环境未发现死锁现象,特此记录下mysql锁机制知识的学习。

一、锁类型介绍

MySQL服务器和独立的存储引擎都可以设置锁。一般来说锁分为读锁(或叫共享锁)和写锁(排它锁)。读锁允许并发线程读取加锁的数据,但禁止写数据;相反,写锁禁止其他线程读写操作

MySQL有4种类型的锁:表锁、行锁、页锁、元数据锁:

表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
元数据锁:元数据是DDL语句的变更信息,如create、drop、alter等,MySQL 5.5版本加入的新特性。目的是为了解决线程可以在其他线程中的并发事务使用相同表的情况下修改表定义或是删除表的问题。

二、产生原因

1.什么是死锁

官方定义如下:两个事务都持有对方需要的锁,并且在等待对方释放,并且双方都不会释放自己的锁。
(这就像你有一个妞,对方也有一个妞,你想要对方的妞又同时想要你现在的,对方也是相同的想法)

2.如何形成的死锁

MySQL的并发控制有两种方式,一个是 MVCC,一个是两阶段锁协议。

两阶段锁协议(2PL)

官方定义:两阶段锁协议是指所有事务必须分两个阶段对数据加锁和解锁,在对任何数据进行读、写操作之前,事务首先要获得对该数据的封锁;在释放一个封锁之后,事务不再申请和获得任何其他封锁。

对应到 MySQL 上分为两个阶段:

扩展阶段(事务开始后,commit 之前):获取锁
收缩阶段(commit 之后):释放锁

就是说呢,只有遵循两段锁协议,才能实现 可串行化调度。但是两阶段锁协议不要求事务必须一次将所有需要使用的数据加锁,并且在加锁阶段没有顺序要求,所以这种并发控制方式会形成死锁。

3.Mysql如何处理死锁的

MySQL有两种死锁处理方式:

1、等待,直到超时(默认innodb_lock_wait_timeout=50s)。
2、发起死锁检测,主动回滚一条事务,让其他事务继续执行(默认innodb_deadlock_detect=on,5.7.15以上的版本才有此参数)。

死锁检测的原理:构建一个以事务为顶点、锁为边的有向图,判断有向图是否存在环,存在即有死锁。检测到死锁之后,选择插入更新或者删除的行数最少的事务回滚,基于 INFORMATION_SCHEMA.INNODB_TRX 表中的 trx_weight 字段来判断。

4.如何避免死锁

1、以固定的顺序访问表和行。比如两个更新数据的事务,事务A更新数据的顺序为1,2;事务B更新数据的顺序为 2 ,1;。这样更可能会造成死锁。

2、大事务拆小。大事务更倾向于死锁,如果业务允许,将大事务拆小。

3、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率。

4、降低隔离级别。如果业务允许,将隔离级别调低也是比较好的选择,比如将隔离级别从RR调整为RC,可以避免很多因为gap锁造成的死锁。

5、为表添加合理的索引。如果不走索引将会为表的每一行记录添加上锁,死锁的概率大大增加。

5.如何定位死锁

1、select * from information_schema.processlist,可以查看所有正在执行的sql线程

mysql写锁和读锁互斥 mysql锁和死锁_加锁


Id:执行的线程id

User、Host、db: 客户端连接的选项

Command:线程中执行的命令

Time:从线程开始执行命令道现在消耗的时间,单位秒

State:内部状态

Info:表明线程当前正在进行的工作

2、show engine innodb status

是专门针对Innodb引擎的,可以确定InnoDB中的请求是否阻塞。该命令在分析并发多语句事务的作用的时候尤为有用,输出内容更加详细,包括执行的时长,执行的sql语句,线程id,查询id等,感兴趣的小伙伴可以自行查阅资料

6.如何解决死锁

临时办法SELECT * FROM information_schema.INNODB_TRX;,如果有记录,则找到trx_mysql_thread_id这个字段对应的id, 将其kill掉。

最终还是需要从程序出发来解决死锁的问题,具体分析出是设计的不合理还是什么原因。

未完待续。。。