1.锁的概述
锁:是计算机协调多个进程或者线程并发访问某一资源的机制。
保证数据并发访问的一致性/有效性,锁冲突也是影响数据库并发访问的性能的一个重要因数。
mysql不同存储引擎支持不同的锁机制
| 表级锁:table-level,开销小,加锁快;锁定粒度大,发生锁冲突的概率高,并发度最低。
| 行级锁:row-level,开销大,加锁慢;会出现死锁;锁定力度最小,发生锁冲突的概率最低,并发度也最高。
| 页面锁:page-level,开销与加锁事件介于表锁和行锁之间;会出现死锁;锁定粒度也是介于表锁/行锁之间;并发度一般。
2.锁的分类
| 按照锁的粒度划分:全局锁、表锁、行锁、页锁
| 按照锁的加锁方式划分:共享锁(S)、排它锁(X)
| 意向锁(表锁):意向共享锁(IS),意向排他锁(IX)
| 还有两种思想上的锁:悲观锁、乐观锁。
| InnoDB中有几种行级锁实现方式:Record Lock、Gap Lock、Next-key Lock
| 共享锁(S读锁):在锁定期间,多个用户可以读取同一个资源,读取过程中数据不会发生变化。
| 排他锁(X写锁):在锁定期间,只允许一个用户写入数据,其它用户的读取,写入等操作都会被拒绝。
| 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
| 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
| 乐观锁:利用数据版本号(version)机制是乐观锁最常用的一种实现方式。提交时候对比读取之前的版本号。
| 悲观锁:悲观锁(Pessimistic Locking)是指在数据处理过程,使数据处于锁定状态,一般使用数据库的锁机制实现。
| Record Lock:对索引项加锁。
| Gap Lock:间隙锁,对索引项之间的“间隙”,第一条记录前的“间隙”或者最后一条记录后的“间隙”加锁。
| Next-key Lock:Record Lock+Gap Lock;前2种的组合,对记录及其前面的间隙加锁。
3.全局锁
Server层实现的
|定义:全局锁就是对整个数据库实例加锁。
|命令: Flush tables with read lock (FTWRL)
|作用:使得整个数据库处于只读状态的时候,以下语句会被阻塞:数据的增删改、建表、修改表结构和更新类事务的提交语句。
|使用场景:做全库逻辑备份。也就是把整库每个表都 select 出来存成文本。通过 FTWRL 确保不会有其他线程对数据库做更新,然后对整个库做备份。
|mysqldump:支持事务的存储引擎可以使用逻辑备份工具是 mysqldump。当 mysqldump 使用参数–single-transaction 的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。
为什么不使用set global readonly=true 的方式呢?
1.readonly 的值会被用来做其他逻辑如从库只读
2.在异常处理机制上有差异;FTWRL 命令发生异常断开,MySQL 会自动释放这个全局锁。而设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。
INSERT into `crawler_list_gift` (`user_id`) VALUES ('douyin3');
UPDATE `crawler_list_douyin` set `status` = 1 WHERE `id` = 1;
//加锁
Flush tables with read lock;
//解锁
unlock tables;
//加锁后进行增删改操作提示错误信息
Error Can't execute the query because you have a conflicting read lock
4.表锁
Server层实现的
MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。
4.1表锁
表锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Read Write)
线程 A 中执行 lock tables t1 read, t2 write; 则其他线程写 t1、读写 t2 的语句都会被阻塞。线程 A 在执行 unlock tables 之前,也只能执行读 t1、读写 t2 的操作。连写 t1 都不允许,自然也不能访问其他表。
SELECT * FROM `test1`;
SELECT * FROM `test2`;
SELECT * FROM `test3`;
//加锁
lock tables test1 read, test2 write;
UPDATE `test2` set `status`=1 WHERE `id` =1;
Error : Table 'test1' was locked with a READ lock and can't be updated
//解锁
unlock tables
4.2 MDL表锁
MDL 在 MySQL 5.5 版本中引入了 MDL,不需要显式使用,在访问一个表的时候会被自动加上。
作用:保证读写的正确性。防止执行查询期间表结构的变更,保证变更表结构操作的安全性。防止DDL和DML并发的冲突。
MDL读锁:对表做增删改查的操作加MDL读锁。
MDL写锁:对表结构做变更操作加MDL写锁。
读锁之间不互斥;读写锁之间、写锁之间是互斥的;如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
//图示语句
BEGIN;
SELECT * FROM `test1`;
rollback;
commit;
问题:sessionA查询会被阻塞吗?
5.页锁
6.行锁
行锁是在引擎层实现的
在 InnoDB 事务中,行锁是在需要的时候才加上的,要等到事务结束时才释放。这个就是两阶段锁协议。
事务B会被事务A阻塞
InnoDB的行锁模式和加锁方式
InnoDB实现了两种类型的行锁
共享锁(S)/ 排他锁(X)
为了允许行锁和表锁共存,实现多粒度机制,InnoDB还有两种内部使用的意向锁,这两种意向锁都是表锁。
意向共享锁(IS)/ 意向排他锁(IX)
//共享锁获得
select * from `test1` where *** lock in share mode;
//排他锁获得:
select * from `test1` where *** for update;
//示例代码语句
set autocommit=0;
SELECT * FROM `test1`;
SELECT * FROM `test1` WHERE `id` = 2 lock in share mode;
UPDATE `test1` SET `name` = '陈' WHERE `id` = 2;
//示例代码语句
//session A
set autocommit = 0;
SELECT * FROM `test1` WHERE `id` = 3;
SELECT * FROM `test1` WHERE `id` = 3 FOR UPDATE;
SELECT * FROM `test1` WHERE `id` = 3 lock in share mode;
UPDATE `test1` set `name` = 'test1' where `id` = 3;
COMMIT;
//session B
set autocommit = 0;
SELECT * FROM `test1` WHERE `id` = 3;
SELECT * FROM `test1` WHERE `id` = 3 lock in share mode;
SELECT * FROM `test1` WHERE `id` = 3 for UPDATE;
InnoDB行锁的实现方式
InnoDB行锁是通过给索引上的索引项加锁来实现的,如果没有索引,InnoDB将通过隐藏的聚簇索引来对记录加锁。
InnoDB的行锁分为3种情形:Record lock / Gap lock / Next-key lock
如果不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,实际效果跟表锁一样。相同的索引值会出现锁冲突。
//session A
set autocommit=0;
SELECT * FROM `test_record1` WHERE `user_id` = '1' for update;
COMMIT;
//session B
set autocommit=0;
SELECT * FROM `test_record1` WHERE `user_id` = '2' for update;
SELECT * FROM `test_record1` WHERE `name` = 'name33' for update;
COMMIT;
当表有多个索引,不同的事务可以使用不同的索引锁定不同的的行。
即便使用索引字段,但是否使用索引检索数据由MySQL决定,在MySQL认为使用全表扫描效率高的情况下,就不会使用索引,这种情况下也会对所有记录加锁。
Gap Lock(间隙锁)
间隙锁是在可重复读隔离级别下才会生效
为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)。
顾名思义,间隙锁,锁的就是两个值之间的空隙。
跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系
目的:保护这个间隙,不允许插入值
//隔离级别需要时可重复读RR
select @@transaction_isolation;
//session A
set autocommit=0;
SELECT * FROM `test_gap` where `age` = 7 lock in share mode;
//session B
set autocommit=0;
SELECT * FROM `test_gap` where `age` = 7 for update;
//session C 出现锁等待
set autocommit=0;
INSERT INTO `test_gap` (`age`) VALUES (8);
Next-key锁
当我们用范围条件检索数据,并请求锁时,InnoDB会给符合的已有记录数据记得索引项加锁;对于健值在范围内但不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的Next-Key锁。
目的:防止幻读 / 满足恢复和复制的需要
//mysql 8 以前 是tx_isolation
//现在 是transaction_isolation
select @@transaction_isolation;
//session A
select @@transaction_isolation;
set autocommit=0;
SELECT * FROM `test_next` WHERE `id` > 5 for UPDATE;
ROLLBACK;
//session B
select @@transaction_isolation;
set autocommit=0;
UPDATE `test_next` SET title='chen' WHERE `id` = 5;
INSERT INTO `test_next` (`user_id`) VALUES ('123');
UPDATE `test_next` SET title='chen' WHERE `id` = 6;
ROLLBACK;
InnoDB中不同的sql在不同的隔离级别下的锁比较
如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放
加锁规则
原则 1:加锁的基本单位是 next-key lock。希望你还记得,next-key lock 是前开后闭区间。
原则 2:查找过程中访问到的对象才会加锁。
优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
7.死锁
概念:当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。
Deadlock found when trying to get lock; try restarting transaction
//session A
set autocommit=0;
SELECT * FROM `test2` WHERE `id` = 1 for UPDATE;
SELECT * FROM `test3` WHERE `id` = 1 for UPDATE;
ROLLBACK;
//session B
set autocommit=0;
SELECT * FROM `test3` WHERE `id` = 1 for UPDATE;
SELECT * FROM `test2` WHERE `id` = 1 for UPDATE;
ROLLBACK;
两个事务都需要对方持有的排他锁才能继续完成事务,这种循环锁等待就是典型的死锁。
进入死锁后的系统策略:
1.直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。
2.发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。
3.拆分热点更新数据行
1.策略1的弊端:时间太长不符合并发系统的设计,时间太短会误伤原本只是锁等待不是死锁的线程。
2.策略2的弊端:每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个时间复杂度是 O(n) 的操作。消耗大量的 CPU 资源,却执行不了几个事务。
//超时事件默认50s
select @@innodb_lock_wait_timeout
//默认为on/1
select @@innodb_deadlock_detect
业务上的策略:
1.控制并发度,比如同一行同时最多只有 10 个线程在更新,那么死锁检测的成本很低。(需结合系统策略2,需更改MySQL源码或者中间件,控制并发度)
2.将一行改成逻辑上的多行来减少锁冲突。(使得业务逻辑更加复杂)
如何减少锁冲突和死锁
1.尽量使用较低的隔离级别;
2.精心设计索引,并尽量使用索引访问数据,使加锁更精确,从而减少锁冲突的机会;
3.选择合理的事务大小,小事务发生锁冲突的几率也更小;
4.给记录集显式加锁时,最好一次性请求足够级别的锁。比如要修改数据,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁;
5.不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会;
6.尽量用相等条件访问数据,这样可以避免Next-Key锁对并发插入的影响;
7.不要申请超过实际需要的锁级别;除非必须,查询时不要显示加锁;
对于一些特定的事务,可以使用表锁来提高处理速度或减少发生死锁的几率。