内容
- 锁种类
- 锁类型
- 一致性非锁定读
- 一致性锁定读
- 锁算法
一 锁种类
根据加锁的范围来说,锁可以分为:全局锁、表级锁、行级锁
1 全局锁(FTWRL)
- 加锁方式:flush tables with read lock
- 用途:全局主要用来全库逻辑备份,加了全局锁之后,会阻塞整库其他线程的更新操作,这个时候可以通过select把整库导出到文本做备份;
- 缺点:如果在主库执行全局锁,主库就停写,如果在备库,那么会导致主库延迟;
- 还可以用其他方式代替全局锁吗?
- 如果所有表都是innodb表,并且隔离级别是可重复读的话,可以通过事务来实现,在一个事务内进行读取数据,事务开启时会生成一个一致性视图,之后在当前事务读取的数据都不会受其他线程更新影响
2 表级锁
- 种类:表级锁有两种:表锁和MDL(metadata lock)
- 表锁加锁:lock tables … read/write
- MDL:mdl锁是隐示的加锁,当访问一个表时会被主动加上
1.当对表进行增删改查时,会申请获取mdl读锁;当进行表结构变更时,会申请mdl写锁;
2.mdl读锁之间不阻塞,可以用多个线程同时获取同一个表的mdl读锁;mdl读锁会堵塞mdl写锁,mdl写锁会堵塞mdl 读写锁,当一个表的mdl读锁被占用了后,不能再对该表进行表结构变更,ddl会被堵塞,假设此时又有其他的线程来申请mdl读锁,那么这些读请求都会被堵塞,所以如果对热点表进行ddl操作,有可能就会堵塞大量请求,然后又会不停重试,导致打爆整个库
3 行级别
- 两阶段锁协议:在innodb事务中,行锁是需要的时候才加上的,但是要等到事务结束了才会释放锁;这个机制给我们启示是:如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量放在事务的后面去执行;
- 死锁和死锁检测
1.当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁 - 当发生死锁是有何种策略解决?
- 直接等待,超时释放,超时时间是有参数:innodb_lock_wait_timeout设定的;
2.发起死锁检测,主动回滚死锁中一个事务,让其他的事务得也执行,将参数:innodb_deadlock_detect 设置为 on(默认就是on),表示开启这个逻辑。
- 直接等待,超时释放,超时时间是有参数:innodb_lock_wait_timeout设定的;
- 死锁检测
1.死锁检测比较耗cpu资源,它是一个o(n)的操作,,所以当并发比较大的时候,发现单位时间内执行的事务缺不多,但是cpu确被打满,那很有可能是死锁检测耗掉了大量的cpu资源 - 控制死锁
1.减少死锁的方向是控制相同资源的并发事务量,一种业务做法是把资源分散,分散对相同资源的并发事务冲突;
一 锁类型
概括性来说,mysql innodb存储引擎有两种锁类型,分别是行级别的行锁和表级别的意向锁;
行锁有如下两种类型:
- 行共享锁(S Lock):允许事务读取一行数据;
-
行排他锁(X Lock): 允许事务删除或是更新一条数据;
如果一个事务想要更新或是删除一行数据,就必须获取这行数据的排他锁;如果已经有其他事务获取该行的共享锁,就会阻塞其他事务获取该行的排他锁,但是不阻塞获取共享,行共享和排他锁的兼容性如下:
image.png
为了支持多种粒度的锁机制,innodb也支持表级别的加锁,要想获取细粒度的锁(行锁),就必须先获取更粗粒度的锁(意向锁),意向锁有两种:
- 意向共享锁(IS Lock): 事务想要获取表中的某行或是某几行的共享锁;
-
意向排他锁(IX Lock): 事务想要获取表中的某行或是某几行的排他锁;
意向锁和行锁的兼容性如下图所示:
image.png
二 一致性非锁定读
一个事务想要读取一行记录,需要先获取这行数据的行共享锁,如果此时另外一个事务先获取该行的行排他锁,根据行锁的兼容性,后来的事务变不能获取行共享,只能等待行排他锁释放,这无疑是不得行了,Innodb通过MVCC机制实现了写不阻塞读,读取的数据如果有其他事务正在进行更新或是删除操作,则读取该行数据的上一个事务版本号的数据,读取的是一个快照数据,这个数据是通过undo日志来获取的,undo日志也即是innodb mvcc机制实现的根本;
三 一致性锁定读
在默认的可重复读隔离级别,innodb读取数据是一致性非锁定读,读不加锁,写不阻塞读;但是有些业务场景需要我们读取数据时手动上锁以保证数据的一致性,有两种方式手动上锁实现一致性锁定读
- select ... for update: 给满足条件的行加上排他锁;
- select ... lock in share mode: 给满足条件的行加上共享锁;
注意:手动加锁需要在一个事务中才生效,也即:需要手动开启一个事务,begin /../ commit
四 锁算法
innodb有三种锁算法,分别是:
- Record Lock: 所有当前记录
- Gap Lock: 间隙锁,锁定一个范围,但是不包括记录本身,例如:(-∞,1)(1, 5)(5,+ ∞)
- Next-Key Lock: Record Lock+Gap Lock, 同样锁定一个范围,但是包括记录本身,例如:(-∞,1](1, 5](5,+ ∞)
规则:
- innodb的Record Lock是根据索引项来加锁的,如果查询条件不是根据索引项来查询,会锁定整张表记录
说明:例如:如果在一个事务中执行:select * from t1 where id=4 for update; 其中id并没有设置为主键,也没有添加任何索引,则此时会阻塞其他事务的任何插入 更新 删除操作; - InnoDB通过索引来实现行锁,而不是通过锁住记录。因此,当操作的两条不同记录拥有相同的索引时,也会因为行锁被锁而发生等待。
- 由于InnoDB的索引机制,数据库操作使用了主键索引,InnoDB会锁住主键索引;使用非主键索引时,InnoDB会先锁住非主键索引,再锁定主键索引。
- 当查询的索引是唯一索引时,InnoDB存储引擎会将Next-Key Lock降级为Record Lock,即只锁住索引本身,而不是范围。
- InnoDB对于辅助索引有特殊的处理,不仅会锁住辅助索引值所在的范围,还会将其下一键值加上Gap LOCK。
说明:例如:如果某个列有1,3,5三个索引值,如果在一个事务中执行:select * from t1 where id=3 for update时会同时锁住(1,3]和(3,5)两个区间,其他事务如果插入这个两个区间的值是不得行的,会阻塞,但是插入非这连个区间是可以的,比如插入:insert into t1 values(7) - InnoDB使用Next-Key Lock机制来避免Phantom Problem(幻读问题)。
说明:个人理解在可重复读隔离级别下,Innodb应该是通过Next-Key Lock机制+MVCC机制共同保证不会发生幻读问题, MVCC不能完全解决幻读问题,MVCC可以解决快照读问题,但是当前读问题不能解决,举例说明如下:
1 MVCC能解决快照度的幻读问题
例子1
创建表t2
CREATE TABLE `t2` (
`id` int(11) DEFAULT NULL,
`name` varchar(20) DEFAULT NULL,
KEY `id` (`id`)
) ENGINE=InnoDB
开启两个事务tx1和tx2,先在tx1事务中查询
mysql> begin;
mysql> select * from t2 where id>5;
+------+------+
| id | name |
+------+------+
| 6 | dd |
| 8 | dd |
| 7 | dd |
| 9 | rr |
| 10 | re |
+------+------+
5 rows in set (0.00 sec)
然后事务2中,插入一行记录,并提交
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t2 values(12,"rr");
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
结论:
再在事务一查询相同语句,查询不到12这条记录,因为普通的select是快照读,快照读根据MVCC机制不会读到大于当前事务版本的数据,所以MVCC可以解决快照读的幻读问题
2 MVCC不能解决当前读的幻读问题,当前读是读取最新的数据,更新删除等就是当前读
例子2:
- 事务1执行以下语句,for update
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t2 where id>5 for update;
+------+------+
| id | name |
+------+------+
| 6 | dd |
| 8 | dd |
| 7 | dd |
| 9 | rr |
| 10 | re |
| 12 | rr |
| 13 | rr |
+------+------+
7 rows in set (0.00 sec)
- 然后再事务2中执行,插入14语句
mysql> begin;
Query OK, 0 rows affected (0.01 sec)
mysql> insert into t2 values(14,"rr");
结论:
发现事务2进行锁等待,不能插入,那是因为事务1中for update对加了next-key lock,对区间(5,+∞)进行了加锁,导致这个区间的插入操作都等待锁,所以next-key lock能解决到一些幻读问题
例子3
先在事务1执行下面操作
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t2;
+------+------+
| id | name |
+------+------+
| 1 | dd3 |
| 2 | dd3 |
| 3 | dd |
| 4 | dd |
| 6 | dd |
| 8 | dd |
| 7 | dd |
| 9 | rr |
| 10 | re |
| 12 | rr |
| 13 | rr |
+------+------+
11 rows in set (0.00 sec)
此时事务一种查询到11条数据,事务一不提交,然后开启事务2,在事务2中执行一个插入操作,并commit
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t2 values(14, "22");
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
然后我们再在事务1中执行更新操作,然后相同的查询
mysql> update t2 set name='aa';
Query OK, 12 rows affected (11.66 sec)
Rows matched: 12 Changed: 12 Warnings: 0
mysql> select * from t2;
+------+------+
| id | name |
+------+------+
| 1 | aa |
| 2 | aa |
| 3 | aa |
| 4 | aa |
| 6 | aa |
| 8 | aa |
| 7 | aa |
| 9 | aa |
| 10 | aa |
| 12 | aa |
| 13 | aa |
| 14 | aa |
+------+------+
12 rows in set (0.00 sec)
结论:
我们发现在事务一中读取到了事务二插入的数据,在同一个事务中两次读取的数据不一致,发生了幻读,原因在于发生了一次当前读(更新操作),所以此时MVCC没有解决到幻读问题
通过例子1 2 3我们得出最终结论:
- 在可重复读隔离级别下,MVCC机制可以解决快照读的幻读问题,但是如果有当前读,那就有可能发生幻读了
- 如果有当前读,想要预防幻读,可以通过加行锁,通过Next-Key Lock算法机制保证在记录周围范围能上锁防止幻读问题
引用:
《MYSQL技术内幕 Innodb存储引擎》
《丁奇45讲》
网友评论