MySQL锁总结

                                

                                                            图-MySQL锁分类html

1. 悲观锁

老是假设最坏的状况,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了不少这种锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁,悲观锁是由数据库内部实现,下文所说的共享锁和排它锁都是悲观锁的范畴。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。java

2. 乐观锁

老是假设最好的状况,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样能够提升吞吐量,像数据库提供的相似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。mysql

2.1 使用场景

从上面对两种锁的介绍,咱们知道两种锁各有优缺点,不可认为一种好于另外一种,像乐观锁适用于写比较少的状况下(多读场景),即冲突真的不多发生的时候,这样能够省去了锁的开销,加大了系统的整个吞吐量。但若是是多写的状况,通常会常常产生冲突,这就会致使上层应用会不断的进行retry,这样反却是下降了性能,因此通常多写的场景下用悲观锁就比较合适。程序员

2.2 常见的两种实现方式

乐观锁通常会使用版本号机制或CAS算法实现。面试

2.2.1 版本号机制

通常是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,不然重试更新操做,直到更新成功
举一个简单的例子:假设数据库中账户信息表中有一个version字段,当前值为1;而当前账户余额字段(balance)为$100。操做员A此时将其读出(version=1),并从其账户余额中扣除$50($100-$50)。在操做员A操做的过程当中,操做员B也读入此用户信息(version=1),并从其账户余额中扣除$20($100-$20)。操做员A完成了修改工做,将数据版本号加一(version=2),连同账户扣除后余额(balance=$50),提交至数据库更新,此时因为提交数据版本大于数据库记录当前版本,数据被更新,数据库记录version更新为2。操做员B完成了操做,也将版本号加一(version=2)试图向数据库提交数据(balance=$80),但此时比对数据库记录版本时发现,操做员B提交的数据版本号为2,数据库记录当前版本也为2,不知足“提交版本必须大于记录当前版本才能执行更新“的乐观锁策略,所以,操做员B的提交被驳回。这样,就避免了操做员B用基于version=1的旧数据修改的结果覆盖操做员A的操做结果的可能。算法

2.2.2 CAS算法

compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的状况下实现多线程之间的变量同步,也就是在没有线程被阻塞的状况下实现变量的同步,因此也叫非阻塞同步(Non-blockingSynchronization)。CAS算法涉及到三个操做数sql

  • 须要读写的内存值V
  • 进行比较的值A
  • 拟写入的新值B

当且仅当V的值等于A时,CAS经过原子方式用新值B来更新V的值,不然不会执行任何操做(比较和替换是一个原子操做)。通常状况下是一个自旋操做,即不断的重试数据库

2.3 乐观锁的缺点

2.3.1 ABA问题

若是一个变量V初次读取的时候是A值,而且在准备赋值的时候检查到它仍然是A值,那咱们就能说明它的值没有被其余线程修改过了吗?很明显是不能的,由于在这段时间它的值可能被改成其余值,而后又改回A,那CAS操做就会误认为它历来没有被修改过。这个问题被称为CAS操做的"ABA"问题。
JDK1.5之后的AtomicStampedReference类就提供了此种能力,其中的compareAndSet方法就是首先检查当前引用是否等于预期引用,而且当前标志是否等于预期标志,若是所有相等,则以原子方式将该引用和该标志的值设置为给定的更新值。编程

2.3.2 循环时间长开销大

自旋CAS(也就是不成功就一直循环执行直到成功)若是长时间不成功,会给CPU带来很是大的执行开销。若是JVM能支持处理器提供的pause指令那么效率会有必定的提高,pause指令有两个做用,第一它能够延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它能够避免在退出循环的时候因内存顺序冲突(memory order violation)而引发CPU流水线被清空(CPU pipeline flush),从而提升CPU的执行效率。segmentfault

2.3.3 只能保证一个共享变量的原子操做

CAS只对单个共享变量有效,当操做涉及跨多个共享变量时CAS无效。可是从JDK1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你能够把多个变量放在一个对象里来进行CAS操做。因此咱们可使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操做。

3. 表锁

3.1 对比行锁

