九又四分之三站台

0%


title: InnoDB锁

date: 2022-11-23

categories:

  • MySQL

MySQL中的一条条SQL语句,都可以理解成一个个事务,而事务基于MySQL连接,也就是线程,当多个事务并发执行的时候也就是多线程并发执行。数据库的锁就是为了解决并发事务下数据的安全性问题。

锁的分类

锁的粒度划分:

  • 表锁
  • 行锁

互斥性划分:

  • 共享锁
  • 排他锁

共享锁和排他锁

  • 共享锁(S Lock),允许事务读一行数据
  • 排他锁(X Lock), 允许事务删除或更新一行数据

如果一个事务T1获取了行r的共享锁,那么事务T2也可以立即获取行r的共享锁,共享锁之间是兼容的,但这是事务T3想获得行r的排他锁,则必须等事务T1、T2释放行r上的共享锁,共享锁和排他锁之间不兼容。

如下表格:

X S
X 不兼容 不兼容
S 不兼容 兼容

意向锁

InnoDB支持多粒度的锁,允许行级上的锁和表级上的锁同时存在。为了支持不同力度上的锁,InnoDB支持一种额外的锁方式,也就是意向锁(Intention Lock)。

意向锁是表级的锁,为了要在一个事务中结实下一行将请求的锁类型

  • 意向共享锁(IS Lock), 事务想要获得一张表中某几行的共享锁,即当事务要对行加共享锁前,要先对表加意向共享锁
  • 意向排他锁(IX Lock),事务想要获取一张表中某几行的排他锁,即当事务要对行加排他锁前,要先对表加意向排他锁

为什么要有意向锁?当给表上S锁时,行不能有X锁,给表上X锁时,行不能有锁。所以给表上锁时要先知道有没有行锁,但是遍历的效率太低又有并发问题,所以加行锁前先加意向锁。

看个例子:

假设表中有一千万的数据,事务T1对id=1000000的事务加行锁,这时事务T2要对这张表加X锁

X锁也就是排他锁,所以T2加锁时要判断表中是否有其他行锁,那么就需要遍历全表,遍历全表的速度较慢,而且可能遍历到一半又有其他事务来对已遍历过的数据加锁。

意向锁就是为了解决这种行锁和表锁的问题,事务T1加行锁前,要先对表加一个意向锁,这样T2在加表锁前只要判断锁是否冲突而不需要遍历全表。

表级意向锁和行级锁的兼容性

IS IX S X
IS 不兼容
IX 不兼容 不兼容
S 不兼容 不兼容
X 不兼容 不兼容 不兼容 不兼容

X锁和其他锁都不兼容,S和IX不兼容

锁的算法

  • Record Lock: 单个行记录上的锁
  • Gap Lock:间隙锁,锁定一个范围但是不包含记录自身
  • Next-Key Lock: Record Lock + Gap Lock,锁定一个范围并包含自身

Record Lock

Record Lock记录锁,实际就是行锁,锁住一行数据, 使用方法:

1
2
3
4
// 获取行级共享锁
select * from user where id = 1 lock in share mode;
// 获取行级排他锁
select * from user where id = 1 for update;

Gap Lock

现在有user表

1
2
3
4
5
6
7
8
9
10
mysql> select * from user;
+----+------+
| id | code |
+----+------+
| 1 | 1 |
| 3 | 3 |
| 10 | 10 |
+----+------+

// id是主键,code是NORMAL索引

那么这里的区间就是(-无穷,1),(1, 3),(3, 10),(10, +无穷)

Next-Key Lock

Next-Key Lock类似Gap Lock,只不过是左开右闭, 如(-无穷,1],(1, 3],(3, 10],(10, +无穷)\

当前查询的索引具有唯一性时,InnoDB会对Next-Key Lock进行优化,降级为Record Lock

1
2
3
4
5
6
7
// 事务1
begin;
select * from user where id = 10 for update;

// 事务2
begin;
insert into user(id) value(9);

在事务1中,会对id=9这条数据加上X锁,由于id是主键且唯一,因此只会锁定这一条数据,而不会锁住(3, 10)这个间隙,因此事务2不会阻塞能立即成功。

上述降级仅仅在查询的列是唯一索引并且命中唯一行的情况下,如果没有命中行数据或者不是唯一索引的话,则不会降级。

1
2
3
4
5
6
7
// 事务1 注意:先删除上个操作插入的id=9的行数据
begin;
select * from user where id = 9 for update;

// 事务2
begin;
insert into user(id) value(7);

这种情况下事务2会阻塞需要ctrl+c手动退出, 因为事务1没有命中行数据,锁定的是(3,9)区间,所以事务2会失败。

1
2
3
4
5
6
7
// 事务1 
begin;
select * from user where code = 10 for update;

// 事务2
begin;
insert into user(code) value(7);

这种情况下虽然code=10的只有id=10一条行数据,但是还是会锁住code索引(3,10]区间,所以事务2会被阻塞。如果把code索引类型改成UNIQUE,那么事务2则不会被阻塞。

除此之外需要注意的是,在RR隔离级别下InnoDB会对辅助索引的下一个键值加上Gap Lock

