3.2 从数据操作的粒度划分:表级锁、页级锁、行锁
为了提高数据库并发度,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但管理锁是很耗资源(涉及获取、检查、释放锁等动作)。因此数据库系统需要在高并发响应和系统性能两方面进行平衡,这样就产生了“锁粒度(Lock granularity)”的概念。
对一条记录加锁影响的也只是这条记录而已,我们就说这个锁的粒度比较细;其实一个事务也可以在表级别进行加锁,自然就被称之为表级锁或者表锁,对一个表加锁影响整个表中的记录,我们就说这个锁的粒度比较粗。锁的粒度主要分为表级锁、页级锁和行锁。
1. 表锁(Table Lock)
该锁会锁定整张表,它是MysQL中最基本的锁策略,并不依赖于存储引擎(不管你是MysQL的什么存储引擎对于表锁的策略都是一样的),并且表锁是开销最小的策略(因为粒度比较大)。由于表级锁一次会将整个表锁定,所以可以很好的避免死锁问题。当然,锁的粒度大所带来最大的负面影响就是出现锁资源争用的概率也会最高,导致并发率大打折扣。
① 表级别的S锁、X锁
lock tables t read : InnoDB 会对表 t 加表级别的 S 锁lock tables t write : InnoDB 会对表 t 加表级别的 X 锁
② 意向锁 (intention lock)
-- 事务要获取某些行的 S 锁,必须先获得表的 IS 锁。
SELECT column FROM table ... LOCK IN SHARE MODE;
②意向排他锁(intention exclusive lock, IX):事务有意向对表中的某些行加排他锁(X锁)
-- 事务要获取某些行的 X 锁,必须先获得表的 IX 锁。
SELECT column FROM table ... FOR UPDATE;
即:意向锁是由存储引擎 自己维护的 ,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前, InooDB 会先获取该数据行 所在数据表的对应意向锁 。
在数据表的场景中, 如果我们给某一行数据加上了排它锁,数据库会自动给更大一级的空间,比如数据页或数据表加上意向锁,告诉其他人这个数据页或数据表已经有人上过排它锁了 ,这样当其他人想要获取数据表排它锁的时候,只需要了解是否有人已经获取了这个数据表的意向排他锁即可。
(2)当前没有其他事务持有teacher表中任意一行的排他锁。
为了检测是否满足第二个条件,事务B必须在确保teacher表不存在任何排他锁的前提下,去检测表中的每一行是否存在排他锁。 很明显这是一个效率很差的做法,但是有了意向锁之后,情况就不一样了。
意向锁不会与行级的共享 / 排他锁互斥!正因为如此,意向锁并不会影响到多个事务对不同数据行加排 他锁时的并发性。(不然我们直接用普通的表锁就行了)
③ 元数据锁(MDL锁)
2. InnoDB中的行锁
行锁(Row Lock)也称为记录锁,顾名思义,就是锁住某一行(某条记录row)。需要的注意的是,MySQL服务器层并没有实现行锁机制,行级锁只在存储引擎层实现。
优点:锁定力度小,发生锁冲突概率低,可以实现的并发度高。
缺点:对于锁的开销比较大,加锁会比较慢,容易出现死锁情况。
InnoDB与MyISAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁。
先建立student表
① 记录锁(Record Locks)
- 当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可 以继续获取X型记录锁;
- 当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁。
② 间隙锁(Gap Locks)
- session 1 执行select ... for update 语句, 由于id = 5 这一行并不存在, 因此会加上间隙锁(3,8)
- session 2 执行select ... for update 语句, 同样加上间隙锁(3,8), 间隙锁之间不会冲突, 因此这个语句可以执行成功
- session 2 试图插入一行, 被session 1 的间隙锁挡住,进入等待
- session 1 视图插入一行, 被session 2 的间隙锁挡住, 两个session进入死锁
③ 临键锁(Next-Key Locks)
记录锁 + 间隙锁
④ 插入意向锁(Insert Intention Locks)
插入意向锁是在插入一条记录行前,由INSERT操作产生的一种间隙锁。该锁用以表示插入意向,
当多个事务在同一区间(gap)插入位置不同的多条数据时,事务之间不需要互相等待。假设存在两条值分别为4和7的记录,两个不同的事务分别试图插入值为5和6的两条记录,每个事务在获取插入行上独占的(排他)锁前,都会获取(4,7)之间的间隙锁,但是因为数据行之间并不冲突,所以两个事务之间并不会产生冲突(阻塞等待)。
总结来说,插入意向锁的特性可以分成两部分:
- 插入意向锁是一种特殊的间隙锁—―间隙锁可以锁定开区间内的部分记录。
- 插入意向锁之间互不排斥,所以即使多个事务在同一区间插入多条记录,只要记录本身(主键、唯一索引)不冲突,那么事务之间就不会出现冲突等待。
注意,虽然插入意向锁中含有意向锁三个字,但是它并不属于意向锁而属于间隙锁,因为意向锁是表锁而插入意向锁是行锁。
比如,把id值为8的那条记录加一个插入意向锁的示意图如下:比如, 现在T1为id值为8 的记录加了一个gap锁, 然后T2 和 T3 分别想向student表中插入id值分别为4,5的两条记录, 所以现在为id值为8的记录加的锁的示意图就如下所示:
从图中可以看到,由于T1持有gap锁,所以T2和T3需要生成一个插入意向锁的锁结构并且处于等待状态。当T1提交后会把它获取到的锁都释放掉,这样T2和T3就能获取到对应的插入意向锁了(本质上就是把插入意向锁对应锁结构的is_waiting属性改为false),T2和T3之间也并不会相互阻塞,它们可以同时获取到id值为8的插入意向锁,然后执行插入操作。事实上插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁。
3.3 从对待锁的态度划分:乐观锁、悲观锁
1. 悲观锁(Pessimistic Locking)
2. 乐观锁(Optimistic Locking)
1. 乐观锁的版本号机制
2. 乐观锁的时间戳机制
3. 两种锁的适用场景
- 乐观锁 适合 读操作多 的场景,相对来说写的操作比较少。它的优点在于 程序实现 , 不存在死锁 问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
- 悲观锁 适合 写操作多 的场景,因为写的操作具有 排它性 。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止 读 - 写 和 写 - 写 的冲突。
3.4 其它锁之:全局锁
Flush tables with read lock
3.5 其它锁之:死锁
1. 概念
这时候,事务1在等待事务2释放id=2的行锁,而事务2在等待事务1释放id=1的行锁。 事务1和事务2在互相等待对方的资源释放,就是进入了死锁状态。当出现死锁以后,有 两种策略 :
- 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。
- 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务(将持有最少行级排他锁的事务进行回滚),让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on ,表示开启这个逻辑。
2. 产生死锁的必要条件
- 两个或者两个以上事务
- 每个事务都已经持有锁并且申请新的锁
- 锁资源同时只能被同一个事务持有或者不兼容
- 事务之间因为持有锁和申请锁导致彼此循环等待
死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。
3.如何处理死锁
- 方式1:等待,直到超时(innodb_lock_wait_timeout=50s)。
即当两个事务互相等待时,当一个事务等待时间超过设置的阈值时,就将其回滚,另外事务继续进行。这种方法简单有效,在innodb中,参数innodb_lock_wait_timeout用来设置超时时间。
缺点:对于在线服务来说,这个等待时间往往是无法接受的。
那将此值修改短一些,比如1s,0.1s是否合适?不合适,容易误伤到普通的锁等待。
- 方式2:使用死锁检测进行死锁处理
方式1检测死锁太过被动,innodb还提供了wait-for graph算法来主动进行死锁检测,每当加锁请求无法立即满足需要并进入等待时,wait-for graph算法都会被触发。
基于这两个信息, 可以绘制wait-for graph(等待图)
死锁检测的原理是构建一个以事务为顶点, 锁为边的有向图,判断有向图是否存在环, 存在即有锁
一旦检测到回路、有死锁,这时候InnoDB存储引擎会选择回滚undo量最小的事务(将持有最少行级排他锁的事务进行回滚),让其他事务继续执行(innodb_deadlock_detect=on `表示开启这个逻辑)。
缺点:每个新的被阻塞的线程,都要判断是不是由于自己的加入导致了死锁,这个操作时间复杂度是o(n)。如果100个并发线程同时更新同一行,意味着要检测100*100 = 1万次,1万个线程就会有1千万次检测。
如何解决?
- 方式1:关闭死锁检测,但意味着可能会出现大量的超时,会导致业务有损。
- 方式2:控制并发访问的数量。比如在中间件中实现对于相同行的更新,在进入引擎之前排队,这样在InnoD内部就不会有大量的死锁检测工作。
进一步的思路:
可以考虑通过将一行改成逻辑上的多行来减少锁冲突. 比如, 连锁超市账户总额的记录, 可以考虑放到多条记录上, 账户总额等于这多个记录的值的总和.
4.如何避免死锁
-
合理设计索引,使业务sQL尽可能通过索引定位更少的行,减少锁竞争。
-
调整业务逻辑sQL执行顺序,避免update/delete长时间持有锁的sQL在事务前面。
-
避免大事务,尽量将大事务拆成多个小事务来处理,小事务缩短锁定资源的时间,发生锁冲突的几率也更小。
-
在并发比较高的系统中,不要显式加锁,特别是是在事务里显式加锁。如select ... for update语句,如果是在事务里运行了start transaction或设置了autocommit等于o,那么就会锁定所查找到的记录。
-
降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为Rc,可以避免掉很多因为gap锁造成的死锁。