Mysql锁

首先思考一个问题:锁是用来干嘛的?

锁一般是用来协调多个进程/线程同时访问一资源时,产生的数据一致性问题。

这里我们要先从MySQL的事务说起

事务

概念

什么是事务?

维基百科:数据库事务是数据库管理系统执行过程中的一个逻辑单位,由有限的数据库操作序列构成。

  • 逻辑单位是不可拆分的
  • 包含了一个或者一系列的 DML 语句

四大特性

事务的四大特性:ACID

原子性(Atomicity)

原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

在 InnoDB 里面是通过 undo log 来实现的,它记录了数据修改之前的值(逻辑日志),一旦发生异常,就可以用 undo log 来实现回滚操作

隔离性(Isolation)

多个事务,对表或者行的并发操作,应该是透明、互相不干扰的。通过这种方式,我们最终也是保证业务数据的一致性。

持久性(Durable)

我们对数据库的任意增删改,只要事务提交成功,那么结果就是永久性的,即使数据库重启、宕机也不应该对其有影响。

在 InnoDB 中,持久性是通过 redo log 和 double write 双写缓冲来实现的

一致性(Consistent)

指的是数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态

这个一致性怎么理解呢?

来几个经典例子:

A、B两个账户各有100元,A转给B 150元

  • 表字段有限制(A金额 > 0),转账失败-回滚
  • 表字段没有限制,一个事务代码中做了判断(A金额 > 0),转账失败-回滚
  • 都没有做限制,转账成功:A = -50,B = 250

上面三种例子,都实现了一致性,但第三种:A怎么能为负数呢?

因为事务的一致性是由用户自己定义的规则,属于应用层的约束,而AID是属于数据库层面的约束,AID都是为了实现数据的一致性。

事务并发带来的问题

脏读

事务A可以读取到事务B未提交的数据

不可重复读

事务A可以读取到事务B已提交的数据更新,导致事务内前后两次读取数据不一致的情况

幻读

一个事务内多次读取到的结果集个数不一致,即读取到了其他事务新增、删除后的数据

事务的隔离级别

读未提交(READ UNCOMMITTED)

事务A可以读取到事务B未提交的数据,什么问题都没解决

读已提交(READ COMMITTED)

事务A可以读取到事务B已提交的数据,解决了脏读

可重复读(REPEATABLE READ)

事务A可以读取到事务B已提交新增、删除的数据,解决了不可重复读,没有解决幻读

而这是MySQL默认的隔离级别,通过实现快照读+当前读来解决幻读问题

序列化(SERIALIZABLE)

事务不同隔离级别解决的问题如下:

事务隔离级别对应解决的问题如下:

InnoDB在RR级别下是怎么解决幻读的?

幻读解决方案

InnoDB默认的事务隔离级别是Repeatable Read(后文中用简称RR),它为了解决该隔离级别下可能产生的幻读问题,提出了LBCC和MVCC两种方案。其中:

LBCC解决的是当前读(实时读)情况下的幻读

undolog+MVCC解决的是普通读(快照读、一致性读)的幻读

快照读

快照读(Consistent Read)也叫普通读、一致性读,就是单纯的 SELECT 语句,不包括下面这两类语句:

SELECT … FOR UPDATE
SELECT … LOCK IN SHARE MODE

快照读的执行方式是生成 ReadView(快照),每次读取的都是建立快照那一刻的数据,并不会对记录进行加锁。

什么时候建立快照?

  • 在 Serializable 隔离级别下 - 普通 select 也变成当前读,即加共享读锁
  • 在 RC 隔离级别下 - 每次 select 都会建立新的快照(就是每次select都是最新的数据)
  • 在 RR 隔离级别下
    • 事务启动后,首次 select 会建立快照
    • 如果事务启动选择了 with consistent snapshot,事务启动时就建立快照
    • 当前事务对旧数据进行修改,会重新建立快照

注意:快照一旦建立,针对的是整个DB实例,跨表依旧有效

InnoDB是怎样实现快照读的?

靠MVCC实现的,MVCC 只在 Read Commited 和 Repeatable Read 两种隔离级别下工作

MVCC

https://www.php.cn/mysql-tutorials-460111.html

MVCC在MySQL的实现原理主要是版本链,undo日志 ,Read View来实现的

  • 版本链
    • db_trx_id6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID。
    • db_roll_pointer(版本链关键)7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
    • db_row_id6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以db_row_id产生一个聚簇索引。
    • 实际还有一个删除flag隐藏字段, 记录被更新或删除并不代表真的删除,而是删除flag变了

  • undo日志

