转载
MySQL中的锁有很多种,各种锁应用在不同的地方。「MySQL依靠锁机制可以让多个事务更新一行数据的时候串行化」。
MySQL中锁总的来说有两种概念:Lock和Latch
实际上MySQL的锁在不同的维度上划分是多种多样的,在特地的场景下,发挥不一样的作用,下面来看看锁的分类。
「全局锁,即对整个数据库实例加锁」。一般当我们需要让整个库处于只读状态的时候,可以给数据库加上全局锁。「加上全局锁之后其他线程的:数据更新语句(增删改)、数据定义语句(包括建表、修改表结构等)都会被阻塞」。
「加锁方式」
MySQL提供了一个加全局读锁的方法,命令
Flush tables with read lock (FTWRL)
「全局锁应用场景」
「做全库逻辑备份」。即把整库每个表都select出来保存成文本。
「通过FTWRL确保不会有其他线程对数据库做更新,然后对整个库做备份。在备份过程中整个库完全处于【只读状态】」。但是整个库都只能读不能写,会有很大的弊端:
由此可见,做全库逻辑备份的时候加全局锁,对系统的影响非常的大,既然如此,为什么要加全局锁呢?
先来看一个案例:
假设现在数据库中现在有两张表:账户余额表,订单表,当我们下一个订单时,会扣减余额,同时在订单表中写入一个订单记录。
下面通过图解来说明对这两张表进行备份的过程,由于备份数据又先后顺序,所以分两种情况来看
由图可以看出:先备份账户余额表,再备份订单表导致备份数据中账户余额没扣钱,但是订单有了,商家血亏,消费者乐的不行,这肯定是不允许发生的。
由图可以看出:先备份订单表,再备份账户余额表导致备份数据中账户余额扣了钱,但是订单没有了,商家白嫖,消费者肯定不干,这肯定也是不允许发生的。
案例结论:
「通过上述案例说明,不加锁的话,备份系统备份的得到的数据不一致的,其实就是数据一个逻辑时间点的,这个读视图【Read View】是逻辑不一致的」。
「通过上述的描述,我们知道在做数据备份的时候,需要加全局锁(FTWRL)来保证数据的一致性,但是由于FTWRL需要关闭所有表对象,数据库禁止写入,执行命令时容易导致数据库hang住」。
Q:既然加全局锁会影响业务,危害大,那做备份的时候有没有不用FTWRL,又能保证数据一致性的方法呢?A:有,方法是由有的,但是有局限性
「FTWRL的本质就是给数据库加一个锁,禁止其他线程写入,也就是将数据库设置成了只读状态」。数据库设置为只读状态其实还有一种更简单的方式,直接使用以下命令即可:
set global readonly=true
但是一般不推荐使用readonly,两者比较,还是推荐使用FTWRL
MySQL里面表级别的锁有两种:一种是表锁,一种是元数据锁(metadata lock,MDL)。
「表级别的锁定是MySQL各【存储引擎中】最大颗粒度的锁定机制」。由于直接锁定一张表,所以获取锁和释放锁的速度很快,避免了死锁问题,但是出现锁定资源争用的概率也最高,并发量降低。
「表锁的加锁语法」
#隐式上锁(默认,自动加锁自动释放
insert、update、delete //上写锁
#显式上锁(手动)
lock table tableName read;//读锁
lock table tableName write;//写锁
「表锁的释放锁语法」
UNLOCK TABLES
客户端断开的时候也会自动释放锁。
「查看表上加过的锁」
show open tables;
「MyISAM引擎默认的锁是表锁」。表锁一般是在数据库引擎不支持行锁的时候才会被用到的。
「表级读锁会阻塞写操作,但是不会阻塞读操作。而写锁则会把读和写操作都阻塞」。
当我们查询查询一个表中的数据时,另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果跟表结构就不一致了,这肯定是允许。这里就用到了元数据锁
在MySQL 5.5版本中引入了MDL,「元数据锁(MDL) 不需要显式使用,在访问一个表的时候会被自动加上」。
页锁应用于 BDB 引擎,一般很少见,了解一下即可,重点理解下行锁
「行锁顾名思义就是对数据行进行加锁。行锁的锁定颗粒度在 MySQL中是最细的,应用于 InnoDB 存储引擎,通过对索引数据页上的记录加锁实现的【即行锁是针对索引加锁】」。
MySQL会比较不同执行计划,当全表扫描比索引效率更高时,InnoDB就使用表锁。因此不一定使用了索引就一定会使用行锁,也有可能使用表锁。
「实际上InnoDB 的行锁也是分为两步获得的:锁住主键索引,锁住非主键索引」。 当两个事务同时执行时, 一个锁住了主键索引,在等待其他索引; 另一个锁住了非主键索引,在等待主键索引, 这样就可能会发生死锁。 「InnoDB可以检测到这种死锁,检测到后会让其中一个事务释放锁回退,另一个获取锁完成事务」。
前面讲到「InnoDB行锁是通过对 索引数据页上的记录加锁实现的」,接下来看看它具体是怎么实现,
InnoDB存储引擎有3种实现行锁的算法:
「在RR隔离级别,InnoDB对于行的查询都是采用【Next-Key Lock】的组合锁定算法」,但是「在查询的列是唯一索引(包含主键索引)的情况下,Next-key Lock会降级为Record Lock,仅锁住索引本身而非范围」。
下面具体看下针对不同的sql语句采用的是那种加锁方式:
查询语句类型一
select ... from ...
「对于普通的select语句,InnoDB引擎采用MVCC机制实现非阻塞读,【InnoDB引擎不加锁】」。
查询语句类型二
select ... from ... lock in share mode
「添加共享锁,InnoDB会使用Next-Key Lock锁进行处理,扫描如果有唯一索引,则降级为RecordLock锁」。
查询语句类型三
select ... from ... for update
「添加排他锁,InnoDB会使用Next-Key Lock锁进行处理,扫描如果有唯一索引,则降级为RecordLock锁」。
修改语句
update ... from ... where ...
「InnoDB会使用Next-Key Lock锁进行处理,扫描如果有唯一索引,则降级为RecordLock锁」。
删除语句
delete ... from ... where
「InnoDB会使用Next-Key Lock锁进行处理,扫描如果有唯一索引,则降级为RecordLock锁」。
插入语句
insert ... from ...
「InnoDB会在将要插入的那一行设置一个排他的RecordLock锁」。
在前文中提到的锁类型按照「操作类型划分」有「读锁(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;
「共享锁工作原理」
「如一个事务获取了一条记录的排他锁,其他事务就不能对该行记录做其他操作,也不能获取该行的锁(共享锁、排他锁),但是获取到排他锁的事务可以对数据进行读写操作」。
这里要注意一下,其他事务不加锁的读是不会被阻塞的,阻塞的是加锁的读
「排他锁为什么是一种行锁也是表锁」
innodb引擎默认会在update,delete语句加上 for update
「读锁,写锁都属于行级锁,行级锁的实现是依靠其对应的索引,如果没用到索引的查询,就会走表锁」。
「意向锁(Intention Lock)简称I锁,是一种表级锁」。
「InnoDB 实现了标准的行级锁,包括:共享锁(S锁)、排它锁(X锁)」,那么为什么需要引入意向锁呢?意向锁解决了什么问题?
假设,事务A获取了某一行记录的排它锁,事物A尚未提交,事务B想要获取表锁时,则事物B必须要确认表的每一行都不存在排他锁,需要进行全表扫描,效率很低,此时就引入意向锁
如上,数据库中存储数据,范围由大到小:表-->页-->行,加锁也是分别加在表-->页-->行中,当我们把锁加在更大一级范围时,也就不需要全表扫描下一级的某些锁,可以很大程度提升性能。
「锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式,即意向锁」
通过上述描述我们知道「意向锁是加在表上,用于防止全表扫描的一种锁,即意向锁是表锁」。意向锁分为两种类型:
「【意向锁都是InnoDB存储引擎自己维护的,用户是无法操作意向锁的】」。
【「在为数据行加共享锁/排他锁之前,InooDB会先获取该数据行所在在数据表的对应意向锁(表级锁)」】,如果没有获取到,否则等待innodb_lock_wait_timeout超时后根据innodb_rollback_on_timeout决定是否回滚事务。
从锁粒度角度:InnoDB 允许行级锁与表级锁共存,而意向锁是表锁; 从锁模式角度:意向锁是一种独立类型,辅助解决记录锁效率不及的问题; 从兼容性角度:意向锁包含了共享/排他两种。
意向共享锁(IS) | 意向排他锁(IX) | |
---|---|---|
意向共享锁(IS) | 兼容 | 兼容 |
意向排他锁(IX) | 兼容 | 兼容 |
意向共享锁(IS) | 意向排他锁(IX) | |
---|---|---|
表级共享锁(S) | 兼容 | 互斥 |
表级排他锁(X) | 互斥 | 互斥 |
「上述的排他锁(X锁)共享锁(S锁)指的都是表锁,意向锁不会与行级的共享锁/排他锁互斥」
「乐观锁/悲观锁其实都是概念上的,只是在并发下防止数据被修改的一种加锁形式」。
「对数据的修改抱有悲观态度的一种并发控制方式,悲观的认为自己(当前线程)拿到的数据是被修改过的,所以在操作数据之前先加锁」。
「乐观锁是对于数据冲突保持一种乐观态度,每次读取数据的时都认为其他线程不会修改数据,所以不上锁,只是在数据修改后提交时才通过【版本号机制或者CAS算法】来验证数据是否被其他线程更新」。
因为乐观锁中并没有【加锁和解锁】操作,因此乐观锁策略也被称为「无锁编程」。
版本号机制有两种方式:使用版本字段(version)和使用时间戳(Timestamp),两者实现原理是一样的。
前文中提到「乐观锁需要开发者自己去实现,所以版本号实现时通过在表中加字段的形式实现的」。
「乐观锁实现案例」
修改用户表中Id为1的用户姓名
第一步:查询记录信息
#使用版本字段(version)
select name,version from user where id=1;
#使用时间戳(Timestamp)
select name,timestamp from user where id=1;
第二步:逻辑处理之后,修改姓名为张三
#使用版本字段(version)
update user set name = '张三',version=version+1 where id=1 and version = #{version};#version 为第一步查询的值
#使用时间戳(timestamp)
update user set name = '张三',timestamp=now() where id=1 and timestamp = #{timestamp};
「CAS算法即compare and swap(比较与交换),是一种有名的无锁算法。即不使用锁的情况下实现多线程之间的变量同步,也就是无锁编程」。
「死锁是指两个或两个以上的事务在执行过程等中,因争夺资源而造成的一种相互等待的现象」。
「死锁发生的以上四个条件缺一都无法导致死锁,而由于互斥条件是非共享资源所必须的,不仅不能改变,还应加以保证,所以恢复死锁主要是破坏产生死锁的其他三个条件」。
行级锁产生死锁有两种情况,一直是资源争夺,一种是行级锁升级为表级锁
MySQL提供了几个与锁有关的参数和命令,可以辅助我们优化锁操作,减少死锁发生。
「查看近期死锁日志信息」
show engine innodb status;
通过以上命令查看近期死锁日志信息,然后使用执行计划进行SQL优化
「查看锁状态变量」
通过以下命令可以检查锁状态变量,从而分析系统中的行锁的争夺情况
show status like'innodb_row_lock%';
「如果等待次数高,而且每次等待时间长,则需要对其进行分析优化」。