MyISAM和InnoDB存储引擎使用的锁:
MyISAM采用表级锁(table-level locking)。
InnoDB支持行级锁(row-levellocking)和表级锁,默认为行级锁,可能出现行锁升级为表锁的状况。
表级锁和行级锁对比:
表级锁:Mysql中锁定粒度最大的一种锁,对当前操做的整张表加锁,实现简单,资源消耗也比较少,加锁快,因为表级锁一次会将整个表锁定,因此能够很好地避免困扰咱们的死锁问题。其锁定粒度最大,触发锁冲突的几率最高,并发度最低,MyISAM和InnoDB引擎都支持表级锁。
行级锁:Mysql中锁定粒度最小的一种锁,只针对当前操做的行进行加锁。行级锁能大大减小数据库操做的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。

3.2 什么时候使用表锁?

第一种状况:全表更新。事务须要更新大部分或所有数据,且表又比较大。若使用行锁,会致使事务执行效率低,从而可能形成其余事务长时间锁等待和更多的锁冲突。
第二种状况:多表查询。事务涉及多个表,比较复杂的关联查询,极可能引发死锁,形成大量事务回滚。这种状况若能一次性锁定事务涉及的表,从而能够避免死锁、减小数据库因事务回滚带来的开销。

  • InnoDB支持表锁和行锁,使用索引做为检索条件修改数据时采用行锁,不然采用表锁。
  • InnoDB自动给修改操做加锁,给查询操做不自动加锁。
  • 行锁可能由于未使用索引而升级为表锁,因此除了检查索引是否建立的同时,也须要经过explain执行计划查询索引是否被实际使用
  • 行锁相对于表锁来讲,优点在于高并发场景下表现更突出,毕竟锁的粒度小。
  • 当表的大部分数据须要被修改,或者是多表复杂关联查询时,建议使用表锁优于行锁。
  • 为了保证数据的一致完整性,任何一个数据库都存在锁定机制。锁定机制的优劣直接影响到一个数据库的并发处理能力和性能。

where条件若是不存在索引字段,那么这个事务是否会致使表锁?有人回答:只有主键和惟一索引才是行锁,普通索引是表锁。结果发现普通索引并不必定会引起表锁,在普通索引中是否引起表锁取决于普通索引的高效程度,即行锁升级为表锁的缘由是有SQL语句中未使用到索引,或者说使用的索引未被数据库承认(至关于没有使用索引)。

属性值重复率
当“值重复率”低时,甚至接近主键或者惟一索引的效果,“普通索引”依然是行锁;当“值重复率”高时,MySQL不会把这个“普通索引”当作索引,即形成了一个没有索引的SQL,此时引起表锁。
同JVM自动优化java代码同样,MySQL也具备自动优化SQL的功能。低效的索引将被忽略,这也就倒逼开发者使用正确且高效的索引。

4. 行锁

InnoDB只有在经过索引条件检索数据时使用行级锁,不然使用表锁,行锁是针对索引加的而不是针对记录加的锁。

从上面的案例看出,行锁变表锁彷佛是一个坑,可MySQL没有这么无聊给你挖坑。这是由于MySQL有本身的执行计划。当你须要更新一张较大表的大部分甚至全表的数据时。而你又傻乎乎地用索引做为检索条件。一不当心开启了行锁(没毛病啊!保证数据的一致性!)。可MySQL却认为大量对一张表使用行锁,会致使事务执行效率低,从而可能形成其余事务长时间锁等待和更多的锁冲突问题,性能严重降低。因此MySQL会将行锁升级为表锁,即实际上并无使用索引。咱们仔细想一想也能理解,既然整张表的大部分数据都要更新数据,在一行一行地加锁效率则更低。其实咱们能够经过explain命令查看MySQL的执行计划,你会发现key为null。代表MySQL实际上并无使用索引,行锁升级为表锁。

5. 共享锁(读锁)

共享锁又称读锁 (read lock),是读取操做建立的锁。若是事务T对数据A加上共享锁后,则其余事务只能对A再加共享锁,不能加排他锁。得到共享锁的事务只能读数据,不能修改数据。当若是事务对读锁进行修改操做,极可能会形成死锁。在查询语句后面增长LOCK IN SHARE MODE,MySQL会对查询结果中的每行都加共享锁,当没有其它线程对查询结果集中的任何一行使用排他锁时,能够成功申请共享锁不然会被阻塞。 其余线程也能够读取使用共享锁的表,并且这些线程读取的是同一个版本的数据。

6. 排它锁(写锁)

若某个事物对某一行加上了排他锁,只能这个事务对其进行读写,在此事务结束以前,其余事务不能对其进行加任何锁(排它锁会阻塞全部的排它锁和共享锁),不能进行任何读写操做,需等待其释放,排它锁是悲观锁的一种实现。