如上图

  • ReadView

事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照。

记录并维护系统当前活跃事务的ID(没有commit,当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以越新的事务,ID值越大),是系统中当前不应该被本事务看到的其他事务id列表。

Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。

当前读

从字面上理解,当前读就是读取的是记录的最新版本,并且在读取之后,还需保证其他事务不能修改当前记录,对读取的记录加锁,如以下这些 SQL 类型:

select … lock in share mode
select … for update
update 、delete 、insert

当前读本质上是加了锁的增删改查语句,无论加的共享锁还是排他锁均为当前读.

为什么update、delete、insert也用到了当前读?

在执行update语句更新某行数据时,需要先读取最新数据行,然后再更新,读取的时候用到的就是当前读

LBCC

LBCC是Lock-Based Concurrent Control的简称,即基于锁的并发控制。

InnoDB是通过锁来实现LBCC方案的

但如果仅仅是基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那就意味着不支持并发的读写操作,而我们的大多数应用都是读多写少的,这样会极大地影响操作数据的效率

InnoDB在RR级别下真的解决了幻读吗?

https://www.jianshu.com/p/b7c53ee0ed0e

步骤S1S2
1begin;begin;
2select * from student where id =8;(没值)
3INSERT INTO student (id, name, age, addr, uniq_id) VALUES (8, ‘张三’, 18, ‘合肥’, 8);
4update student set addr=‘合肥’ where id=8;
5select * from student where id =8; (有值)
6commit;

上面的场景下没有解决幻读

S1下的步骤2和步骤5读取的数据集不一致,产生了幻读

锁的类型

https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html

兼容性

共享锁

共享锁,Share lock,也叫读锁。它是指当事务获取对象的共享锁时,允许其它事务读取对象、获取对象的共享锁,但是不可以写入

排它锁

排它锁,Exclusive Lock,也叫写锁或者独占锁,主要是防止其它事务和当前加锁事务锁定同一对象。

锁类型排他锁共享锁
排他锁互斥互斥
共享锁互斥兼容

机制

在MySQL中,无论是悲观锁还是乐观锁,都是人们对概念的一种抽象,它们本身还是利用 MySQL提供的锁机制来实现的。除了MySQL,在Java中也有乐观锁和悲观锁的概念。

乐观锁

乐观锁是对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁(这使得多个任务可以并行的对数据进行操作),只有到数据提交的时候才通过一种机制来验证数据是否存在冲突(一般实现方式是通过加版本号然后进行版本号的对比方式实现)

比如MVCC、CAS

悲观锁

总是假设以最悲观的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁

比如synchronized

粒度

https://zhuanlan.zhihu.com/p/387087073

全局锁

  • 概念

对整个DB实例加锁

  • 语句
    • 加锁:flush tables with read lock(FTWRL)读锁执行后,整个库处于只读状态的时候,其他线程涉及到排他锁的操作都会被阻塞。

若FTWRL之前有读写,FTWRL会等待读写执行完毕后再执行。FTWRL执行时,要刷脏页数据到磁盘, 因为要保持数据一致性,所以执行FTWRL的时机是所有事务都提交完毕后。

- 解锁:unlock tables或客户端断开连接
  • 适用场景

全库备份场景

FTWRL会带来什么问题?

  • 若你在主库备份,则备份期间,主库都不能再执行更新,业务直接停摆
  • 若你在从库备份,则备份期间,从库不能执行主库同步过来的binlog,主从延迟加剧

mysqldump

官方推荐的逻辑备份工具mysqldump。当mysqldump使用参数–single-transaction,导数据前会启动一个事务,确保得到一致性视图。 由于MVCC,该过程中数据依旧能正常更新。

存储引擎需要支持事务,这也是提倡存储引擎尽量使用InnoDB的原因。

mysqldump的时候不要执行DDL语句:

master对表test的DDL传输到slave去应用时,mysqldump还没对test进行备份,该DDL会在slave的test表应用成功,但当导出到test表时,会报“ERROR 1412 (HY000): Table definition has changed, please retry transaction” 错误,导致导出失败!

为什么不用reaonly

set global read_only=1;
  • 有些系统的readonly值会被用来做其他逻辑,比如判断一个库是主库or备库。因此,修改global变量的方式影响面太大!
  • 执行FTWRL后,由于客户端异常断开,MySQL会自动释放该全局锁,整库回到可正常更新的状态。而将整库设为readonly后,若客户端异常,则数据库就一直保持readonly,导致整库长时间不可写。

