业务场景中存在共享资源,多个进程或线程需要竞争获取并处理共享资源,为了保证公平、可靠、结果正确等业务逻辑,要把并发执行的问题变为串行,串行时引入第三方锁当成谁有权限来操作共享资源的判断依据。因为数据库是多用户共享资源的,在并发环境下会破坏数据库的一致性,所以需要锁来控制它们。数据库锁机制简单来说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则。
data manipulation language, 它们是SELECT、UPDATE、INSERT、DELETE,就象它的名字一样,这4条命令是用来对 数据库里的数据
进行操作的语言;
data definition language, DDL比DML要多,主要的命令有CREATE、ALTER、DROP等,DDL主要是用在 定义或改变表(TABLE)的结构
,数据类型,表之间的链接和约束等初始化工作上,他们大多在建立表时使用;
Data Control Language,是数据库控制功能。是用来设置或更改数据库用户或角色权限 的语句,包括(grant,deny,revoke等)语句。在默认状态下,只有sysadmin,dbcreator,db_owner或db_securityadmin等人员才有权力执行DCL
MySQL 数据库由于其自身架构的特点,存在多种数据存储引擎,每种存储引擎的锁定机制都是为各自所面对的特定场景而优化设计,所以各存储引擎的锁定机制也有较大区别。
存储引擎 | 表级锁 | 行级锁 | 页面锁 |
---|---|---|---|
MyISAM | √ | × | × |
InnoDB | √ | √ | × |
MEMORY | √ | × | × |
BDB | √ | × | √ |
除了以上几种粒度,还有一种全局锁(提到比较少)。后面分别介绍下。
锁类型按照 操作类型划分 有「读锁(S锁),写锁(X锁)」,其实它们与共享锁,排他锁是一个意思,只是不同叫法而已。
共享锁(Shared Lock)又称为读锁,简称S锁,是一种行级锁。
共享锁
就是多个事务对于同一数据共享一把锁,都能访问到数据,但是只能读不能修改。
# 加锁方式
select ... from ... lock in share mode;
# 事务提交或回滚释放:
commit;
rollback;
排他锁(EXclusive Lock)又称为写锁,简称X锁,是一种行锁也可以是表锁。
排他锁
就是不能与其他锁并存,即当前写操作没有完成前,会阻断其他写锁和读锁。
注意:其他事务不加锁的读是不会被阻塞的,阻塞的是加锁的读
# 加锁方式:
# innodb引擎默认会在update,delete语句加上 for update
SELECT * FROM student FOR UPDATE; # 排他锁
INSERT INTO student values ... # 排他锁
DELETE FROM student WHERE ... # 排他锁
UPDATE student SET ... # 排他锁
# 事务提交或回滚释放:
commit;
rollback;
意向锁(Intention Lock)
简称 I锁
,是表级锁的一种,它是由数据库引擎自行维护的,用户自己无需也无法操作意向锁。
意向锁
能够将 检查行锁
的时间复杂度由 O(n) 变成 O(1),其加锁的具体做法就是,当一个事务想要获取表中某一行的(共享/排他)锁的时候,它会自动尝试给当前表的加上意向(共享/排他)锁,用于防止全表扫描的一种锁。
(1)意向共享锁(IS锁-表级锁)
意向共享锁(Intention Shared Lock) 简称IS锁,事务想要给某一个数据行加行级共享锁(S锁)之前必须先获取该表的IS锁(表级锁)
(2)意向排他锁(IX锁-表级锁)
意向排他锁(Intention EXclusive Lock) 简称IX锁,事务想要给某一个数据行加行级排他锁(X锁)之前必须先获取该表的IX锁(表级锁)
意向锁之间的兼容互斥性:意向锁之间是互相兼容的。
意向锁与其他锁兼容互斥性:意向锁与普通的排他锁/共享锁互斥(这的排他锁(X锁)共享锁(S锁)指的都是表锁,意向锁不会与行级的共享锁/排他锁互斥。上面表锁部分讲到的 显式加锁 加的就是排他锁/共享锁)。
从操作的性能可分为乐观锁和悲观锁。乐观锁/悲观锁其实都是概念上的,只是在并发下防止数据被修改的一种加锁形式。
全局锁就是对整个数据库实例加锁,主要被备份工具使用,使用 逻辑方式进行备份(mydumper,mysqldump) 或 物理方式进行备份(percona-xtrabackup) 。MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock ,简称 FTWRL ,保证了数据的一致性,在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。
# 加锁
flush tables with read lock; # 即FTWRL,推荐使用
# 解锁
unlock tables;
加了全局锁之后,整个数据库都是【只读状态】,如果数据库里有很多数据,备份就会花费很多的时间,这样会造成业务停滞,有很大的弊端:
(1)备份期间,业务只能读数据,而不能更新数据;
(2)由于 FTWRL 需要关闭所有表对象,当这个时候还有长时间的 select 堵塞 FTWRL, 因为FTWRL会释放所有空闲的table缓存,如果有占用者占用某些table缓存,则会等待占用者自己释放这些table缓存,再去关闭表对象,执行命令时容易导致库卡死;
(3)如果在主库上备份。那么在备份期间都不能执行写入操作;
(4)如果在从库上备份。那么在备份期间,从库不能执行主库同步过来的 binlog,从而造成主从延时。
如果数据库的引擎支持的事务支持 可重复读 的隔离级别,那么在备份数据库之前 先开启事务 ,会先创建 Read View,然后整个事务执行期间都在用这个 Read View,而且由于 MVCC 的支持,备份期间业务依然可以对数据进行更新操作。
官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数 –single-transaction 的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。如果有的表使用了不支持事务的引擎,那么备份就只能通过 FTWRL 方法。这往往是 DBA 要求业务开发人员使用 InnoDB 替代 MyISAM 的原因之一。
表锁
是 MySQL 中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分 MySQL 引擎支持。当然,锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,致使并大度大打折扣。
MySQL里面表级别的锁有两种:一种是 表锁
,一种是 元数据锁(metadata lock,MDL)
。表锁一般是在数据库引擎不支持行锁的时候才会被用到的。默认使用表级锁定的主要是MyISAM,MEMORY,CSV等一些非事务性存储引擎。
# 隐式上锁(默认,自动加锁自动释放
insert、update、delete 上写锁
# 显式上锁(手动)
lock table tableName read; # 读锁/共享锁
lock table tableName write;# 写锁/排他锁
#解锁
unlock tables; # 客户端断开的时候也会自动释放锁。
# 查看表上加过的锁
show open tables;
表级读锁会阻塞写操作,但是不会阻塞读操作。而写锁则会把读和写操作都阻塞。
表级读锁 : 当前表加read锁,当前连接和其他的连接都可以读操作;但是当前连接写操作会报错,其他连接写操作会被阻塞。
表级写锁 : 当前表加write锁,当前连接可以对表做读写操作,其他连接对该表所有操作(读写操作)都被阻塞。
元数据锁
2.1、介绍:
metadata lock,简称 MDL。
Metadata lock 机制是为了保证数据一致性存在的。当有用户A查询一个表中的数据时,另一个用户B要对这个表结构做变更,删了一列,那么用户A拿到的结果跟表结构就不一致了,就会出现 waiting for table metadata lock。
Metadata lock 加锁过程是系统自动控制,无法直接干预,自动提交模式下,单语句就是一个事务,执行完了,事务也就结束了。
Metadata lock 是server层的锁,表级锁,每执行一条DML、DDL语句时都会申请 Metadata lock ,DML操作需要 Metadata lock 读锁,DDL操作需要 Metadata lock 写锁,读锁和写锁的阻塞关系如下:
读锁和读锁之间不会产生阻塞。也就是增删改查不会因为 Metadata lock 产生阻塞,可以并发执行,日常工作中大家看到的Metadata lock 之间的锁等待是innodb行锁引起的,和 Metadata lock 无关。
申请 Metadata lock 的操作会形成一个队列,队列中写锁获取优先级高于读锁。一旦出现写锁等待,不但当前操作会被阻塞,同时还会阻塞后续该表的所有操作。事务一旦申请到 Metadata lock 后,直到事务执行完才会将锁释放。(这里有种特殊情况如果事务中包含DDL操作,mysql会在DDL操作语句执行前,隐式提交commit,以保证该DDL语句操作作为一个单独的事务存在,同时也保证元数据排他锁的释放)。
InnoDB行锁分类和 Metadata lock 很类似,也主要分为读锁和写锁,或者叫共享锁和排他锁,读写锁之间阻塞关系也一致。二者最重要的区别一个是表锁,一个是行锁,且行锁中的读写操作对应在 DML 中都属于读锁。
注意: 支持事务的InnoDB引擎表和不支持事务的MyISAM引擎表,都会出现Metadata Lock Wait等待现象。一旦出现Metadata Lock Wait等待现象,后续所有对该表的访问都会阻塞在该等待上,导致连接堆积,业务受影响。
MDL 锁一旦发生会对业务造成极大影响,因为后续所有对该表的访问都会被阻塞,造成连接积压。要尽量避免 MDL 锁阻塞的发生:
(1)开启 metadata_locks 表记录 MDL 锁。
(2)设置参数 lock_wait_timeout 为较小值,使被阻塞端主动停止。
(3)规范使用事务,及时提交事务,避免使用大事务。
(4)增强监控告警,及时发现 MDL 锁。
(5)DDL 操作及备份操作放在业务低峰期执行。
顾名思义,MySQL的行锁每次操作锁住一行数据,锁定粒度最小,发生锁冲突的概率最低,并发度最高,但开销大,加锁慢,而且会出现死锁。
InnoDB行锁是通过对索引数据页上的记录加锁实现的,所以即使访问的不同记录,只要使用的是同一索引项,也可能会出现锁冲突。
行锁主要实现算法有 3 种:Record Lock
、Gap Lock
和 Next-key Lock
。
(1)RecordLock锁(记录锁):锁定单个行记录的锁,如果表中没有主键和任何一个索引,那InnoDB会使用隐式的主键来进行锁定。(RC、RR隔离级别都支持)
(2)GapLock锁(间隙锁):锁定索引记录间隙,确保索引记录的间隙不变,但不包含记录本身。(RR隔离级别支持)
(3)Next-key Lock 锁(记录锁+间隙锁):记录锁和间隙锁组合,锁定数据前后范围,并且锁定记录本身。(RR隔离级别支持)
注意: 在RR隔离级别,InnoDB对于行的查询都是采用 Next-Key Lock
的组合锁定算法,但是 在查询的列是唯一索引(包含主键索引)的情况下,Next-key Lock
会降级为 Record Lock
,仅锁住索引本身而非范围。
对于InnoDB,如果设置 AUTOCOMMIT = 1,如果没有 显式explicit(即写出begin transaction语句),任何一个语句,都是独立的一个事务,每个语句前面隐式(implicit)加了begin transaction,然后随后自动commit。如果设置AUTOCOMMIT = 0,没有写commit语句,那么所有的语句都在一个事务里,等着最后写一个commit去提交不管commit语句出现没有,都会有Redo Log和Undo Log(commit语句的标志会记录在Redo Log里)。
下面具体看下针对不同的sql语句采用的是那种加锁方式:
1)select ... from 语句:InnoDB引擎采用MVCC机制实现非阻塞读,所以对于普通的select语句,InnoDB不加锁。
2)select ... from lock in share mode语句:追加了共享锁,InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。
3)select ... from for update语句:追加了排他锁,InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。
4)update ... where 语句:InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。
5)delete ... where 语句:InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。
6)insert语句:InnoDB会在将要插入的那一行设置一个排他的RecordLock锁。
两阶段加锁协议-S2PL(Strict-2PL)
:在一个事务里面,分为加锁(lock)阶段和解锁(unlock)阶段,也即所有的lock操作都在unlock操作之前,只有提交(commit)或者回滚(rollback)时才是解锁阶段,
事务A | 事务B |
---|---|
begin; update user set k=k+1 where id = 1 update user set k=k+1 where id = 2 |
|
begin; update user set k=k+1 where id = 1 |
|
commit |
事务A持有的两个记录的行锁都是在commit的时候才释放的,事务B的update语句会被阻塞,直到事务A执行commit之后,事务B才能继续执行。
在InnoDB事务中,行锁是在需要的时候才加,但并不是不需要了就立刻释放,而是事务结束时释放。如果事务中需要锁多个行,要把最可能造成锁冲突,最可能影响并发度的语句尽量往后放。
并发系统中出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,会导致几个线程无限等待,称为死锁。
事务A在等待事务B释放id=2的行锁,而事务B在等待事务A释放id=1的行锁。事务A和事务B在互相等待对方的资源释放,就是进入了死锁状态。
(1)一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout来设置。
(2)另一种策略是,发起 死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑。
每次锁定相邻的一组记录,锁定粒度界于表锁和行锁之间,开销和加锁时间界于表锁和行锁之间,并发度一般,页级锁和行级锁一样,会发生死锁。应用在 BerkeleyDB
存储引擎中,一般很少见,了解一下即可。