diff --git a/Database.md b/Database.md index c1f2c55..20e7910 100644 --- a/Database.md +++ b/Database.md @@ -597,11 +597,37 @@ WHERE A.EMP_SUPV_ID = B.EMP_ID; # 数据库锁 -按照锁的粒度进行分类,MySQL主要包含三种类型锁: +数据库锁可以按粒度、加锁算法、加锁策略、兼容性和其它等方面进行分类: -- **全局锁**:锁的是整个 `database`。由MySQL的**SQL layer**层实现的 -- **表级锁**:锁的是某个 `table`。由MySQL的**SQL layer**层实现的 -- **⾏级锁**:锁的是 `某⾏数据` 或 `⾏之间的间隙`。由某些**存储引擎**实现,如InnoDB +- **锁粒度** + - **行锁** + - 锁的是 `某⾏数据` 或 `⾏之间的间隙`。由某些**存储引擎**实现,如InnoDB + - 开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高 + - **表锁** + - 锁的是某个 `table`。由MySQL的**SQL layer**层实现的 + - 开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低 + - **页锁** + - 开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般 + - **全局锁** + - 锁的是整个 `database`。由MySQL的**SQL layer**层实现的 + +- **加锁算法** + - **Record Lock(记录锁)** + - **Gap Lock(间隙锁)** + - **Next-Key Lock(临键锁)** + + +- **加锁策略** + - **悲观锁** + - **乐观锁** + +- **兼容性** + - **排它锁** + - **共享锁** + - **意向锁** + +- **其它锁** + - **自增锁(AUTO-INC锁)** @@ -613,12 +639,6 @@ WHERE A.EMP_SUPV_ID = B.EMP_ID; | MyISAM | | √ | | | BDB | √ | √ | √ | -- **表锁**: 开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低 -- **行锁**: 开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高 -- **页锁**: 开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般 - - - 在 InnoDB 存储引擎中: - `SELECT` 操作的不可重复读问题通过 `MVCC` 得到了解决 @@ -627,45 +647,7 @@ WHERE A.EMP_SUPV_ID = B.EMP_ID; -锁粒度 - -- 行锁 -- 表锁 -- 页锁 - -算法 - -- Record Lock -- Gap Lock -- Next-Key Lock - -实现机制 - -- 悲观锁 -- 乐观锁 - -兼容性 - -- 排它锁 -- 共享锁 -- 意向锁 - - - -从加锁策略上分: - -- **乐观锁** -- **悲观锁** - - - -其它: - -- **自增锁(AUTO-INC锁)**:自增锁是一种特殊的**表级锁**,主要用于事务中插入自增字段,也就是我们最常用的自增主键id - - - -## 全局锁 +## 全局锁(Global-Level) **全局锁就是对整个数据库实例加锁**。MySQL 提供了一个加全局读锁的方法,命令是`Flush tables with read lock (FTWRL)`。 当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞: @@ -734,7 +716,7 @@ unlock tables -## 表级锁 +## 表级锁(Table-Level) 表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MyISAM与InnoDB都支持表级锁定。表级锁分为: @@ -782,45 +764,23 @@ MDL 是在事务提交后才会释放,这意味着**事务执行期间,MDL -### 意向锁(Intention Locks) - -**意向锁的目的是为了快速判断表里是否有记录被加锁**。 - -- 在使用 InnoDB 引擎的表里对某些记录加上**共享锁**之前,需要先在表级别加上一个**意向共享锁** -- 在使用 InnoDB 引擎的表里对某些纪录加上**独占锁**之前,需要先在表级别加上一个**意向独占锁** - -也就是,当执行插入、更新、删除操作,需要先对**表**加上**意向共享锁**,然后对该记录加**独占锁**。而普通的 select 是不会加行级锁的,**普通的select语句是利用MVCC实现一致性读**,是无锁的。不过select也是可对记录加共享锁和独占锁,如下: - -```sql --- 先在表上加上意向共享锁,然后对读取的记录加独占锁 -select ... lock in share mode; --- 先表上加上意向独占锁,然后对读取的记录加独占锁 -select ... for update; -``` - -意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables … read)和独占表锁(lock tables … write)发生冲突。 - - +### 意向锁(Intention Lock) -表锁和行锁是满足读读共享、读写互斥、写写互斥的。 +**意向锁是表级锁,是InnoDB主动加的,不需要手动处理。其目的是为了快速判断表里是否有记录被加锁。** - 如果没有**意向锁**,那么加**独占表锁**时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢 -- 如果有了**意向锁**,由于在对记录加独占锁前,先会加上表级别的意向独占锁,那么在加**独占表锁**时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录 - - - -意向共享锁和意向排它锁是数据库主动加的,不需要我们手动处理。意向共享锁和意向排他锁锁定的是表。 +- 如果有了**意向锁**,在对记录加独占锁前,先会加上表级的意向独占锁,那么在加**独占表锁**时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录 -为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁,这两种意向锁都是表锁: +对于insert、update、delete,InnoDB会自动给涉及的数据加排他锁(X);对于一般的Select语句,InnoDB不会加任何锁,事务可以通过以下语句给显示加共享锁或排他锁。 -- **意向共享锁(IS锁,Intention Shared Lock)** -- **意向排他锁(IX锁,Intention Exclusive Lock)** +- 共享锁:`SELECT ... LOCK IN SHARE MODE;` +- 排他锁:`SELECT ... FOR UPDATE;` #### 意向共享锁(Intention Shared Lock) -**意向共享锁(IS锁)是指当事务准备在某条记录上加S锁时,需要先在表级别加一个IS锁。** +**意向共享锁(IS锁)是指当事务准备在某行记录上加共享锁(S锁)时,需要先在表级别加一个IS锁。** **作用**:通知数据库接下来需要施加什么锁并对表加锁。如果需要对记录A加共享锁,那么此时innodb会先找到这张表,对该表加意向共享锁之后,再对记录A添加共享锁。 @@ -828,7 +788,7 @@ select ... for update; #### 意向排他锁(Intention Exclusive Lock) -**意向排他锁(IX锁)是指当事务准备在某条记录上加X锁时,需要先在表级别加一个IX锁** +**意向排他锁(IX锁)是指当事务准备在某行记录上加排他锁(X锁)时,需要先在表级别加一个IX锁** **作用**:通知数据库接下来需要施加什么锁并对表加锁。如果需要对记录A加排他锁,那么此时innodb会先找到这张表,对该表加意向排他锁之后,再对记录A添加排他锁。 @@ -852,7 +812,7 @@ InnoDB 存储引擎提供了个`innodb_autoinc_lock_mode`的系统变量,是 -## 页级锁 +## 页级锁(Page-Level) 页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折中的页级锁,**一次锁定相邻的一组记录**。BDB 支持页级锁。 @@ -860,7 +820,7 @@ InnoDB 存储引擎提供了个`innodb_autoinc_lock_mode`的系统变量,是 -## 行级锁 +## 行级锁(Row-Level) 行级锁是MySQL中锁定粒度最细的一种锁。表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开销也最大。行级锁分为: @@ -954,6 +914,11 @@ id 为 1 的记录行会被锁住。需要注意: UPDATE t_user SET age = 50 WHERE id = 1; ``` +**注意**:在MySQL中,**行级锁**并不是直接锁记录,而是**锁索引**。索引分为**主键索引**和**非主键索引**两种: + +- 如果一条sql语句操作了**主键索引**,MySQL就会**锁定这条主键索引** +- 如果一条语句操作了**非主键索引**,MySQL会**先锁定该非主键索引**,**再锁定相关的主键索引** + #### Gap Lock(间隙锁) @@ -974,9 +939,22 @@ SELECT * FROM t_user WHERE id > 1 AND id < 10 FOR UPDATE; +**间隙锁的目的** + +1. 防止幻读,以满足相关隔离级别的要求 +2. 满足恢复和复制的需要 + +**产生间隙锁的条件(RR事务隔离级别下)** + +1. 使用普通索引锁定 +2. 使用多列唯一索引 +3. 使用唯一索引锁定多行记录 + + + #### Next-Key Lock(临键锁) -Gap Lock + Record Lock, 左开又闭。 +**Next-Key Lock(Record Lock + Gap Lock)锁定的是一个范围,并且锁定记录本身,MySql 防止幻读就是使用此锁实现。** 临键锁是一种特殊的**间隙锁**,也可以理解为一种特殊的**算法**。通过**临建锁**可以解决`幻读`的问题。每个数据行上的`非唯一索引列`上都会存在一把**临键锁**,当某个事务持有该数据行的**临键锁**时,会锁住一段**左开右闭区间**的数据。需要强调的一点是,`InnoDB` 中`行级锁`是基于索引实现的,**临键锁**只与`非唯一索引列`有关,在`唯一索引列`(包括`主键列`)上不存在**临键锁**。 @@ -1015,16 +993,49 @@ INSERT INTO table VALUES(100, 30, 'zhang'); +## 加锁机制 + +### 乐观锁(Optimistic Lock) + +乐观锁的特点先进行业务操作,不到万不得已不去拿锁。即“乐观”的认为拿锁多半是会成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好。 + +乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持。一般的做法是在需要锁的数据上增加一个版本号或时间戳。乐观锁的两种实现方式: + +- **使用数据版本(version)记录对比机制** + + 当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。 + +- **使用时间戳(timestamp)记录对比机制** + + 在更新提交时检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,一致则OK,否则为版本冲突。 + + + +### 悲观锁(Pessimistic Lock) + +悲观锁(**一锁二查三更新**)的特点是先获取锁,再进行业务操作,即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。**在数据库上的悲观锁需要数据库本身提供支持,即通过常用的 `select … for update` 操作来实现悲观锁。** + +MySQL还有个问题是 `select... for update` 语句执行中,如果数据表没有添加索引或主键,所有扫描过的行都会被锁上,这一点很容易造成问题。因此如果在MySQL中用悲观锁务必要确定走了索引,而不是全表扫描。 + + + ## 锁问题 ### 死锁 -当两个及以上的事务,双方都在等待对方释放已经持有的锁或因为加锁顺序不一致造成循环等待锁资源,就会出现“死锁”。常见的报错信息为 ” `Deadlock found when trying to get lock...`”。`MySQL` 出现死锁的几个要素为: +**MyISAM中是不会产生死锁的,因为MyISAM总是一次性获得所需的全部锁,要么全部满足,要么全部等待。而在InnoDB中,锁是逐步获得的,就造成了死锁的可能。** + +当两个及以上的事务,双方都在等待对方释放已经持有的锁或因为加锁顺序不一致造成循环等待锁资源,就会出现“死锁”。常见的报错信息为 ” `Deadlock found when trying to get lock...`”。 + +#### 避免死锁 + +三种常见的避免死锁方式: + +- 如果不同程序会并发存取多个表,尽量**约定以相同的顺序访问表**,可以大大降低死锁机会 +- 在同一个事务中,尽可能做到**一次锁定所需要的所有资源**,减少死锁产生概率 +- 对于非常容易产生死锁的业务部分,可以尝试使用**升级锁定颗粒度**,通过表级锁定来减少死锁产生的概率 + -- 两个或者两个以上事务 -- 每个事务都已经持有锁并且申请新的锁 -- 锁资源同时只能被同一个事务持有或者不兼容 -- 事务之间因为持有锁和申请锁导致彼此循环等待 #### 预防死锁 @@ -1042,8 +1053,8 @@ INSERT INTO table VALUES(100, 30, 'zhang'); #### 解决死锁 -- 等待事务超时,主动回滚 -- 进行死锁检查,主动回滚某条事务,让别的事务能继续走下去 +- **等待事务超时,主动回滚** +- **进行死锁检查,主动回滚某条事务,让别的事务能继续走下去** 下面提供一种方法,解决死锁的状态: @@ -1105,41 +1116,6 @@ kill trx_mysql_thread_id; -# MVCC - -https://mp.weixin.qq.com/s/KbOiJ8SKJ_wFZcIyDVGD9g - -**MVCC**主要是通过**版本链**和**ReadView**来实现的。在Mysql的InnoDB引擎中,只有**已提交读(READ COMMITTD)**和**可重复读(REPEATABLE READ)**这两种隔离级别下的事务采用了MVCC机制。 - -- **版本链** - - 在InnoDB引擎表中,它的每一行记录中有两个必要的隐藏列: - - - `DATA_TRX_ID`:表示插入或更新该行的最后一个事务的事务标识符,同样删除在内部被视为更新,在该更新中,行中的特殊位被设置为将其标记为已删除。行中会有一个特殊位置来标记删除。 - - `DATA_ROLL_PTR`:存储了一个指针,它指向这条记录的上一个版本的位置,通过它来获得上一个版本的记录信息。 - - **作用**:解决了读和写的并发执行。 - -- **ReadView** - - ReadView主要存放的是当前事务操作时,系统中仍然活跃着的事务(事务开启后,没有提交或回滚的事务)。 - - - **ReadView数据结构**:ReadView是MySQL底层使用C++代码实现的一个结构体,主要的内部属性如下: - - `trx_ids`:数组,存储的是创建readview时,活跃事务链表里所有的事务ID - - `low_limit_id`:存储的是创建readview时,活跃事务链表里最大的事务ID - - `up_limit_id`:存储的是创建readview时,活跃事务链表里最小的事务ID - - `creator_trx_id`:当前readview所属事务的事务版本号 - - - **ReadView创建策略**:对于读提交和可重复读事务隔离级别来说,ReadView创建策略是不同的,这样才能保证隔离性不同 - - `可重复读隔离级别`:事务开启后,第一次查询的时候创建,之后一直不变,直到事务结束 - - `读提交隔离级别`:事务开启后,每一次读取都重新创建 - - 也就是说已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。 - - - - - # 数据库事务 **什么叫事务?** @@ -1285,6 +1261,39 @@ https://mp.weixin.qq.com/s/KbOiJ8SKJ_wFZcIyDVGD9g +## MVCC + +https://mp.weixin.qq.com/s/KbOiJ8SKJ_wFZcIyDVGD9g + +**MVCC**主要是通过**版本链**和**ReadView**来实现的。在Mysql的InnoDB引擎中,只有**已提交读(READ COMMITTD)**和**可重复读(REPEATABLE READ)**这两种隔离级别下的事务采用了MVCC机制。 + +- **版本链** + + 在InnoDB引擎表中,它的每一行记录中有两个必要的隐藏列: + + - `DATA_TRX_ID`:表示插入或更新该行的最后一个事务的事务标识符,同样删除在内部被视为更新,在该更新中,行中的特殊位被设置为将其标记为已删除。行中会有一个特殊位置来标记删除。 + - `DATA_ROLL_PTR`:存储了一个指针,它指向这条记录的上一个版本的位置,通过它来获得上一个版本的记录信息。 + + **作用**:解决了读和写的并发执行。 + +- **ReadView** + + ReadView主要存放的是当前事务操作时,系统中仍然活跃着的事务(事务开启后,没有提交或回滚的事务)。 + + - **ReadView数据结构**:ReadView是MySQL底层使用C++代码实现的一个结构体,主要的内部属性如下: + - `trx_ids`:数组,存储的是创建readview时,活跃事务链表里所有的事务ID + - `low_limit_id`:存储的是创建readview时,活跃事务链表里最大的事务ID + - `up_limit_id`:存储的是创建readview时,活跃事务链表里最小的事务ID + - `creator_trx_id`:当前readview所属事务的事务版本号 + + - **ReadView创建策略**:对于读提交和可重复读事务隔离级别来说,ReadView创建策略是不同的,这样才能保证隔离性不同 + - `可重复读隔离级别`:事务开启后,第一次查询的时候创建,之后一直不变,直到事务结束 + - `读提交隔离级别`:事务开启后,每一次读取都重新创建 + + 也就是说已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。 + + + ## Spring事务机制 ### 实现方式