表级锁

https://cloud.tencent.com/developer/article/1553461

https://zhuanlan.zhihu.com/p/573336304

表锁

https://blog.youkuaiyun.com/u022812849/article/details/122847603

表锁一般是在数据库引擎不支持行锁的时候才会被用到的

  • 语法
-- 加锁
lock tables tablename read/write
-- 解锁
unlock tables
-- 客户端自动断开连接的时候,也会解锁
  • 特点
    • 写锁:本线程可读、写,其他线程不可读写
    • 读锁:所有线程可读

注:一个会话中只要使用lock table加表锁,read还是write其他表或用表别名访问同一个表会报错

MDL

MDL全称为meta data lock,即元数据锁,主要作用是维护表元数据的一致性

元数据锁是server层的锁,表级锁,每执行一条DML、DDL语句时都会申请MDL锁

  • DML操作需要MDL读锁
  • DDL操作需要MDL写锁

MDL加锁过程是系统自动控制,无法直接干预,读读共享,读写互斥,写写互斥

S1S2S3
begin
select * from student
ALTER TABLE student ADD INDEX idx_name(name);(阻塞)
select * from student(阻塞)
commit;okok

若只有S2被阻塞还没啥,但之后所有要在表student上新申请MDL读锁的请求也会被S2阻塞。 所有对表的CRUD操作都要先申请MDL读锁,就都被锁住,等于该表此时完全不可读写!

遇到这种问题怎么临时解决呢?

  • show full processlist 可以看到正在等待MDL锁

  • **SELECT * FROM information_schema.INNODB_TRX ; 根据这个事务的线程ID(trx_mysql_thread_id),这是个长事务,直接kill trx_mysql_thread_id **

MySQL5.6后支持Online DDL,对表操作增加字段等功能时,不会阻塞读写,那为啥还会出现上述案例的结果?

Online DDL过程

  1. 拿MDL写锁
  2. 降级成MDL读锁
  3. 真正做DDL
  4. 升级成MDL写锁
  5. 释放MDL锁

1、2、4、5若无锁冲突,执行时间很短。第3步占用了DDL绝大部分时间,这期间该表可正常读写数据,因此称为Online。

但案例中,第1步就阻塞了。所以必须等待之前的事务全部提交后,才能开始执行。

怎样避免阻塞?

  1. 有长事务
    • 原地等待,先暂停DDL
    • 或kill掉这长事务
  2. 热点表,查询请求特别多

alter table语句设定等待时间,若在这个指定的等待时间里面能够拿到MDL写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者DBA再通过重试命令重复这个过程。

表级意向锁

一句话总结:由于InnoDB支持多粒度锁,允许行锁和表锁共存,为了快速的判断表中是否存在行锁,InnoDB推出了意向锁。

  1. 排他意向锁:若有事务在表里执行增删改操作,那在行级会加独占锁,此时同时会在表级加一个意向独占锁
  2. 共享意向锁:若有事务在表执行查询操作,会在表级加一个意向共享锁

注:意向锁与意向锁、行级锁之间不会产生冲突,意向锁只会与表级锁产生冲突

锁类型独占锁共享锁意向独占锁意向共享锁
独占锁互斥互斥互斥互斥
共享锁互斥不互斥互斥不互斥
意向独占锁互斥互斥不互斥不互斥
意向共享锁互斥不互斥不互斥不互斥

表级的意向锁有什么意义呢?

当我们准备给表加上表锁的时候,先要判断有没其他的事务锁定了其中了某些行,如果有的话,肯定不能加上表锁。那么在有意向锁的时候,就不需要扫描表中的所有行来找行锁,如果有,就直接返回失败。如果没有,就可以加锁成功。

AUTO-INC锁

AUTO-INC锁是一种特殊的表级锁,当表中有AUTO_INCREMENT的列时,如果向这张表插入数据时,InnoDB会先获取这张表的AUTO-INC锁,等插入语句执行完成后,AUTO-INC锁会被释放。

AUTO-INC锁可以使用innodb_autoinc_lock_mode变量来配置自增锁的算法。innodb_autoinc_lock_mode变量可以选择三种值如下表:

innodb_autoinc_lock_mode变量含义
0传统锁模式(insert语句排队获取锁)
1连续锁模式(insert确定行数的并行,不确定行数的排队获取锁)
2交错锁模式(MySQL8默认,都不排队)

