文章内容是学习过程中的知识总结,如有纰漏,欢迎指正
文章目录
前言
锁机制是为了解决数据库的并发控制问题而产生的。如在同一时刻,客户端对同一个表做更新或查询操作,为了保证数据的一致性,必须对并发操作进行控制。同时,锁机制也为实现 MySQL 的各个隔离级别提供了保证
可以将锁机制理解为使各种资源在被并发访问时变得有序所设计的一种规则。
在学习mysql数据库锁机制之前先看下并发事务访问数据库的几种情况
一、并发事务访问数据情况
并发事务访问相同记录的情况大致可以划分为3种:
-
读-读
即并发事务相继读取相同的记录,因为没涉及到数据的更改,所以不会有并发安全问题,允许这种情况发生
-
写-写
即并发事务对相同记录进行修改,会出现脏写问题,因为任何一种隔离级别都不允许发生脏写,所以多个未提交的事务对同一个记录修改时需要加锁,保证它们是顺序执行的
-
读-写 或 写-读
即一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生脏读 、不可重复读 、幻读的问题。
可以使用两种方式解决(都离不开锁):
- 读写都采用加锁的方式,读写也需要排队执行,性能较差
- 写操作加锁,读操作利用MVCC多版本并发控制,读取历史记录,性能更高
现在开始进入正文
二、锁的划分
1.操作类型分类
*写锁与其他锁都不兼容
1.1.读锁/共享锁(Shared Lock,S锁):针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,相互不阻塞的。
MySQL提供了两种特殊的select语句:
- 对读取的记录加S锁
SELECT ... LOCK IN SHARE MODE;
或者
SELECT ... FOR SHARE [NOWAIT|SKIP LOCKED];
-- 8.0新特性,NOWAIT表示不等待,获取不到锁直接报错
-- SKIP LOCKED表示立即返回,如果获取不到锁跳过该行,返回的结果不包含该行
单纯的select不会加任何锁
- 对读取的记录加X锁
SELECT ... FOR UPDATE;
该select语句会被视为获取X锁,如果当前事务执行了该语句,会给记录加上X锁,不允许其他事务获取该记录的S锁和X锁。
1.2.写锁/排它锁(Exclusive Lock,X锁):允许事务对某些数据进行删除或更新。如果当前操作还没完成,其他事务的S和X锁是会被阻塞的,确保在多个事务中,对同一资源,只有一个事务能写入,并防止其他用户读取正在写入的资源。
平常所用到的写操作无非是 DELETE、UPDATE、INSERT这三种。读操作可以加共享锁或者排它锁,而写操作是必须加排它锁的。
2.锁的粒度分类
2.1 行锁
行锁(Row Lock)也称为记录锁,顾名思义,就是锁住某一行(某条记录row)。需要的注意的是,MySQL server层并没有实现行锁机制,行级锁只在存储引擎层实现。
优点:锁定力度小,发生锁冲突概率低,可以实现的并发度高。
缺点:对于锁的开销比较大,加锁会比较慢,容易出现死锁情况。
InnoDB与MyISAM引擎的最大不同有两点:一是支持事务; 二是采用了行级锁。
- 记录锁(Record Locks)
记录锁就是行级别的X锁和S锁,仅仅锁住一行记录,分S型记录锁和X型记录锁,和前面的规则一样,官方的类型名称为: LOCK_REC_NOT_GAP
需要特别注意的是加锁的执行过程中所有扫描到的行都会被锁上,因此必须确定条件使用了索引,这样才能精准锁定,而如果没有索引,会进行全表扫描,那么就会锁住无关紧要的数据。
针对唯一索引进行检索时,对已存在的记录进行等值匹配时,将会自动优化为行锁
INNODB的行锁是针对于索引加的锁,不通过索引条件检索数据,那么INNODB将对表中的所有记录加锁,此时就会升级为表锁
- 间隙锁(Gap Locks)
MySQL在REPEATABLE READ隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用 MVCC方案解决,也可以采用加锁方案解决。官方的类型名称为: LOCK_GAP
加锁方式有点尴尬,幻影记录还未出现,给谁加锁呢?InnoDB提出了一种称之为Gap Locks的锁。比如,把id值为8的那条记录加一个gap锁的示意图如下。
意味着不允许别的事务在(5,8)之间插入新记录。比如,有另外一个事务再想插入一条id值为6的新记录,它定位到该条新记录的下一条记录的id值为8,而这条记录上又有一个gap锁,所以就会阻塞插入操作,直到拥有这个gap锁的事务提交了之后,id列的值在区间(5, 8)中的新记录才可以被插入。
- 临键锁(Next-Key Locks)
有时候我们既想锁住某条记录 ,又想阻止其他事务在该记录前边的间隙插入新记录,所以InnoDB就提出了一种称之为Next-Key Locks的锁 。官方的类型名称为: LOCK_ORDINARY。我们也可以简称为 next-key锁
其实 临键锁=行锁+间隙锁
临键锁是在存储引擎InnoDB、事务级别在可重复读 的情况下使用的数据库锁, InnoDB默认的锁就是临键锁
-- 给id小于等于8的所有记录加上临键锁
select * from student where id<=8 for update;
-- 给id为[6,8]的记录加上临键锁
select * from student where id<=8 and id>6 for update;
select * from student where id<=8 for update;
- 插入意向锁
一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了间隙锁,如果有的话,插入操作需要等待,直到有间隙锁的那个事务提交。但是InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新记录,但是现在在等待。InnoDB就把这种类型的锁命名为插入意向锁 。插入意向锁是一种Gap锁,不是意向锁,在insert操作时产生。
插入意向锁是在插入一条记录行前,由INSERT操作产生的一种间隙锁 。 事实上插入意向锁并不会阻止别的事务继续获取该记录上任何信息
2.2 表锁
- 表级别的S锁、X锁
一般情况下,不会使用到InnoDB中提供的表级别的S锁和X锁,只会在一些特殊情况下,比方说崩溃恢复过程中用到;而在MyISM比较常用。
- MyISAM在执行查询语句(SELECT)前,会给涉及的所有表加读锁,在执行增删改操作前,会给涉及的表加写锁。
- 对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这个表添加表级别的 S锁 或者 X锁
手动 获取 InnoDB存储引擎提供的表t 的 S锁 或者 X锁:
* LOCK TABLES t READ :InnoDB存储引擎会对表 t 加表级别的 S锁 。
* LOCK TABLES t WRITE :InnoDB存储引擎会对表 t 加表级别的 X锁 。
-- 解锁
UNLOCK TABLES; 尽量避免在InnoDB存储引擎的表上使用 LOCK TABLES 这样的手动锁表语句
-
意向锁 (intention lock)
意向锁的作用就是加快表锁的检查过程
意向锁是由存储引擎自己维护的 ,用户无法手动获取,在为数据行加共享/排他锁之前,InooDB会先获取该数据所在表的对应意向锁。意向锁可分为:
- 意向共享锁(intention shared lock, IS):在使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前,需要先在表级别加上一个「意向共享锁」;
-- 事务要获取某些行的 S 锁,必须先获得表的 IS 锁。
SELECT column FROM table ... LOCK IN SHARE MODE;
- 意向排他锁(intention exclusive lock, IX):在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」;
-- 事务要获取某些行的 X 锁,必须先获得表的 IX 锁。
SELECT column FROM table ... FOR UPDATE;
-
自增锁(AUTO-INC锁)
自增锁是一种特殊的表级别锁(table-level lock),专门针对事务插入AUTO_INCREMENT类型的列。最简单的情况,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。
AUTO-INC 锁是特殊的表锁机制,锁不是再一个事务提交后才释放,而是再执行完插入语句后就会立即释放。
在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为被 AUTO_INCREMENT
修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉。
那么,一个事务在持有 AUTO-INC 锁的过程中,其他事务的如果要向该表插入语句都会被阻塞,从而保证插入数据时,被 AUTO_INCREMENT
修饰的字段的值是连续递增的。
-
元数据锁(MDL锁)
一个事务在执行select操作,另一个事务想为该表新增一个字段,此时新增字段的操作就会阻塞
MySQL5.5引入了meta data lock,简称MDL锁,属于表锁范畴。
我们不需要显示的使用 MDL,因为当我们对数据库表进行操作时,会自动给这个表加上 MDL:
- 对一张表进行 CRUD 操作时,加的是 MDL 读锁;
- 对一张表做结构变更操作的时候,加的是 MDL 写锁;
- MDL 是在事务提交后才会释放,这意味着事务执行期间,MDL 是一直持有的
2.3 全局锁
全局锁就是对 整个数据库实例 加锁。当你需要让整个库处于 只读状态 的时候,可以使用这个命令,之后 其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结 构等)和更新类事务的提交语句。
全局锁的典型使用 场景 是:做全库逻辑备份。
Flush tables with read lock
unlock tables
3.从锁的态度分类
需要注意的是,乐观锁和悲观锁并不是锁,而是锁的设计思想
3.1悲观锁(Pessimistic Locking)
假设最坏的情况,每次操作数据都会加上锁,如行锁、表锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
3.2乐观锁
乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,它不采用数据库自身的锁机制,而是通过程序来实现。
在程序上,我们可以采用版本号机制或者CAS机制实现。乐观锁适用于多读和冲突不激烈的应用类型,这样可以提高吞吐量。在Java中通过CAS实现的。
乐观锁机制
在表中增加一个版本字段version,对数据进行更新时会执行UPDATE ... SET version=version+1 WHERE version=xx。如果已经有事务对这条数据进行了更新,则不会成功。
总结
以上就是今天要学习的内容,本文介绍了mysql数据库锁机制及分类。在实际工作中大部分时间MySql数据库的并发操作不会总发生,属于小概率事件,并且一部分并发问题也可以在程序中解决,要根据实际场景去选择合理的数据库锁机制。