1
2
3
4
5
6
7
// 事务1 
begin;
select * from user where code = 10 for update;

// 事务2
begin;
insert into user(code) value(11);

这种情况下,事务1除了锁住(3,10)区间外,还会锁住(10,+无穷)区间,所以事务2会阻塞

插入意向锁

在《MySQL技术内幕 InnoDB存储引擎》第2版中关于锁的算法只介绍了三种,并没有特别标明插入意向锁,只有一些关于insert操作的描述:

在InnoDB存储引擎中,对于insert操作,其会检查插入记录的下一条记录是否被锁定,若已经被锁定,则不允许插入。

来自《MySQL技术内幕 InnoDB存储引擎》第2版第6章6.4.1小节末尾

插入意向锁实际是一种间隙锁,在insert操作时会判断要插入的位置有没有间隙锁或者临键锁,有的话则等待持有锁的事务提交。当事务持有插入意向锁后,新来事务也要在改区间插入则不会阻塞。也就是说插入意向锁和间隙锁的区别是: 插入意向锁和已有的间隙锁排斥(无论是lock in share mode 还是 for update锁住的),但是插入意向锁之间不互斥,并且插入意向锁是隐式的无法手动获取。

锁的兼容性

横向是已持有锁,纵向是正在请求的锁

Gap Insert Intention Record Next-Key
Gap 冲突
Insert Intention 冲突 冲突
Record 冲突 冲突 冲突
Next-Key 冲突 冲突 冲突

这个表格是我看了许多博客都是这样描述的(加粗部分的冲突与看到的许多博客描述不符合,博客上大多是写着不冲突,但是我自己实验的结果是冲突的), 但是我的个人看法是无论是Record、Gap还是Next-Key都是锁的算法中的一种,互斥性则是X锁S锁,这两者是不同维度的。 通过 lock in share mode锁的间隙是S锁,通过for update锁住的间隙是X锁,所以这张表格只用来了解插入意向锁和其他锁的兼容性即可。

其他

元数据锁

基于表的元数据加锁, 更改表结构时加的表锁,避免更改表结构时其它事务进行CRUD操作。

自增锁

是专门为了提升自增ID的并发插入性能而设计的, 改善插入数据时的性能。自增锁也是一种特殊的表锁,但它仅为具备AUTO_INCREMENT 自增字段的表服务

参考

掘金:MySQL锁机制
《MySQL技术内幕 InnoDB引擎》第2版

MVCC机制的全称为Multi-Version Concurrency Control,即多版本并发控制技术, 主要是为了提升数据库并发性能而设计的,其中采用更好的方式处理了读-写并发冲突,做到即使有读写冲突时,也可以不加锁解决,从而确保了任何时刻的读操作都是非阻塞的。

阅读全文 »

test

Java锁

  • synchronized
  • Lock

单机锁,不适合分布式场景

阅读全文 »

简介

CountDownLatch 的作用是:当一个线程需要另外一个或多个线程完成后,再开始执行。比如主线程要等待一个子线程完成环境相关配置的加载工作,主线程才继续执行,就可以利用 CountDownLatch 来实现。

阅读全文 »

ThreadPoolExecutor是Executor框架最核心的类,也是线程池的实现类,有以下4个组件构成。

  1. corePool:核心线程池大小
  2. maximumPool:最大线程池大小
  3. BlockingQueue:用来暂时保存任务的工作队列
  4. RejectedExecutionHandler:当ThreadPoolExecutor已经关闭或者已经饱和(达到最大线程池大小并且工作队列已满),execute()方法将调用Handler(拒绝策略)
阅读全文 »

GC算法是内存回收的方法论,而垃圾收集器就是内存回收的具体实现。不同的厂商、不同版本之间的虚拟机所提供的垃圾收集器可能会有很大差别,并且一般都会提供参数供用户根据自己的需求组合出各个年代所使用的收集器。

阅读全文 »

简介

Redis是内存型据库,它的数据是存在内存中的,但是如果只能存在内存中的话,当服务器进程出现问题,那么内存中的数据就会丢失。为了解决这个问题,Redis提供了持久化功能。Redis持久化分为RDB持久化和AOF持久化两种。

阅读全文 »

bin log (二进制日志)

比如我们如果在MySQL中修改了一条记录,而用户检索出来的数据是通过MySQL的搜索引擎的,为了保证能检索到最新的数据,需要把搜索引擎的数据也一起改掉。

bin log记录了数据库表结构和表数据变更的记录,比如create/insert/update/delete等操作,但是select不会记录,因为select不存在修改表的操作。bin log记录着每条变更的SQL已经事务id等记录。

阅读全文 »

概述

垃圾收集(Garbage Collection, GC), 诞生于1960年 MIT 的 Lisp 语言。

程序计数器,虚拟机栈,本地方法栈3个区域岁线程而生,随线程而灭;栈中的栈桢随着方法的进入和退出而有条不紊地执行者出栈和入栈的操作,实现了内存的自动清理。因此,我们的内存垃圾回收主要集中于java堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。

阅读全文 »