三种模式的具体含义参考https://www.jianshu.com/p/8f6598da99e4

innodb_autoinc_lock_mode=2的性能最好,阿里的数据库默认是第三种模式,但这样带来两个问题

  1. 并发插入数据时,自增长的值可能不是连续的
  2. 基于Statement的主从复制会出现问题。因此使用这个模式,主从复制应该使用Row方式。这样才能保证最大的并发性能及主从数据的一致

行锁

加锁方式
记录锁

记录锁就是对表中的记录加锁,简称行锁。比如

SELECT * FROM `test` WHERE `id`=1 FOR UPDATE; 

它会在 id=1 的记录上加上记录锁,以阻止其他事务增删改 id=1 这一行

记录锁的条件

  • id 列必须为唯一索引列或主键列
  • 条件必须为精准匹配(=),不能为 >、<、like等。
间隙锁

  • 间隙锁 是 Innodb 在 RR(可重复读) 隔离级别 下为了解决幻读问题时引入的加锁机制

RC在特殊情况下也会使用间隙锁封锁区间:

  1. 外键约束检查(foreign-key constraint checking)
  2. 重复键检查(duplicate-key checking)
  • 使用间隙锁锁住的是一个区间(左开右开),而不仅仅是这个区间中的每一条数据
  • 间隙锁存在于非唯一索引中。

间隙锁的加锁规则:

  • 原则
    • 加锁的基本单位是 next-key lock,next-key lock 是左开右闭区间
    • 查找过程中访问到的对象才会加锁
  • 优化
    • 等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁;如果不是唯一索引,需要访问到第一个不满足条件的值,此时next-key lock会退化为间隙锁。
    • 范围查询:无论是否是唯一索引,范围查询都需要访问到不满足条件的第一个值为止

关闭间隙锁的2种方式:

(1)将事务隔离级别变为read committed

(2)将参数innodb_locks_unsafe_for_binlog设置为1(默认是0,即开启间隙锁)

有一个特殊的间隙锁:

**插入意向锁 **(Insert Intention Lock)

https://zhuanlan.zhihu.com/p/344542398

插入意向锁是在插入一条记录行前,由 INSERT 操作产生的一种间隙锁。在多事务同时写入不同数据至同一索引间隙的时候,并不需要等待其他事务完成,不会发生锁等待。

假设有一个记录索引包含键值4和7,不同的事务分别插入5和6,每个事务都会产生一个加在4-7之间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突,但是第三个事务获取间隙的排他锁进行update或delete则会阻塞。

临键锁

**Next-key锁是记录锁和间隙锁的组合(**左开右闭的区间)

InnoDB默认使用的是临键锁,部分场景下会退化成记录锁、间隙锁。

  • 查询条件没有用到索引,直接锁表,比如查询条件不是索引列,比如隐式转换
  • 查询条件的列是唯一索引且命中,临键锁退化为记录锁
  • 查询条件的列是唯一索引但未命中,临键锁退化为间隙锁(间隙锁与间隙锁之间兼容,间隙锁只是为了防止在锁定期间有数据插入这个间隙)

间隙锁到底锁住了什么?

https://www.jianshu.com/p/42e60848b3a6

系统参数

事务隔离级别

-- 8.0以上事务隔离级别
select @@global.transaction_isolation;
show variables like '%transaction_isolation%';
-- 8.0以下事务隔离级别
select @@global.tx_isolation;-- MySQL8.0以下
show variables like '%tx_isolation%';

-- 修改事务隔离级别为RC
set global transaction_isolation='READ-COMMITTED';
set transaction_isolation='READ-COMMITTED';

事务自动提交

-- ON为自动提交  OFF反之
show variables like 'autocommit';
-- 修改自动提交为OFF
set autocommit=0

死锁

-- 死锁等待时间,默认50秒
show VARIABLES like '%innodb_lock_wait_timeout%'
-- 死锁自动检测,默认开启
show VARIABLES like '%innodb_deadlock_detect%'

用户正在运行的状态

-- 显示用户正在运行的线程
show processlist

-- 查看正在运行的事务
select * from information_schema.innodb_trx;

-- 8.0之前查看当前的锁信息
select * from information_schema.innodb_locks;
-- 8.0之后查看当前的锁信息
select * from performance_schema.data_locks;

-- 8.0之前查看锁等待的信息
select * from information_schema.innodb_lock_waits;
-- 8.0之后查看锁等待的信息
select * from performance_schema.data_lock_waits;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值