锁
数据库锁机制就是为了保证数据的一致性,使各种共享资源在被访问时变的有序而设计的一种规则。
MySQL不同的存储引擎支持不同的锁机制:
- <font color='red'>myisam</font>和<font color='red'>memory</font>存储引擎采用的是表级锁;
- <font color='red'>innodb</font>存储引擎既<font color='orange'>支持行级锁,也支持表级锁</font>,但默认情况下<font color='orange'>采用行级锁</font>。
MySQL两种锁特性归纳:
表锁:开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低
-
行锁:开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高
一、InnoDB锁的类型
InnoDB的行锁类型主要有<font color='red'>读锁(共享锁,S锁)、写锁(排他锁,互斥锁,X锁)、意向锁和MDL锁</font>
读锁
<font color='red'>一个事务获取了一个数据行的读锁,其他事务能获得该行对应的读锁,但不能获得写锁</font>(一个事务在读取一行数据时,其他事务也可以读,但不能对该数据进行修改)
有两种select方式应用:
- <font color='red'>一致性非锁定读:</font>自动提交模式下的select查询语句,不需要加任何的锁,直接返回查询结果
- 通过<font color='orange'>select……lock in share mode</font>在被读取时加一个读锁,其他的事务可以读,但想要申请写锁就会被阻塞
写锁
<font color='red'>如果事务A对数据加上排他锁后,则其他事务不能再对该数据加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。</font>
只有一个应用:
<font color='orange'>select for update</font>,对读取的数据上加一个写锁,其他的事务不能对被锁定的行加上任何的锁,否则会被阻塞
MDL锁(Meta Data Lock)
该锁用来保护表中元数据的信息;<font color='red'>元数据即表结构</font>。在一个事务中,表开启了查询事务后,会自动获取一个MDL锁,那另一个事务就不可以执行任何的DDL语句操作。
意向锁
需要强调一下,意向锁是一种<font color='red'>不与行级锁冲突表级锁</font>,这一点非常重要。意向锁分为两种:
-
意向共享锁
(intention shared lock, IS):事务有意向对表中的某些行加<font color='orange'>共享锁(S锁)</font>
-- 事务要获取某些行的 S 锁,必须先获得表的 IS 锁。 SELECT column FROM table ... LOCK IN SHARE MODE;
-
意向排他锁
(intention exclusive lock, IX):事务有意向对表中的某些行加<font color='orange'>排他锁(X锁)</font>
-- 事务要获取某些行的 X 锁,必须先获得表的 IX 锁。 SELECT column FROM table ... FOR UPDATE;
即:<font color='red'>意向锁是有数据引擎自己维护的,用户无法手动操作意向锁</font>,在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。
二、InnoDB行锁种类
记录锁
是通过给<font color='orange'>索引上的索引项加锁</font>来实现记录锁的(行锁)。<font color='orange'>只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!</font>
- 在不通过索引条件查询的时候,InnoDB使用的是表锁,而不是行锁。
- 由于MySQL的行锁是针对<font color='red'>索引加的锁</font>,不是针对记录加的锁,所以<font color='red'>虽然是访问不同行的记录</font>,但是如果是使用相同的索引键,<font color='red'>是会出现锁冲突的。</font>
- 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
- 即便在条件中使用了索引字段,但是否使用索引来检索数据是由<font color='red'>MySQL通过判断不同执行计划的代价来决定</font>的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。
间隙锁
在<font color='red'>RR(可重复读)</font>这个事务隔离级别,为了避免出现幻读,引入间隙锁。间隙锁只锁定<font color='balue'>行记录数据的范围</font>(如取的范围为9,那就<font color='red'>取9以下</font>离得最近的一个值到<font color='red'>取9以上</font>离得最近的一个值,这个值时存在数据库中的),不包含数据本身,不允许在此范围内插入任何数据。
临键锁(Next-Key Locks)
<font color='red'>临键锁时记录锁和间隙锁的组合</font>,即锁住了记录也锁住了范围。 临键锁的主要目的,也是为了<font color='red'>避免幻读</font>。
三、锁等待和死锁
锁等待
锁等待是指一个事务过程中产生的锁,其他事务需要等待上一个事务释放它的锁,才能占用资源。如果该事务一直不释放,就需要持续等待下去,直到超过锁等待时间,会报一个等待超时的错误。
查看锁等待时间:
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
修改锁等待时间为5秒:
set innodb_lock_wait_timeout = 5;
死锁
死锁就是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,就是所谓的锁资源请求产生了回路现象,即死循环。
死锁常见的报错:
Deadlock found when trying to get lock;
try restarting transaction;
避免死锁的方法
- 如果不同程序会并发存取多个表,或者涉及多行记录时,尽量约定以相同的顺序访问表,可以大大降低死锁的机会
- 业务中尽量采用小事务,避免使用大事务,要及时提交或回滚事务,可减少死锁产生的概率
- 在同一事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生的概率
- 对于非常容易产生死锁的业务部分,可以尝试使用升级锁粒度,通过表锁定来减少死锁产生的概率
四、乐观锁、悲观锁
悲观锁
悲观锁的特点是<font color='red'>先获取锁,再进行业务操作</font>,即“悲观”的认为获取锁是非常有可能失败的,因此要<font color='orange'>先确保获取锁成功再进行业务操作</font>。通常所说的“一锁二查三更新”即指的是使用悲观锁。
通过常用的<font color='orange'>select … for update</font>操作来实现悲观锁。当数据库执行<font color='orange'>select for update</font>时会获取被<font color='orange'>select</font>中的数据行的行锁,因此其他并发执行的<font color='orange'>select for update</font>如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。<font color='orange'>select for update</font>获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。
乐观锁
乐观锁的特点<font color='red'>先进行业务操作,不到万不得已不去拿锁</font>。即“乐观”的认为拿锁多半是会成功的,因此在<font color='orange'>进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好。</font>
五、MVCC
MVCC是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。
-
当前读
像<font color='orange'>select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)</font>这些操作都是一种当前读。读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁 -
快照读
像不加锁的select操作就是快照读,即<font color='orange'>不加锁的非阻塞读</font>;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
快照读和MVCC的关系
- 准确的说,MVCC多版本并发控制指的是 “维持一个数据的多个版本,使得读写操作没有冲突” 这么一个概念。仅仅是一个理想概念
- 而在MySQL中,实现这么一个MVCC理想概念,我们就需要MySQL提供具体的功能去实现它,而快照读就是MySQL为我们实现MVCC理想模型的其中一个具体非阻塞读功能。而相对而言,当前读就是悲观锁的具体功能实现
- 要说的再细致一些,快照读本身也是一个抽象概念,再深入研究。MVCC模型在MySQL中的具体实现则是由
3个隐式字段
,undo日志
,Read View
等去完成的,具体可以看下面的MVCC实现原理
MVCC的好处
多版本并发控制(MVCC)是一种用来解决读-写冲突
的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以MVCC可以为数据库解决以下问题
在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
-
同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
日志
对MVCC有帮助的实质是update undo log
,undo log
实际上就是存在rollback segment
中旧记录链,它的执行流程如下:
*一、* 比如一个有个事务插入persion表插入了一条新记录,记录如下,name
为Jerry, age
为24岁,隐式主键
是1,事务ID
和回滚指针
,我们假设为NULL
[图片上传失败...(image-6a1bd7-1609831716947)]
*二、* 现在来了一个事务1
对该记录的name
做出了修改,改为Tom
- 在
事务1
修改该行(记录)数据时,数据库会先对该行加排他锁
- 然后把该行数据拷贝到
undo log
中,作为旧记录,既在undo log
中有当前行的拷贝副本 - 拷贝完毕后,修改该行
name
为Tom,并且修改隐藏字段的事务ID为当前事务1
的ID, 我们默认从1
开始,之后递增,回滚指针指向拷贝到undo log
的副本记录,既表示我的上一个版本就是它 - 事务提交后,释放锁
[图片上传失败...(image-db101f-1609831716947)]
*三、* 又来了个事务2
修改person表
的同一个记录,将age
修改为30岁
- 在
事务2
修改该行数据时,数据库也先为该行加锁 - 然后把该行数据拷贝到
undo log
中,作为旧记录,发现该行记录已经有undo log
了,那么最新的旧数据作为链表的表头,插在该行记录的undo log
最前面 - 修改该行
age
为30岁,并且修改隐藏字段的事务ID为当前事务2
的ID, 那就是2
,回滚指针指向刚刚拷贝到undo log
的副本记录 - 事务提交,释放锁
[图片上传失败...(image-334967-1609831716947)]
从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log
成为一条记录版本线性表,既链表,undo log
的链首就是最新的旧记录,链尾就是最早的旧记录(当然就像之前说的该undo log的节点可能是会purge线程清除掉,向图中的第一条insert undo log,其实在事务提交之后可能就被删除丢失了,不过这里为了演示,所以还放在这里)