Mysql的锁机制是我们在使用Mysql的时候所遇见的最为常见的一个处理并发的机制,尤其因为InnoDB引擎支持事务的特性,因此对于锁机制显得更加重要。
下面我们好好聊聊Mysql的锁机制。
数据库锁设计的初衷是处理并发问题。作为多用户共享的资源,当出现并发访问的时候,为了保证数据的一致性,数据库需要合理地控制资源的访问规则
。而锁就是用来实现这些访问规则的重要机制。
数据表就好比您开的一家酒店,而每行数据就像酒店的房间,如果大家随意进出,就会出现多人抢夺同一个房间的情况,而在房间上装上锁,申请到钥匙的人才可以入住并且将房间锁起来,其他人只有等他用完退房后才可以再次使用,这样保证了房间的一致性,方便酒店进行管理。
MySQL锁机制的初衷便是如此,当然,MySQL数据库由于其自身架构的特点,存在多种数据存储引擎,每种存储引擎所针对的应用场景特点都不太一样,为了满足各自特定应用场景的需求,每种存储引擎的锁定机制都是为各自所面对的特定场景而优化设计,所以各存储引擎的锁定机制也有较大区别。
先甩一张图:
按锁粒度从大到小分类:表锁,页锁和行锁;以及特殊场景下使用的全局锁
如果按锁级别分类则有:共享(读)锁、排他(写)锁、意向共享(读)锁、意向排他(写)锁;
以及Innodb引擎为解决幻读等并发场景下事务存在的数据问题,引入的Record Lock(行记录锁)、Gap Lock(间隙锁)、Next-key Lock(Record Lock + Gap Lock结合)等;
还有就是我们面向编程的两种锁思想:悲观锁、乐观锁。
接下来我们主要分析几个比较重要的概念。
表级别的锁定是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。
当然,锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,大大降低并发度。
读锁(read lock),也叫共享锁(shared lock) 针对同一份数据,多个读操作可以同时进行而不会互相影响(select)
写锁(write lock),也叫排他锁(exclusive lock) 当前操作没完成之前,会阻塞其它读和写操作(update、insert、delete)
对整张表加锁
开销小
加锁快
无死锁
锁粒度大,发生锁冲突概率大,并发性低
注:MyISAM的读写锁调度是写优先,这也是MyISAM不适合做写为主表的引擎,因为写锁以后,其它线程不能做任何操作,大量的更新使查询很难得到锁,从而造成永远阻塞。
// 隐式上锁(默认,自动加锁自动释放)
select //上读锁
insert、update、delete //上写锁
// 显式上锁(手动)
lock table tableName read;//读锁
lock table tableName write;//写锁
// 解锁
unlock tables;//所有锁表
读锁(read lock),也叫共享锁(shared lock) 允许一个事务去读一行,阻止其他事务获得相同数据集的读锁
写锁(write lock),也叫排他锁(exclusive lock) 允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享锁和排他锁
意向共享锁(IS):一个事务给一个数据行加共享锁时,必须先获得表的IS锁
意向排它锁(IX) 一个事务给一个数据行加排他锁时,必须先获得该表的IX锁
对一行数据加锁
开销大
加锁慢
会出现死锁
锁粒度小,发生锁冲突概率最低,并发性高
更新丢失 解决:让事务变成串行操作,而不是并发的操作,即对每个事务开始---对读取记录加排他锁
脏读 解决:隔离级别为Read uncommitted,MVCC
不可重读 解决:MVCC 中ReadVIew 的快照读
幻读 解决:使用Next-Key Lock+Gap算法来避免或者MVCC
// 隐式上锁(默认,自动加锁自动释放)
select //不会上锁
insert、update、delete //上写锁
// 显示上锁(手动)
select * from tableName lock in share mode;//读锁
select * from tableName for update;//写锁
// 解锁(手动)
提交事务(commit)
回滚事务(rollback)
kill 阻塞进程
开销、加锁时间和锁粒度介于表锁和行锁之间,会出现死锁,并发处理能力一般(此锁不做多介绍)
对于
共享(读)锁
、排他(写)锁
,比如咱们住酒店,入住前顾客都是有权看房的,只看不住想白嫖都是可以的,前台小姐姐会把门给你打开。当然,也允许不同的顾客一起看(共享 读
)。看房时房间相当于公共场所,小姐姐嘱咐不能乱涂乱画,也不能偷喝免费的矿泉水。。如果你觉得不错,偷偷跑到前台要定这间房,交钱后会给你这个房间的钥匙并将房间状态改为已入住,不再允许其他人看房(排他写)。
对了,当办理入住时前台小姐姐也会通知看房的别人说这间房已经有人定了!!!然后你锁上门哼着歌儿,开始干那些见不得人的事儿~~直到你退房前,其他人无法在看你的房。
可见,读锁是可以并发获取的(共享的),而写锁只能给一个事务处理(排他的)。当你想获取写锁时,需要等待之前的读锁都释放后方可加写锁;而当你想获取读锁时,只要数据没有被写锁锁住,你都可以获取到读锁,然后去看房。
另外还有意向读\写锁,严格来说他们并不是一种锁,而是存放表中所有行锁的信息。就像我们在酒店,当我们预定一个房间时,就对该行(房间)添加 意向写锁,但是同时会在酒店的前台对该行(房间)做一个信息登记(旅客姓名、男女、住多长时间、家里几头牛等)。
大家可以把意向锁当成这个酒店前台,它并不是真正意义上的锁(钥匙),它维护表中每行的加锁信息,是共用的。后续的旅客通过酒店前台来看哪个房间是可选的,那么,如果没有意图锁,会出现什么情况呢?假设我要住房间,那么我每次都要到每一个房间看看这个房间有没有住人,显然这样做的效率是很低下的。
上文中,我们多次提到了MVCC这个东西,那么它究竟是个什么东西呢?
全称Multi-Version Concurrency Control,即多版本并发控制
,主要是为了提高数据库的并发性能
。以下文章都是围绕InnoDB引擎来讲,因为MyIsam不支持事务。
同一行数据平时发生读写请求时,会上锁阻塞
住。但mvcc用更好的方式去处理读—写请求,做到在发生读—写请求冲突时不用加锁
。
这个读是指的快照读
,而不是当前读
,当前读是一种加锁操作,是悲观锁
。
它读取的数据库记录,都是当前最新
的版本
,会对当前读取的数据进行加锁
,防止其他事务修改数据。是悲观锁
的一种操作。
如下操作都是当前读:
select lock in share mode (共享锁)
select for update (排他锁)
update (排他锁)
insert (排他锁)
delete (排他锁)
串行化事务隔离级别
快照读的实现是基于多版本
并发控制,即MVCC,既然是多版本,那么快照读读到的数据不一定是当前最新的数据,有可能是之前历史版本
的数据。
如下操作是快照读:
不加锁的select操作(注:事务级别不是串行化)
MVCCC
是“维持一个数据的多个版本,使读写操作没有冲突”的一个抽象概念
。
这个概念需要具体功能去实现,这个具体实现就是快照读
。
3、MVCC
1、数据库并发场景
读-读
:不存在任何问题,也不需要并发控制
读-写
:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
写-写
:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
2、MVCC解决并发哪些问题
mvcc用来解决读—写冲突的无锁并发控制,就是为事务分配单向增长
的时间戳
。为每个数据修改保存一个版本
,版本与事务时间戳相关联
。
读操作只读取
该事务开始前
的数据库快照
。
解决问题如下:
并发读-写时
:可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作。
解决脏读
、幻读
、不可重复读
等事务隔离问题,但不能解决上面的写-写 更新丢失
问题。
因此有了下面提高并发性能的组合拳
:
MVCC + 悲观锁
:MVCC解决读写冲突,悲观锁解决写写冲突
MVCC + 乐观锁
:MVCC解决读写冲突,乐观锁解决写写冲突
对于MVCC究竟能否解决幻读?请大家阅读一下博客
【MySQL】面试题之:MVCC能否解决幻读?
MVCC的实现原理主要是版本链
,undo日志
,Read View
来实现的
我们数据库中的每行数据,除了我们肉眼看见的数据,还有几个隐藏字段
,得开天眼
才能看到。分别是db_trx_id
、db_roll_pointer
、db_row_id
。
db_trx_id
6byte,最近修改(修改/插入)事务ID
:记录创建
这条记录/最后一次修改
该记录的事务ID
。
db_roll_pointer(版本链关键)
7byte,回滚指针
,指向这条记录
的上一个版本
(存储于rollback segment里)
db_row_id
6byte,隐含的自增ID
(隐藏主键),如果数据表没有主键
,InnoDB会自动以db_row_id产生一个聚簇索引
。
实际还有一个删除flag
隐藏字段, 记录被更新
或删除
并不代表真的删除,而是删除flag
变了
如上图,db_row_id
是数据库默认为该行记录生成的唯一隐式主键
,db_trx_id
是当前操作该记录的事务ID
,而db_roll_pointer
是一个回滚指针
,用于配合undo日志
,指向上一个旧版本
。
每次对数据库记录进行改动,都会记录一条undo日志
,每条undo日志也都有一个roll_pointer
属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来
,串成一个链表
,所以现在的情况就像下图一样:
对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer
属性连接成一个链表
,我们把这个链表称之为版本链
,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id,这个信息很重要,在根据ReadView判断版本可见性的时候会用到。
Undo log 主要用于记录
数据被修改之前
的日志,在表信息修改之前先会把数据拷贝到undo log
里。
当事务
进行回滚时
可以通过undo log 里的日志进行数据还原
。
Undo log 的用途
保证事务
进行rollback
时的原子性和一致性
,当事务进行回滚
的时候可以用undo log的数据进行恢复
。
用于MVCC快照读
的数据,在MVCC多版本控制中,通过读取undo log
的历史版本数据
可以实现不同事务版本号
都拥有自己独立的快照数据版本
。
undo log主要分为两种:
insert undo log
代表事务在insert新记录时产生的undo log , 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
update undo log(主要)
事务在进行update或delete时产生的undo log ; 不仅在事务回滚时需要,在快照读时也需要;
所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
事务进行快照读
操作的时候生产的读视图
(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照
。
记录并维护系统当前活跃事务的ID
(没有commit,当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以越新的事务,ID值越大),是系统中当前不应该被本事务
看到的其他事务id列表
。
Read View主要是用来做可见性
判断的, 即当我们某个事务
执行快照读
的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务
能够看到哪个版本
的数据,既可能是当前最新
的数据,也有可能是该行记录的undo log里面的某个版本
的数据。
Read View几个属性
trx_ids
: 当前系统活跃(未提交
)事务版本号集合。
low_limit_id
: 创建当前read view 时“当前系统最大事务版本号
+1”。
up_limit_id
: 创建当前read view 时“系统正处于活跃事务最小版本号
”
creator_trx_id
: 创建当前read view的事务版本号;
1、db_trx_id
< up_limit_id
|| db_trx_id
== creator_trx_id
(显示)
如果数据事务ID小于read view中的最小活跃事务ID
,则可以肯定该数据是在当前事务启之前
就已经存在
了的,所以可以显示
。
或者数据的事务ID
等于creator_trx_id
,那么说明这个数据就是当前事务自己生成的
,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示
的。
2、db_trx_id
>= low_limit_id
(不显示)
如果数据事务ID大于read view 中的当前系统的最大事务ID
,则说明该数据是在当前read view 创建之后才产生
的,所以数据不显示
。如果小于则进入下一个判断
3、db_trx_id
是否在活跃事务
(trx_ids)中
不存在
:则说明read view产生的时候事务已经commit
了,这种情况数据则可以显示
。
已存在
:则代表当前事务Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的。
上面所讲的Read View
用于支持RC
(Read Committed,读提交)和RR
(Repeatable Read,可重复读)隔离级别
的实现
。
RR、RC生成时机
RC
隔离级别下,是每个快照读
都会生成并获取最新
的Read View
;
而在RR
隔离级别下,则是同一个事务中
的第一个快照读
才会创建Read View
, 之后的
快照读获取的都是同一个Read View
,之后的查询就不会重复生成
了,所以一个事务的查询结果每次都是一样的
。
解决幻读问题
快照读
:通过MVCC来进行控制的,不用加锁。按照MVCC中规定的“语法”进行增删改查等操作,以避免幻读。
当前读
:通过next-key锁(行锁+gap锁)来解决问题的。
总结
从以上的描述中我们可以看出来,所谓的MVCC指的就是在使用READ COMMITTD
、REPEATABLE READ
这两种隔离级别的事务在执行普通的SEELCT
操作时访问记录的版本链
的过程,这样子可以使不同事务的读-写
、写-读
操作并发执行
,从而提升系统性能
。