文章目录
什么是锁机制
锁是计算机协调多个进程或线程并发访问某一资源的控制.
- 锁不仅仅限于数据库领域,在计算机中,当多个进程或线程并发的访问某个数据的时候,对于一些重要敏感的数据,为了保证数据的
完整性
和一致性
,我们需要保证最多只有一个线程在访问,所以诞生了锁机制。 - 锁机制的作用是对并发操作进行控制
- 在MySQL中,锁保证了事务的隔离性
并发的访问相同数据的情况
读是查询操作,写是增删改操作.
读读
两个事务并发读取相同数据的情况.
- 读读操作都是查询操作,本身对数据没有什么影响,所以没有什么问题.
写写
两个并发事务相继对同一个数据进行修改的情况.
- 写写情况下,会发生数据脏写问题,脏写问题非常严重,任何的事物隔离级别都不允许发生这个问题
- 为了避免脏写,数据库强制未提交事务必须排队执行,通过锁实现强制.
- 假设事务T1想要修改一条记录,如果内存中没有与之相关的锁结构,会生成一个锁结构与它进行关联。简化图:.
锁结构中,trx标识该锁结构属于哪个事务,is_waiting标识该事务是否在等待
- T1之前没有任何事务对该记录进行修改,所以is_waiting是false,表示无须等待,可以直接修改,即加锁成功.在事务T1未提交之前,如果事务T2也想对该记录修改,就需要等待,is_waiting是true,需要等待,即加锁失败.
- 在事务T1提交后,锁会释放,交给T2,T2的is_waiting变为false,然后T2就会开始执行.
读写/写读
两个并发事务,对于同一条数据,一个读取,一个修改。
- 读写/写读情况下会发生脏读、不可重复读、幻读问题
- 解决这些问题有两个办法:一是对读操作使用MVCC,写操作进行加锁.方法二是读写操作都进行加锁.
锁的分类
- 对数据的操作类型划分:共享锁(读锁)、排他锁(写锁)
- 从锁粒角度划分:表锁、行锁、页锁
- 从锁的态度进行划分:悲观锁、乐观锁
- 从加锁方式划分:隐式锁、显式锁
- 从其他角度划分:全局锁、死锁
共享锁(S锁)
- 共享锁(
Shared Lock
) 也叫读锁,对于同一份数据,多个事务进行读取时可以同时进行互不影响
排他锁( X锁)
- 排他锁(
Exclusive Lock
)也叫写锁、X锁,对于同一份数据,当一个数据进行写操作时,禁止其他事务的读、写操作.
对于Innodb引擎,读锁和写锁可以加在表上,也可以加在行上.
两个锁之间的兼容性
锁定读操作
读取时加S锁:
SELECT ... Lock in share mode;
或者
select....for share;
为这条记录添加了S锁后,就不允许其他事务获得该记录的X锁
读取时加X锁:
SELECT.... for update:
为这条记录加了X锁后,就不允许其他事务获得该记录的S、X锁.
锁定写操作
写操作只有三种:delete、update、insert
对于delete操作:获取记录位置,然后获取它的X锁,再进行deletemark操作----可以理解为获取X的锁定读
对于update操作:
获取记录位置,相当于获取X锁,锁定读。然后主键更改,就删除,并重新添加一个,如果没更改,就直接在原记录中更新.
对于insert操作:
并不加锁.有隐式锁来保护不被其他事务打扰.
表锁
表锁会锁住整张表,是MySQL中最基本的锁策略,并且不依赖于任何存储引擎.
表级别的S、X锁.
- 对表执行普通的增加删除修改更新操作时,不会添加表级的S、X锁.
- 当事务A对表执行增加删除修改更新操作,事务B并发执行影响表结构的操作(如
alter table
、drop table
)时,才会有表级锁的出现,对事务进行阻塞 - 同理,事务A对表结构进行修改时,如果事务B并发执行普通的增删查改,也会发生阻塞.
为表添加S、X锁
LOCK TABLES T read;
LOCK TABLES t WRITE;
虽然可以手动添加表锁,但一般很少使用,因为有更强大的行锁
限制关系
对限制关系的演示
初始化数据
mysql> create table mylock(
-> id int not null primary key auto_increment,
-> name varchar(20)
-> )engine myisam;
Query OK, 0 rows affected (0.06 sec)
mysql> insert into mylock(name) values('a');
Query OK, 1 row affected (0.04 sec)
加锁
mysql> lock tables mylock read;
Query OK, 0 rows affected (0.00 sec)
加锁后自己可读
mysql> select*from mylock;
+----+------+
| id | name |
+----+------+
| 1 | a |
+----+------+
1 row in set (0.00 sec)
自己不可写
mysql> update mylock set name='a1' where id=1;
## ERROR 1099 (HY000): Table 'mylock' was locked with a READ lock and can't be updated;
#自己不可操作其他表
mysql> select *from account;
## ERROR 1100 (HY000): Table 'account' was not locked with LOCK TABLES
他人可读
mysql> select* from mylock;
+----+------+
| id | name |
+----+------+
| 1 | a |
+----+------+
1 row in set (0.00 sec)
他人不可写 会阻塞
mysql> update mylock set name='a2' where id=1;
释放锁
mysql> unlock tables ;
Query OK, 0 rows affected (0.00 sec)
改为写锁
mysql> lock tables mylock write;
Query OK, 0 rows affected (0.00 sec)
自己可以读取
mysql> select*from mylock;
+----+------+
| id | name |
+----+------+
| 1 | a |
+----+------+
1 row in set (0.00 sec)
自己可以写入
mysql> update mylock set name='a3' where id=1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
自己不能操作其他表
mysql> select*from account;
ERROR 1100 (HY000): Table 'account' was not locked with LOCK TABLES
他人读取会阻塞
mysql> select*from mylock;
## 阻塞
他人写入会阻塞
mysql> update mylock set name='a4' where id=1;
## 阻塞
意向锁.
事务A对某一行数据加了一个锁,事务B如果想对整张表加一个锁,就需要检查该表中是否已经存在锁,在数据量非常大的情况下,一页一页的检索是非常复杂的一件事情.此时就可以用到意向锁.
- 意向锁是表级别的锁,对整张表添加
- 意向锁是存储引擎自动添加,不需要手动添加
- 意向锁不会与行锁发生冲突
- 意向锁分两种:意向共享锁、意向排他锁
- 如果我们给某一行数据加上了排他锁,数据库会自动给更大一级的空间(比如数据页、数据表)加上意向锁,告诉他人这个数据页或数据表已经有人上过排他锁了
意向共享锁(IS锁)
存在事务想对表中的某个数据添加共享锁
--事务想要获取某项行的S锁,必须先获得表的IS锁
select...from table...lock in share mode;
意向排他锁(IX锁)
存在事务想对表中的某个数据添加排他锁
---事务想要获得某项行的X锁,必须先获得表的IX锁
select ... from table...for update;
- 意向锁之间相互兼容,互不影响
- 意向锁和表级别的X,S锁不兼容,除了IS和S
自增锁
- 特殊的表级锁,事务向
AUTO_INCREMENT
字段添加新数据时就会持有自增锁 - 如果事务A正在向自增列添加新数据,此处事务B尝试
INSERT
,就会被阻塞. - 不同锁模式下运行机制不同,行为不同,锁模式通过参数
innodb_autoinc_lock_mode
进行设置 - 自增列必须是索引才能用自增锁.
元数据锁
- 元数据锁(
meta data lock
,MDL锁)是表锁. - 当事务对一个表进行增删改查时,会自动添加MDL读锁,当事务对表结构进行修改的时候,会自动添加MDL写锁
- 自动添加MDL锁的作用就是防止表在增删改时,表结构被改动。
- 读读不互斥、读写、写写互斥
- 自动添加,无须手动
行锁
- 对某一条记录进行加锁
- 优势是锁定单位小,所以冲突概率低,并发性非常高,劣势是会用太多的锁资源,加锁慢,容易出现死锁问题
- Innodb和MyISAM引擎最大的不同:支持事务;支持行锁
记录锁
- 非常普通的锁,就是对某一条记录加锁
- 分为S、X型。
- 当事务获的某条记录的S型记录锁后,其他事务只能获取S型
- 当事务获得某条记录的X型锁后,其他事务只能等待
间隙锁
- 我们可以通过加锁的方式解决幻读问题,但是问题是幻读产生的那些幻影记录一开始并不存在,无法加锁,为了解决这个问题,就产生了间隙锁(
gap锁
) - 间隙锁只有一个作用:防止插入幻影记录
- 比如我们对id=8的记录加上间隙锁,那么上一条记录和id=8的记录之间就不允许插入新的记录,如果其他事务想要插入新的记录,会被阻塞.
- 对某记录之后的区间加上gap锁,需要用两条伪记录:
Infimum
记录,表示该页中最小记录;Supremum
记录,表示该页中最大的记录; - 直接对
Supremum
记录加gap锁即可
临键锁
- 临键锁=记录锁+间隙锁
- 作用:锁住某条记录,并阻止其他事务在该事务前面的间隙插入事务
- 分为临键S和临键X锁,互斥情况和记录锁相同
begin;
select*from student where id<=8 and id>3 for update;
插入意向锁
- 插入意向锁是在插入一条记录时,由
INSERT
操作产生的一种间隙锁. - 插入意向锁本质是间隙锁
- 在插入一条记录时,需要看插入位置是否被别的事务加了间隙锁,如果有,需要等待,知道间隙锁的事物提交.等待时,会生成一个锁结构,表名某事务有在某位置插入数据的意图,这个锁结构就是意向锁
- 插入意向锁互不排斥
当事务T1结束后,事务T2和T3都会获取到插入意向锁:
页锁
- 粒度大小介于表锁和行锁之间
- 开销介于表锁和行锁之间
- 锁的空间大小有限,超过大小时,会自动进行锁升级.
粒度越大,开销越小。粒度大小:表>页>行。开销:表<页<行
其他锁
全局锁
- 对整个数据库实例加锁,整个库处于只读的状态
- 增删查改、修改表结构的等都会阻塞
- 使用场景:全库备份(对数据库进行备份时,不允许操作)
- 命令:
Flush tables with read lock
死锁
- 两个或多个事务在同一资源上进行占用,都有对方的需要的锁,但都不释放,陷入死循环
- 例如下图:事务1对id=1的记录设置了X锁,事务2又对id=2的记录设置了X锁。事务1在尝试更新记录2时陷入阻塞,事务2再尝试更新记录1时陷入阻塞。然后两个事务互相僵持,陷入死锁状态
死锁条件
- 必须有两个事务
- 每个事务都持有锁,还申请新的锁
- 新锁恰好为对方所有
如何处理死锁
-
方法1:等待,直到超时
-
两个事务互相等待,当一个事务等待时间超过设置的阈值时,就将其回滚。另一个事务就可以继续进行。
-
Innodb中,使用
innodb_lock_wait_timeout
设置时间 -
方法2:使用死锁检测进行死锁处理
-
innodb中有wait-for graph算法主动检测死锁,每次加锁需要等待时就会触发
-
死锁检测原理:
数据库会保存锁的信息链表
和事务等待链表
根据上面两个信息,可以画出等待有向图:
在等待图中,如果有环的存在,就是死锁。
- 如果出现死锁,存储引擎就会回滚
操作量最小的事务
.