系列文章目录
【MySQL 流浪之旅】 第一讲 MySQL 安装
【MySQL 流浪之旅】 第二讲 MySQL 基础操作
【MySQL 流浪之旅】 第三讲 MySQL 基本工具
【MySQL 流浪之旅】 第四讲 MySQL 逻辑备份
【MySQL 流浪之旅】 第五讲 数据库设计的三个范式
目录
系列文章目录
一、什么是锁?
二、锁的类型?
三、锁的算法?
四、锁与事务隔离级别
4.1. 隔离级别简介
4.2. Read Uncommitted(读取未提交内容)
4.3. Read Committed(读取提交内容)
4.4. Repeatable Read(可重读)
4.5. Serializable(可串行化)
五、死锁
一、什么是锁?
锁是实现事务隔离性最广泛使用的技术。以 MySQL InnoDB 为例,数据库的锁有排他锁,共享锁,意向锁,自增锁,间隙锁。
按照粒度分为锁定整个表的表级锁(table-level locking)和锁定数据行的行级锁(row-level locking):
- 表级锁具有开销小、加锁快的特性;但是表级锁的锁定粒度较大,发生锁冲突的概率高,支持的并发度低;
- 行级锁具有开销大,加锁慢的特性;但是行级锁的锁定粒度较小,发生锁冲突的概率低,支持的并发度高。
InnoDB 存储引擎同时支持行级锁(row-level locking)和表级锁(table-level locking),默认情况下采用行级锁。
InnoDB的行锁是实现在索引上的,而不是锁在物理行记录上。所以如果访问没有命中索引,就无法使用行锁,将要退化为表锁(共享行锁上升为共享表锁,排他行锁上升为排他表锁)。
二、锁的类型?
InnoDB存储引擎实现的是行级锁
- 共享锁(S Lock):允许事务读一行数据
- 排他锁(X Lock):允许事务删除或更新一行数据
排他锁和共享锁的兼容性
- 意向共享锁(IS Lock):事务想要获得一张表中某几行的共享锁
- 意向排他锁(IX Lock):事务想要获得一张表中某几行的排他锁
意向排他锁和意向共享锁的兼容性
- 间隙锁:where条件是一个范围时,数据库会锁定区间数据,主要是解决幻读问题。
- 自增锁:自增锁是一种特殊的表级别锁(table-level lock),专门针对事务插入AUTO_INCREMENT类型的列。最简单的情况,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。控制同一sql 语句插入的所有记录的自增id是连续的。
平时还有提到的悲观锁,乐观锁,在数据库层面上没有这个锁的概念,如果要做简单映射,悲观锁可以映射成排他锁,乐观锁是由应用层面保障的,和 DB 的锁概念无关。
- 乐观锁:认为冲突概率低,操作数据时,不会锁定数据,只有在提交修改时,才检查。如果数据已被修改,则回滚,否则提交。
- 悲观锁:一种并发控制方法。认为冲突概率高,每次读写前都加锁。
所以我们说,锁的分类从不同角度来看有不同的类型:
三、锁的算法?
- Record Lock:单个行记录上的锁
- Gap Lock:间隙锁,锁定一个范围,但不包含基本本身
- Next-key Lock:Gap Lock + Record Lock:锁定一个范围,并且锁定记录本身
四、锁与事务隔离级别
4.1. 隔离级别简介
事务的隔离级别是用来描述并发控制机制中不同事务之间隔离程度的参数。以下是几种常见的隔离级别及其特点:
- Read Uncommitted(读取未提交内容)
- Read Committed(读取提交内容)
- Repeatable Read(可重读)
- Serializable(可串行化)
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
Read Uncommitted(读取未提交内容) | 是 | 是 | 是 |
Read Committed(读取提交内容) | 否 | 是 | 是 |
Repeatable Read(可重读) | 否 | 否 | 否 |
Serializable(可串行化) | 否 | 否 | 否 |
4.2. Read Uncommitted(读取未提交内容)
定义:所有事务都可以看到其他未提交事务的执行结果。(本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少)读取未提交的内容也称为脏读,所以在该隔离级别下是存在脏读这种缺陷的。在该隔离级别是允许脏读。
锁的状态:Read Uncommitted这个隔离级别是不加锁的,所有的事物都能任意操作数据,数据是没有任何一致性可言,其实数据就是处于混乱的状态中。
图示
图中所示,会导致什么问题呢?
tab2在B时刻查询到了表的数据,但是如果在C时刻发生了回滚,那么tab1的数据的插入不成功的,其实这时表里是没有数据的,但是事务tab2却查询到了数据,这就导致数据不一致了。
4.3. Read Committed(读取提交内容)
定义:只能读取到已经提交的数据,这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 会出现所谓的不可重复读(Nonrepeatable Read)这种缺陷,因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
锁的状态:该隔离级别下,数据的读取都是不加锁的,但是事务的写入、修改、删除是加锁的。
4.4. Repeatable Read(可重读)
定义:这是MySQL的默认事务隔离级别,他确保一事务的多个会话在并发读取数据时,会看到同样的数据行。
T1 | T2 | T3 |
begin; | begin; | begin; |
root@localhost> select id,class_name,teacher_id from class_teacher; +----+--------------+------------+ | id | class_name | teacher_id | +----+--------------+------------+ | 1 | 初三一班 | 1| | 2 | 初三二班 | 1 | +----+--------------+------------+ 2 rows in set (0.00 sec) | ||
update class_teacher set class_name='初三三班' where id=1; Waiting…… | ||
root@localhost> select id,class_name,teacher_id from class_teacher; +----+--------------+------------+ | id | class_name | teacher_id | +----+--------------+------------+ | 1 | 初三一班 | 1| | 2| 初三二班 | 1 | +----+--------------+------------+ 2 rows in set (0.00 sec) | insert into class_teacher values (null,'初三三班',1); | |
commit; | ||
root@localhost> select id,class_name,teacher_id from class_teacher; +----+--------------+------------+ | id | class_name | teacher_id | +----+--------------+------------+ | 1 | 初三一班 | 1| | 2 | 初三二班 | 1 | | 3 | 初三三班 | 1 | +----+--------------+------------+ 3 rows in set (0.00 sec) |
从上图可以看出,传统的Repeatable Read(可重读),为了保证可重读,读取出来的数据行是加锁的,所以这些行在commit(锁释放)前是不能修改的,但是他不会对新插入的行加锁,所以T3是可以插入数据的,并且再次查询的时候就会看到新插入的的行,这种现象就是幻读。
4.5. Serializable(可串行化)
定义:这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁(每次读都需要获得表级共享锁)。在这个级别,可能导致大量的超时现象和锁竞争,读写相互都会阻塞。因此,会导致大量的超时现象。
锁的状态:MySQL从MVCC并发控制退化为基于锁的并发控制。不区别快照读与当前读,所有的读操作均为当前读,读加读锁 (S锁),写加写锁 (X锁)。Serializable隔离级别下,读写冲突,因此并发度急剧下降,在MySQL/InnoDB下不建议使用。
五、死锁
死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。
show engine innodb status查看死锁信息
死锁通常包括以下四个必要条件:
- 互斥条件(Mutual Exclusion):每个资源只能同时被一个线程占用。如果一个线程占用了某个资源,其他线程就无法同时占用它。
- 请求与保持条件(Hold and Wait):线程在持有某些资源的同时,又请求其他资源。如果线程在等待其他资源时不释放已占用的资源,就可能导致死锁。
- 不可剥夺条件(No Preemption):系统不能抢占线程占用的资源,只能由线程自愿释放。
- 循环等待条件(Circular Wait):多个线程之间形成一个环形等待其他线程释放资源的情况。
为了预防和解决死锁问题,可以采取以下策略:
- 加强互斥条件:尽量减少共享资源的互斥性,或者使用更高级的同步机制来替代基本的互斥锁。
- 加强请求与保持条件:要求数资源时,尽量一次性请求所有需要的资源,而不是逐个请求。
- 加强不可剥夺条件:允许操作系统抢占线程占用的资源,以避免某些线程长时间占用资源而导致死锁。
- 加强循环等待条件:引入资源分配图,检测潜在的死锁情况,然后采取措施来打破等待环路。
- 使用超时:线程在等待资源时,设置一个超时时间,超时后可以放弃等待或者重试。
- 使用死锁检测和恢复机制:系统可以定期检测死锁,并采取措施来解除死锁,如终止某些线程或回滚操作。