读取为何要加读锁呢?防止数据在被读取的时候被别的线程加上写锁。排他锁使用方式:在须要执行的语句后面加上for update就能够了 select status from TABLE where id=1 for update;(能够参考使用select for share,for update的场景及死锁陷阱

7. 间隙锁

当咱们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫作“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。
举例来讲,假如emp表中只有101条记录,其empid的值分别是1,2,...,100,101,下面的SQL:
Select * from emp where empid>100 for update;
复制代码是一个范围条件的检索,InnoDB不只会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
InnoDB使用间隙锁的目的,一方面是为了防止幻读,以知足相关隔离级别的要求,对于上面的例子,要是不使用间隙锁,若是其余事务插入了empid大于100的任何记录,那么本事务若是再次执行上述语句,就会发生幻读;另一方面,是为了知足其恢复和复制的须要
很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这每每会形成严重的锁等待。所以,在实际应用开发中,尤为是并发插入比较多的应用,咱们要尽可能优化业务逻辑,尽可能使用相等条件来访问更新数据,避免使用范围条件。若执行的条件是范围过大,则InnoDB会将整个范围内全部的索引键值所有锁定,很容易对性能形成影响。
还要特别说明的是,InnoDB除了经过范围条件加锁时使用间隙锁外,若是使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁

8. 意向锁

意向锁是为了支持多种粒度锁同时存在。申请意向锁的动做是数据库完成的,即事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不须要咱们程序员使用代码来申请。当再向一个表添加表级X锁的时候若是没有意向锁的话,则须要遍历全部整个表判断是否有行锁的存在,以避免发生冲突。若是有了意向锁,只须要判断该意向锁与即将添加的表级锁是否兼容便可。由于意向锁的存在表明了有行级锁的存在或者即将有行级锁的存在,于是无需遍历整个表便可获取结果。(具体能够参考知乎-InnoDB 的意向锁有什么做用?

9. next-key锁

Next-Key Locks(简称NK锁)是记录锁和间隙锁的组合,锁定一个范围,包含记录自己,主要是为了解决幻读问题。

记录锁针对索引记录,老是锁定索引记录,即便表没有索引InnoDB会建立隐式的索引并使用这个索引实施记录锁,能够参考Clustered and Secondary Indexes

幻读(Phantom read): 幻读与不可重复读相似。它发生在一个事务(T1)读取了几行数据,接着另外一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些本来不存在的记录,就好像发生了幻觉同样,因此称为幻读。

10. MVCC(无锁)

InnoDB默认的RR事务隔离级别下,不显式加『lock in share mode』与『for update』的『select』操做都属于快照读,保证事务执行过程当中只有第一次读以前提交的修改和本身的修改可见,其余的均不可见,即不加锁读,读取记录的快照版本而非最新版本,经过MVCC实现。MVCC即多版本并发控制,与之对应的是基于锁的并发控制。

                                

                                                          图-MVCC(来自高性能MySQL)

11. 死锁问题

11.1 死锁场景列举

  • 案例一 加锁顺序不一致致使select for update死锁
#须要关闭自动提交
SET AUTOCOMMIT=0;
#session依次执行
select * from table1 where id = 171 for update;
select * from table1 where id = 172 for update;
#session依次执行
select * from table1 where id = 172 for update;
select * from table1 where id = 171 for update;
#改成一次性加锁可避免死锁,在in里面的列表值mysql是会自动从小到大排序,加锁也是一条条从小到大加的锁
select * from table1 where id in (xx,xx,xx) for update
  • 案例二 间隙锁引发的死锁
SET AUTOCOMMIT=0;
#session1,
#当对存在的行进行锁的时候(主键),mysql就只有行锁。
#当对未存在的行进行锁的时候(即便条件为主键),mysql是会锁住一段范围(有gap锁)
select * from km_personal_articles where id = 181 for update;
#session2
select * from km_personal_articles where id = 182 for update;
#session1
INSERT INTO `km_personal_articles` (`id`, `empid`, `name`, `url`, `pv`, `created_at`)
VALUES
	(181, 'G6411', '测试', 'http', 277, '2018-11-19 16:06');
#session2
#插入意向锁和间隙锁冲突,因此两个事务互相等待,最后造成死锁
INSERT INTO `km_personal_articles` (`id`, `empid`, `name`, `url`, `pv`, `created_at`)
VALUES
	(182, 'G6411', '测试', 'http', 277, '2018-11-19 16:06');
#用mysql特有的语法来解决此问题。由于insert语句对于主键来讲,插入的行无论有没有存在,都会只有行锁。
insert into t3(xx,xx) on duplicate key update `xx`='XX';
  • 案例三 隐晦加锁顺序不一致致使死锁
CREATE TABLE `students` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `no` varchar(24) DEFAULT NULL,
  `name` varchar(512) DEFAULT NULL,
  `score` int(11) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `no` (`no`),
  KEY `name` (`name`),
  KEY `age` (`age`)
) ENGINE=InnoDB AUTO_INCREMENT=181 DEFAULT CHARSET=utf8mb4 COMMENT='死锁分析demo';
id  no      name score age
15	S001	aaa	100	25
18	S002	bbb	100	24
20	S003	ccc	100	24
30	S004	ddd	4	23
37	S005	eee	5	22
49	S006	fff	6	25
50	S007	ggg	7	23
SET AUTOCOMMIT=0;
#session1
update students set score = 100 where id < 30;
#session2
#在范围查询时,加锁是一条记录一条记录挨个加锁的,若是两条SQL语句的加锁顺序不同,也会致使死锁。
#事务A的范围条件为id<30,加锁顺序为:id=15->18->20,事务B走的是二级索引age,除了在二级索引加锁外,还会在聚簇索引上加锁。加锁顺序为:(age,id)=(24,18)->(24,20)->(25,15)->(25,49),其中,对id的加锁顺序为id=18->20->15->49。能够看到事务A先锁15,再锁18,而事务B先锁18,再锁15,从而造成死锁
update students set score = 100 where age > 23;

11.2 死锁检测

11.2.1 死锁产生的条件

产生死锁的四个必要条件:
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已得到的资源保持不放。
不剥夺条件: 进程已得到的资源,在末使用完以前,不能强行剥夺。
循环等待条件:若干进程之间造成一种头尾相接的循环等待资源关系。
虽然不能彻底避免死锁,但可使死锁的数量减至最少。将死锁减至最少能够增长事务的吞吐量并减小系统开销,由于只有不多的事务回滚,而回滚会取消事务执行的全部工做,因为死锁回滚的操做由应用程序从新提交。

11.2.2 MySQL死锁检测机制

直观方法是在两个事务相互等待时,当一个等待时间超过设置的某一阀值时,对其中一个事务进行回滚,另外一个事务就能继续执行。这种方法简单有效,在innodb中,参数innodb_lock_wait_timeout(事务锁超时时间)用来设置超时时间。
仅用上述方法来检测死锁太过被动,innodb还提供了wait-for graph算法来主动进行死锁检测,每当加锁请求没法当即知足须要并进入等待时,wait-for graph算法都会被触发。

InnoDB将各个事务看为一个个节点,资源就是各个事务占用的锁,当事务1须要等待事务2的锁时,就生成一条有向边从1指向2,最后行成一个有向图。咱们只要检测这个有向图是否出现环路便可,出现环路就是死锁。这就是wait-for graph算法。

                                                    

                                                        图-wait-for graph算法

11.2.3 分析行锁定

经过检查innodb_row_lock状态变量分析系统上中行锁的争夺状况
mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0     |
| Innodb_row_lock_time          | 0     |
| Innodb_row_lock_time_avg      | 0     |
| Innodb_row_lock_time_max      | 0     |
| Innodb_row_lock_waits         | 0     |
+-------------------------------+-------+
innodb_row_lock_current_waits: 当前正在等待锁定的数量
innodb_row_lock_time: 从系统启动到如今锁定总时间长度;很是重要的参数,
innodb_row_lock_time_avg: 每次等待所花平均时间;很是重要的参数,
innodb_row_lock_time_max: 从系统启动到如今等待最常的一次所花的时间;
innodb_row_lock_waits: 系统启动后到如今总共等待的次数;很是重要的参数。直接决定优化的方向和策略。

行锁优化

  1. 尽量让全部数据检索都经过索引来完成,避免无索引行或索引失效致使行锁升级为表锁。
  2. 尽量避免间隙锁带来的性能降低,减小或使用合理的检索范围。
  3. 尽量减小事务的粒度,好比控制事务大小,而从减小锁定资源量和时间长度,从而减小锁的竞争等,提升性能。
  4. 尽量低级别事务隔离,隔离级别越高,并发的处理能力越低。

11.2.4 分析表锁定

查看加锁状况
show open tables; 1表示加锁,0表示未加锁。
mysql> show open tables where in_use > 0;
+----------+-------------+--------+-------------+
| Database | Table       | In_use | Name_locked |
+----------+-------------+--------+-------------+
| lock     | myisam_lock |      1 |           0 |
+----------+-------------+--------+-------------+
分析表锁定
能够经过检查table_locks_waited和table_locks_immediate状态变量分析系统上的表锁定
mysql> show status like 'table_locks%';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Table_locks_immediate      | 104   |
| Table_locks_waited         | 0     |
+----------------------------+-------+
table_locks_immediate: 表示当即释放表锁数。
table_locks_waited: 表示须要等待的表锁数。此值越高则说明存在着越严重的表级锁争用状况。

此外,MyISAM的读写锁调度是写优先,这也是MyISAM不适合作写为主表的存储引擎。由于写锁后,其余线程不能作任何操做,大量的更新会使查询很可贵到锁,从而形成永久阻塞。

11.3 如何定位死锁成因

1)经过应用业务日志定位到问题代码,找到相应的事务对应的sql;
由于死锁被检测到后会回滚,这些信息都会以异常反应在应用的业务日志中,经过这些日志咱们能够定位到相应的代码,并把事务的sql给梳理出来。
此外,咱们根据日志回滚的信息发如今检测出死锁时这个事务被回滚。
2)肯定数据库隔离级别。
执行select @@global.tx_isolation,能够肯定数据库的隔离级别,咱们数据库的隔离级别是RC,这样能够很大几率排除gap锁形成死锁的嫌疑;
3)找DBA执行下show engine innodb status \G; 看看最近详细的死锁日志,在打印出来的信息中找到“LATEST DETECTED DEADLOCK”一节内容。

