一、简介
锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问。InnoDB 存储引擎会在行级别上对表数据上锁,这固然不错。不过 InnoDB存储引擎也会在数据库内部其他多个地方使用锁,从而允许对多种不同资源提供并发访问。例如,操作缓冲池中的LRU列表,删除、添加、移动LRU 列表中的元素,为了保证一致性,必须有锁的介入。数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。 另一点需要理解的是,虽然现在数据库系统做得越来越类似,但是有多少种数据库,就可能有多少种锁的实现方法。
InnoDB 存储引擎锁的实现和 Oracle 数据库非常类似,提供一致性的非锁定读、行级锁支持。行级锁没有相关额外的开销,并可以同时得到并发性和一致性。
上一张网图让大家宏观的理解mysql的锁分类
二、MVCC多版本并发控制
MVCC是高并发情况下对写锁的优化,所以在读未提交的隔离级别下是不涉及的
1.快照读和当前读
(1)快照读(一致性非锁定读):
在读已提交和可重复读的隔离级别下,普通的select语句是快照读(一致性非锁定读)
也就是说当读和写并发执行,读会读到写之前的数据,相当于读到一个快照,之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
(2)当前读(一致性锁定读):
在序列化隔离级别下。普通的select语句也是一致性锁定读
除此之外像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
二、行锁
行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。有可能会出现死锁的情况。 行级锁按照使用方式分为共享锁和排他锁。
1.共享锁(S锁 读锁):
允许事务读一行数据。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
共享锁就是允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有。
2.排他锁(X 锁、写锁):
排它锁,也称作独占锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁
3.行锁的算法
InnoDB 存储引擎有 3 种行锁的算法,其分别是
(1)Record Lock∶ 单个行记录上的锁
(2) Gap Lock∶间隙锁,锁定一个范围,但不包含记录本身,GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
(3)Next-Key Lock(行锁和间隙锁组合)∶ 锁定一个范围,并且锁定记录本身 Record Lock 总是会去锁住索引记录,如果 InnoDB 存储引擎表在建立的时候没有设置任何一个索引,那么这时 InnoDB存储引擎会使用隐式的主键来进行锁定。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。
Next-Key Lock使用条件:
如果更新条件为索引字段,但是并非唯一索引(包括主键索引),例如执行“update from t1 set v2=0 where v1=9;” 那么此时更新会使用Next-Key Lock。使用Next-Key Lock的原因:
a)、首先要保证在符合条件的记录上加上排他锁,会锁定当前非唯一索引和对应的主键索引的值;
b)、还要保证锁定的区间不能插入新的数据。
3)、如果更新条件为唯一索引,则使用Record Lock(记录锁)。
间隙锁的使用条件:
官方说明:
当我们用范围条件条件检索数据(非聚簇索引、非唯一索引),并请求共享或排他锁时,InnoDB会给符合条件的数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,称为间隙,InnoDB也会为这些间隙加锁,即间隙锁。
Next-Key锁是符合条件的行锁加上间隙锁个人理解
(1)隔离级别为RR
(2)当前读
(3)查询条件能够走到索引
三、表锁
1.概述
表级锁是mysql锁中粒度最大的一种锁,表示当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。被大部分的mysql引擎支持,MyISAM和InnoDB都支持表级锁,但是InnoDB默认的是行级锁。
2.表锁产生场景:
(1)导致这个的原因第一是sql语句写法问题,没有合理构建和使用索引。如:用select.. for update的时候,如果走索引的话触发行锁,没走索引就变表锁
(2)第二个原因是mysql的优化器,有时优化器发现:即使使用了索引,还是要做全表扫描,故而放弃了索引,也就没有使用行锁,却使用了表锁
类似上面,表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须先取得该表的IX锁。
3.意向锁
InnoDB为了支持多粒度(表锁与行锁)的锁并存,引入意向锁。而意向锁就是其中的一种表级锁
。
(1)意向共享锁(IS)
表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的IS锁
SELECT column FROM table ... LOCK IN SHARE MODE; //事务要获取某些行的 S 锁,必须先获得表的 IS 锁。
(2)意向排他锁(IX)
事务有意向对表中的某些行加排他锁(X锁)
SELECT column FROM table ... FOR UPDATE; //事务要获取某些行的 X 锁,必须先获得表的 IX 锁。
意向锁要解决的问题
如果另一个任务试图在该表级别上应用共享或排它锁,则受到由第一个任务控制的表级别意向锁的阻塞。第二个任务在锁定该表前不必检查各个页或行锁,而只需检查表上的意向锁
例:
事务A锁住了表中的一行,让这一行只能读,不能写。
之后,事务B申请整个表的写锁。
如果事务B申请成功,那么理论上它就能修改表中的任意一行,这与A持有的行锁是冲突的。
数据库需要避免这种冲突,就是说要让B的申请被阻塞,直到A释放了行锁。
数据库要怎么判断这个冲突呢?
step1:判断表是否已被其他事务用表锁锁表
step2:判断表中的每一行是否已被行锁锁住。
注意step2,这样的判断方法效率实在不高,因为需要遍历整个表。
于是就有了意向锁。
在意向锁存在的情况下,事务A必须先申请表的意向共享锁,成功后再申请一行的行锁。
在意向锁存在的情况下,上面的判断可以改成
step1:不变
step2:发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞。
注意:申请意向锁的动作是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要我们程序员使用代码来申请。
意向共享锁(IS) | 意向排他锁(IX) | |
---|---|---|
共享锁(S) | 兼容 | 互斥 |
排他锁(X) | 互斥 | 互斥 |
这里的排他 / 共享锁指的都是表锁!!!意向锁不会与行级的共享锁 / 排他锁互斥!!!
四、死锁
1.死锁产生原因
两个事务都持有对方需要的锁,并且在等待对方释放,并且双方都不会释放自己的锁。
2.MySQL有两种死锁处理方式:
(1)等待,直到超时(innodb_lock_wait_timeout=50s)。
发起死锁检测,主动回滚一条事务,让其他事务继续执行(innodb_deadlock_detect=on)。
由于性能原因,一般都是使用死锁检测来进行处理死锁。
死锁检测
死锁检测的原理是构建一个以事务为顶点、锁为边的有向图,判断有向图是否存在环,存在即有死锁。
(2)回滚
检测到死锁之后,选择插入更新或者删除的行数最少的事务回滚,基于 INFORMATION_SCHEMA.INNODB_TRX 表中的 trx_weight 字段来判断。
3.如何避免发生死锁
(1)收集死锁信息:
利用命令 SHOW ENGINE INNODB STATUS查看死锁原因。
调试阶段开启 innodb_print_all_deadlocks,收集所有死锁日志。
(2)使用事务,不使用 lock tables 。
(3)保证没有长事务。操作完之后立即提交事务,特别是在交互式命令行中。
(4)如果在用 (SELECT ... FOR UPDATE or SELECT ... LOCK IN SHARE MODE),尝试降低隔离级别。
(5)修改多个表或者多个行的时候,将修改的顺序保持一致。
(6)创建索引,可以使创建的锁更少。
(7)最好不要用 (SELECT ... FOR UPDATE or SELECT ... LOCK IN SHARE MODE)。
五、锁升级
锁升级(Lock Escalation)是指将当前锁的粒度降低。举例来说,数据库可以把一个表的 1000 个行锁升级为一个页锁,或者将页锁升级为表锁。如果在数据库的设计中认为锁是一种稀有资源,而且想避免锁的开销,那数据库中会频繁出现锁升级现象。 Microsoft SQL Server 数据库的设计认为锁是一种稀有的资源,在适合的时候会自动地将行、键或分页锁升级为更粗粒度的表级锁。这种升级保护了系统资源,防止系统使用太多的内存来维护锁,在一定程度上提高了效率。