11.4 如何解除死锁状态

#查询是否锁表
show OPEN TABLES where In_use > 0;
#查询进程(若是您有SUPER权限,您能够看到全部线程。不然,您只能看到您本身的线程)
show processlist
#杀死进程id(就是上面命令的id列)
kill id

#查看当前的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
#查看当前锁定的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
#查看当前等锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
#杀死进程
kill 进程ID

11.5 如何避免死锁

  • 如上面的案例一和案例三所示,对索引加锁顺序的不一致极可能会致使死锁,因此若是能够,尽可能以相同的顺序来访问索引记录和表。在程序以批量方式处理数据的时候,若是事先对数据排序,保证每一个线程按固定的顺序来处理记录,也能够大大下降出现死锁的可能;
  • 如上面的案例二所示,间隙锁每每是程序中致使死锁的真凶,因为默认状况下MySQL的隔离级别是RR(RR级别为了解决幻读引入了间隙锁),因此若是能肯定幻读和不可重复读对应用的影响不大,能够考虑将隔离级别改为RC,能够避免间隙锁致使的死锁;
  • 在同一个事务中,尽量作到一次锁定所须要的全部资源,减小死锁几率,如案例一所示一次锁定所需的记录能够避免死锁。
  • 为表添加合理的索引,若是不走索引将会为表的每一行记录加锁,死锁的几率就会大大增大;
  • 咱们知道 MyISAM 只支持表锁,它采用一次封锁技术来保证事务之间不会发生死锁,因此,咱们也可使用一样的思想,在事务中一次锁定所须要的全部资源,减小死锁几率;
  • 避免大事务,尽可能将大事务拆成多个小事务来处理;由于大事务占用资源多,耗时长,与其余事务冲突的几率也会变高;
  • 避免在同一时间点运行多个对同一表进行读写的脚本,特别注意加锁且操做数据量比较大的语句;咱们常常会有一些定时脚本,避免它们在同一时间点运行;
  • 设置锁等待超时参数:innodb_lock_wait_timeout,这个参数并非只用来解决死锁问题,在并发访问比较高的状况下,若是大量事务因没法当即得到所需的锁而挂起,会占用大量计算机资源,形成严重性能问题,甚至拖跨数据库。咱们经过设置合适的锁等待超时阈值,能够避免这种状况发生。

参考资料

segmentfault-MySQL InnoDB锁机制全面解析分享

掘金-全面了解mysql锁机制(InnoDB)与问题排查

segmentfault-MySQL InnoDB 锁—官方文档

程序员DD-多是全网最好的MySQL重要知识点 | 面试必备

解决死锁之路(终结篇)- 再见死锁

cnblogs-mysql死锁问题分析

cnblogs-Mysql并发时经典常见的死锁缘由及解决方法

相关文章
相关标签/搜索