@ -6,16 +6,11 @@
# 数据库范式
# 数据库范式
- **1NF** :所有字段仅包含单值(即单个字段不可在分割使用)
- **1NF** :所有字段值都是不可分解的原子值
- **2NF** :非键字段必须全完依赖于主键(不能是主键的部分)
- **2NF** :确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关
- **3NF** :非键字段不能依赖于非键字段(禁止传递依赖)
- **3NF** :确保数据表中的每一列数据都和主键直接相关,而不能间接相关
- **BCNF** :并且主属性不依赖于主属性
- **BCNF** :并且主属性不依赖于主属性
- ** 第四范式( 4NF) **:要求把同一表内的多对多关系删除
- ** 第四范式( 4NF) **:要求把同一表内的多对多关系删除
- ** 第五范式( 5NF) **:从最终结构重新建立原始结构
- ** 第五范式( 5NF) **:从最终结构重新建立原始结构
> 四种范式之间的关系:
> 四种范式之间的关系:
@ -28,7 +23,7 @@
## 第一范式(1NF)
## 第一范式(1NF)
**列都是不可再分 **。即实体中的某个属性有多个值时,必须拆分为不同的属性。例如:
**所有字段值都是不可分解的原子值 **。即实体中的某个属性有多个值时,必须拆分为不同的属性。例如:
**用户信息表**
**用户信息表**
@ -53,7 +48,7 @@
## 第二范式(2NF)
## 第二范式(2NF)
**每个表只描述一件事情**。满足第一范式( 1NF) 的情况下, 每行必须有主键, 且主键与非主键之间是完全函数依赖关系( 消除部分子函数依赖) 。即数据库表中的每一列都和主键的所有属性相关, 不能只和主键的部分属性相关。完全函数依赖: 有属性集 X, Y, 通过 X 中的所有属性能够推出 Y 中的任意属性,但是 X 的任何真子集,都不能推出 Y 中的任何属性 。例如:
**确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关**(主要针对联合主键而言)。也就是说在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中 。例如:
**学生课程表**
**学生课程表**
@ -98,7 +93,7 @@
## 第三范式(3NF)
## 第三范式(3NF)
**不存在对非主键列的传递依赖 **。满足第二范式( 2NF) 的情况下, 任何非主属性不依赖于其它非主属性( 消除传递函数依赖) 。例如:
**确保数据表中的每一列数据都和主键直接相关,而不能间接相关 **。满足第二范式( 2NF) 的情况下, 任何非主属性不依赖于其它非主属性( 消除传递函数依赖) 。例如:
**学生表**
**学生表**
@ -401,7 +396,7 @@ mysql> SELECT *
## LEFT JOIN EXCLUDING INNER JOIN
## LEFT JOIN EXCLUDING INNER JOIN
返回左表有但右表没有关联数据的记录集。
**左连接后排除内连接**。 返回左表有但右表没有关联数据的记录集。
**文氏图**
**文氏图**
@ -432,7 +427,7 @@ WHERE B.PK IS NULL;
## RIGHT JOIN EXCLUDING INNER JOIN
## RIGHT JOIN EXCLUDING INNER JOIN
返回右表有但左表没有关联数据的记录集。
**右连接后排除内连接**。 返回右表有但左表没有关联数据的记录集。
**文氏图**
**文氏图**
@ -463,7 +458,7 @@ WHERE A.PK IS NULL;
## FULL JOIN EXCLUDING INNER JOIN
## FULL JOIN EXCLUDING INNER JOIN
返回左表和右表里没有相互关联的记录集。
**全连接后排除内连接**。 返回左表和右表里没有相互关联的记录集。
**文氏图**
**文氏图**
@ -614,87 +609,229 @@ WHERE A.EMP_SUPV_ID = B.EMP_ID;
表级锁和行级锁的区别:
在 InnoDB 存储引擎中:
- SELECT 操作的不可重复读问题通过`MVCC`得到了解决
- UPDATE、DELETE 的不可重复读问题通过`Record Lock`(记录锁)解决
- INSERT 的不可重复读问题是通过`Next-Key Lock`(临键锁)解决
按照对数据操作的锁粒度来分:
- ** 行级锁**
- ** 表级锁**
- ** 页级锁**
- ** 间隙锁**
按照锁的共享策略来分:
- ** 读锁(共享锁)**: Shared Locks( S锁) 。针对同一份数据, 多个读操作可以同时进行而不会互相影响
- ** 写锁(排它锁)**: Exclusive Locks( X锁) 。当前写操作没有完成前, 它会阻断其他写锁和读锁
- **IS锁** : 意向共享锁、Intention Shared Lock。当事务准备在某条记录上加S锁时, 需要先在表级别加一个IS锁
- **IX锁** : 意向排他锁、Intention Exclusive Lock。当事务准备在某条记录上加X锁时, 需要先在表级别加一个IX锁
- ** 表级锁**:开销⼩,加锁快;不会出现死锁;锁定粒度⼤,发⽣锁冲突的概率最⾼,并发度最低
- ** ⾏级锁**:开销⼤,加锁慢;会出现死锁;锁定粒度最⼩,发⽣锁冲突的概率最低,并发度也最⾼
从加锁策略上分:
- ** 乐观锁**
- ** 悲观锁**
其它:
- ** 自增锁( AUTO-INC锁) **:自增锁是一种特殊的**表级锁**, 主要用于事务中插入自增字段, 也就是我们最常用的自增主键id
## 全局锁
## 全局锁
锁的是整个database。由MySQL的SQL layer层实现的。
**全局锁就是对整个数据库实例加锁**。MySQL 提供了一个加全局读锁的方法,命令是`Flush tables with read lock (FTWRL)`。 当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:
- ** 数据更新语句**(数据的增删改)
- ** 数据定义语句**(包括建表、修改表结构等)
- ** 更新类事务的提交语句**
### 使用场景
## 行级锁
- 典型使用场景是做**全库逻辑备份( mysqldump) **
在 `InnoDB` 事务中,行锁通过给索引上的索引项加锁来实现。即只有通过索引条件检索数据,`InnoDB` 才使用行级锁,否则将使用表锁。行级锁定同样分为两种类型:`共享锁` 和`排他锁`,以及加锁前需要先获得的 `意向共享锁` 和 `意向排他锁` 。行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
**数据库只读状态的危险性:**
在 InnoDB 存储引擎中, SELECT 操作的不可重复读问题通过 MVCC 得到了解决,而 UPDATE、DELETE 的不可重复读问题通过 Record Lock 解决, INSERT 的不可重复读问题是通过 Next-Key Lock( Record Lock + Gap Lock) 解决的。
- 如果你在主库上备份,那么在备份期间都不能执行更新,业务基本上就能停止
- 如果你在从库上备份, 那么备份期间从库不能执行主库同步过来的binlog, 会导致主从延迟
**共享锁(Shared Locks)**
### 全局读锁(FTWRL)
共享锁又称为 `S锁` 或 `读锁` 。若事务T对数据对象A加上 `S锁` , 则事务T `只能读A` ; 其他事务只能再对A加S锁, 而不能加X锁, 直到T释放A上的S锁。这就保证了其他事务可以读A, 但在T释放A上的S锁之前不能对A做任何修改。
**为什么需要全局读锁(FTWRL)? **
- `select ... lock in share mode` : 会加`共享锁`
当 `mysqldump` 使用参数 `--single-transaction` 的时候,导数据之前就会启动一个事务,来确保拿到一致性快照视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。所以,**single-transaction 方法只适用于所有的表使用事务引擎的库**。如果有的表使用了不支持事务的引擎,那么备份就只能通过 FTWRL 方法。这往往是 DBA 要求业务开发人员使用 InnoDB 替代 MyISAM 的原因之一。
**排它锁(Exclusive Locks)**
要使用全局锁,则要执行这条命:
排它锁又称为 `X锁` 或 `写锁` 。若事务T对数据对象A加上X锁, 则只允许T读取和修改A, 其它任何事务都不能再对A加任何类型的锁, 直到T释放A上的锁。它防止任何其它事务获取资源上的锁, 直到在事务的末尾将资源上的原始锁释放为止。在更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁。
```sql
flush tables with read lock
```
注意:排他锁会阻止其它事务再对其**锁定的数据**加读或写的锁,但是不加锁的就没办法控制了。
执行后,**整个数据库就处于只读状态了**,这时其他线程执行以下操作,都会被阻塞:
- `insert` 、`update`、`delete`、`select ... for update`:会加`排它锁`
- 对数据的增删查改操作,比如 insert、delete、update等语句;
- 对表结构的更改操作,比如 alter table、drop table 等语句。
如果要释放全局锁,则要执行这条命令:
```sql
unlock tables
```
**MVCC**
当然,当会话断开了,全局锁会被自动释放。
**MVCC**主要是通过**版本链**和**ReadView**来实现的。在Mysql的InnoDB引擎中, 只有**已提交读(READ COMMITTD)**和**可重复读(REPEATABLE READ)**这两种隔离级别下的事务采用了MVCC机制。
- ** 版本链**
在InnoDB引擎表中, 它的每一行记录中有两个必要的隐藏列:
### 两种全局锁
- `DATA_TRX_ID` :表示插入或更新该行的最后一个事务的事务标识符,同样删除在内部被视为更新,在该更新中,行中的特殊位被设置为将其标记为已删除。行中会有一个特殊位置来标记删除。
全局锁两种方法:
- `DATA_ROLL_PTR` :存储了一个指针,它指向这条记录的上一个版本的位置,通过它来获得上一个版本的记录信息。
** 作用**:解决了读和写的并发执行。
- **FLUSH TABLES WRITE READ LOCK( `FTWRL`) **
- **set global readonly=true**
- **ReadView**
一般建议使用 `FTWRL` 方式,因为:
ReadView主要存放的是当前事务操作时, 系统中仍然活跃着的事务( 事务开启后, 没有提交或回滚的事务) 。
- 有些系统中 readonly 的值会被用来做其它逻辑。如判断一个库是主库或备库。因此修改 global 变量的方式影响面更大
- 异常处理机制上有差异:
- 如果执行`FTWRL`后由于客户端发生异常断开, 则MySQL会自动释放这个全局锁, 整个库回到可正常更新状态
- 将库设置为`readonly`后,若客户端发生异常,则数据库会一直保持`readonly`状态,导致整库长时间处于不可写状态
- readonly 对super用户权限无效
- **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。
## 表级锁
表级锁是MySQL中锁定粒度最大的一种锁, 表示对当前操作的整张表加锁, 它实现简单, 资源消耗较少, 被大部分MySQL引擎支持。最常使用的MyISAM与InnoDB都支持表级锁定。表级锁分为:
- ** 表共享读锁(共享锁)**
- ** 表独占写锁(排他锁)**
**行锁实现算法**
**特点:开销小、加锁快、不会出现死锁、发生锁冲突的概率最高、并发度也最低。 **
- Record Lock( 记录锁)
### 表锁
- Gap Lock( 间隙锁)
- Next-Key Lock( 间隙锁)
**表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作**。
如果我们想对学生表( t_student) 加表锁, 可以使用下面的命令:
```sql
-- 表级别的共享锁,也就是读锁
lock tables t_student read;
-- 表级别的独占锁,也就是写锁
lock tables t_stuent wirte;
```
不过尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能,**InnoDB 的优势在于实现了颗粒度更细的行级锁**。要释放表锁,可以使用下面这条命令,会释放当前会话的所有表锁:
```sql
unlock tables
```
### 元数据锁(MDL)
**MDL作用是防止DDL和DML并发的冲突, 保证读写的正确性**。元数据锁是为了保证当用户对表执行 CRUD 操作时,防止其它线程对这个表结构做了变更。不需要显示的使用 MDL, 因为当我们对数据库表进行操作时, 会自动给这个表加上 MDL。
- 对一张表进行 CRUD 操作时,加的是 **MDL 读锁**
当有线程在执行 select 语句( 加 MDL 读锁)的期间,如果有其他线程要更改该表的结构( 申请 MDL 写锁),那么将会被阻塞,直到执行完 select 语句( 释放 MDL 读锁)。
- 对一张表做结构变更操作的时候,加的是 **MDL 写锁**
当有线程对表结构进行变更( 加 MDL 写锁)的期间,如果有其他线程执行了 CRUD 操作( 申请 MDL 读锁),那么就会被阻塞,直到表结构变更完成( 释放 MDL 写锁)。
MDL 是在事务提交后才会释放,这意味着**事务执行期间, MDL是一直持有的**。申请 MDL 锁的操作会形成一个队列,队列中**写锁获取优先级高于读锁**,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。
### 意向锁
**意向锁的目的是为了快速判断表里是否有记录被加锁**。
- 在使用 InnoDB 引擎的表里对某些记录加上**共享锁**之前,需要先在表级别加上一个**意向共享锁**
- 在使用 InnoDB 引擎的表里对某些纪录加上**独占锁**之前,需要先在表级别加上一个**意向独占锁**
也就是,当执行插入、更新、删除操作,需要先对**表**加上**意向共享锁**,然后对该记录加**独占锁**。而普通的 select 是不会加行级锁的,**普通的select语句是利用MVCC实现一致性读**, 是无锁的。不过select也是可对记录加共享锁和独占锁, 如下:
```sql
-- 先在表上加上意向共享锁,然后对读取的记录加独占锁
select ... lock in share mode;
-- 先表上加上意向独占锁,然后对读取的记录加独占锁
select ... for update;
```
意向共享锁和意向独占锁是表级锁, 不会和行级的共享锁和独占锁发生冲突, 而且意向锁之间也不会发生冲突, 只会和共享表锁( lock tables … read) 和独占表锁( lock tables … write) 发生冲突。
表锁和行锁是满足读读共享、读写互斥、写写互斥的。
- 如果没有**意向锁**,那么加**独占表锁**时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢
- 如果有了**意向锁**,由于在对记录加独占锁前,先会加上表级别的意向独占锁,那么在加**独占表锁**时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录
### AUTO-INC锁(自增长锁)
在为某个字段声明 `AUTO_INCREMENT` 属性时,之后可以在插入数据时,可以不指定该字段的值,数据库会自动给该字段赋值递增的值,这主要是通过 `AUTO-INC` 锁实现的。
`AUTO-INC` 锁是特殊的表锁机制,锁**不是在一个事务提交后才释放,而是在执行完插入语句后就会立即释放**。**在插入数据时,会加一个表级别的 `AUTO-INC` 锁**,然后为被 `AUTO_INCREMENT` 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 `AUTO-INC` 锁释放掉。
`AUTO-INC` 锁在对大量数据进行插入的时候,会影响插入性能,因为另一个事务中的插入会被阻塞。因此, 在 MySQL 5.1.22 版本开始, InnoDB 存储引擎提供了一种**轻量级的锁**来实现自增。一样也是在插入数据的时候,会为被 `AUTO_INCREMENT` 修饰的字段加上轻量级锁,**然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁**。
InnoDB 存储引擎提供了个`innodb_autoinc_lock_mode`的系统变量,是用来控制选择用 `AUTO-INC` 锁,还是**轻量级**的锁。
- innodb_autoinc_lock_mode=0: **采用`AUTO-INC`锁**
- innodb_autoinc_lock_mode=2: **采用轻量级锁**。性能最高,但自增长值可能不是连续的,在主从复制场景中是不安全的
- innodb_autoinc_lock_mode=1: 默认值, 两种锁混着用
- 如果能够确定插入记录的数量就采用**轻量级**锁
- 不确定时就采用 `AUTO-INC` 锁
## 页级锁
页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折中的页级锁,**一次锁定相邻的一组记录**。BDB 支持页级锁。
**特点:开销和加锁时间界于表锁和行锁之间、会出现死锁、锁定粒度界于表锁和行锁之间、并发度一般。**
## 行级锁
行级锁是MySQL中锁定粒度最细的一种锁。表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突, 其加锁粒度最小, 但加锁的开销也最大。行级锁分为:
- ** 共享锁**
- ** 排他锁**
### Record Lock( 记录锁)
**特点:开销大、加锁慢、会出现死锁、发生锁冲突的概率最低、并发度也最高。**
在 `InnoDB` 事务中,行锁通过给索引上的**索引项加锁**来实现。只有通过索引条件检索数据,才使用行级锁,否则将使用表锁。行级锁定同样分为两种类型:`共享锁` 和 `排他锁` ,以及加锁前需要先获得的 `意向共享锁` 和 `意向排他锁` 。行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是**两阶段锁协议**。
行锁实现算法(**3种锁都是排它锁(X锁)**):
- **Record Lock(记录锁)**
- **Gap Lock(间隙锁)**
- **Next-Key Lock(间隙锁)**
### Record Lock(记录锁)
记录锁就是为**某行**记录加锁,它封锁该行的索引记录:
记录锁就是为**某行**记录加锁,它封锁该行的索引记录:
@ -717,7 +854,7 @@ UPDATE t_user SET age = 50 WHERE id = 1;
### Gap Lock(间隙锁)
### Gap Lock(间隙锁)
**间隙锁**基于`非唯一索引`,它`锁定一段范围内的索引记录`。**间隙锁**基于下面将会提到的`Next-Key Locking` 算法,请务必牢记:**使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据**。
**间隙锁**基于`非唯一索引`,它`锁定一段范围内的索引记录`。**间隙锁**基于下面将会提到的`Next-Key Locking` 算法,请务必牢记:**使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据**。
@ -733,7 +870,7 @@ SELECT * FROM t_user WHERE id > 1 AND id < 10 FOR UPDATE;
### Next-Key Lock(临键锁)
### Next-Key Lock(临键锁)
临键锁是一种特殊的**间隙锁**,也可以理解为一种特殊的**算法**。通过**临建锁**可以解决`幻读`的问题。每个数据行上的`非唯一索引列`上都会存在一把**临键锁**,当某个事务持有该数据行的**临键锁**时,会锁住一段**左开右闭区间**的数据。需要强调的一点是,`InnoDB` 中`行级锁`是基于索引实现的,**临键锁**只与`非唯一索引列`有关,在`唯一索引列`(包括`主键列`)上不存在**临键锁**。
临键锁是一种特殊的**间隙锁**,也可以理解为一种特殊的**算法**。通过**临建锁**可以解决`幻读`的问题。每个数据行上的`非唯一索引列`上都会存在一把**临键锁**,当某个事务持有该数据行的**临键锁**时,会锁住一段**左开右闭区间**的数据。需要强调的一点是,`InnoDB` 中`行级锁`是基于索引实现的,**临键锁**只与`非唯一索引列`有关,在`唯一索引列`(包括`主键列`)上不存在**临键锁**。
@ -768,96 +905,58 @@ INSERT INTO t_user VALUES(100, 26, 'tian');
INSERT INTO table VALUES(100, 30, 'zhang');
INSERT INTO table VALUES(100, 30, 'zhang');
```
```
那最终我们就可以得知,在根据`非唯一索引` 对记录行进行 `UPDATE \ FOR UPDATE \ LOCK IN SHARE MODE` 操作时, InnoDB 会获取该记录行的 `临键锁` ,并同时获取该记录行下一个区间的`间隙锁`。
那最终我们就可以得知,在根据`非唯一索引` 对记录行进行 `UPDATE` 、`FOR UPDATE`、`LOCK IN SHARE MODE` 操作时, InnoDB 会获取该记录行的 `临键锁` ,并同时获取该记录行下一个区间的`间隙锁`。即`事务 A`在执行了上述的 SQL 后,最终被锁住的记录区间为 `(10, 32)` 。
即`事务 A`在执行了上述的 SQL 后,最终被锁住的记录区间为 `(10, 32)` 。
## 表级锁
MySQL 里面表级别的锁有这几种:
- 表锁
- 元数据锁( MDL)
- 意向锁
- AUTO-INC 锁
### 表锁
- 表锁会限制别的线程的读写外
- 表锁也会限制本线程接下来的读写操作
如果我们想对学生表( t_student) 加表锁, 可以使用下面的命令:
```sql
-- 表级别的共享锁,也就是读锁
lock tables t_student read;
-- 表级别的独占锁,也就是写锁
lock tables t_stuent wirte;
```
不过尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能,**InnoDB 的优势在于实现了颗粒度更细的行级锁**。要释放表锁,可以使用下面这条命令,会释放当前会话的所有表锁:
```sql
unlock tables
```
### 元数据锁( MDL)
## 其它锁
MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。不需要显示的使用 MDL, 因为当我们对数据库表进行操作时, 会自动给这个表加上 MDL:
### 共享锁(Shared Locks)
- 对一张表进行 CRUD 操作时,加的是 **MDL 读锁**
共享锁又称为 `S锁` 或 `读锁` 。若事务T对数据对象A加上 `S锁` , 则事务T `只能读A` ; 其他事务只能再对A加S锁, 而不能加X锁, 直到T释放A上的S锁。这就保证了其他事务可以读A, 但在T释放A上的S锁之前不能对A做任何修改。
当有线程在执行 select 语句( 加 MDL 读锁)的期间,如果有其他线程要更改该表的结构( 申请 MDL 写锁),那么将会被阻塞,直到执行完 select 语句( 释放 MDL 读锁)。
- 对一张表做结构变更操作的时候,加的是 **MDL 写锁**
- `select ... lock in share mode` : 会加`共享锁`
当有线程对表结构进行变更( 加 MDL 写锁)的期间,如果有其他线程执行了 CRUD 操作( 申请 MDL 读锁),那么就会被阻塞,直到表结构变更完成( 释放 MDL 写锁)。
### 排它锁(Exclusive Locks)
MDL 是在事务提交后才会释放,这意味着**事务执行期间, MDL 是一直持有的**。申请 MDL 锁的操作会形成一个队列,队列中**写锁获取优先级高于读锁**,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作 。
排它锁又称为 `X锁` 或 `写锁` 。若事务T对数据对象A加上X锁, 则只允许T读取和修改A, 其它任何事务都不能再对A加任何类型的锁, 直到T释放A上的锁。它防止任何其它事务获取资源上的锁, 直到在事务的末尾将资源上的原始锁释放为止。在更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁。
注意:排他锁会阻止其它事务再对其**锁定的数据**加读或写的锁,但是不加锁的就没办法控制了。
- `insert` 、`update`、`delete`、`select ... for update`:会加`排它锁`
### 意向锁
- 在使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前,需要先在表级别加上一个「意向共享锁」;
- 在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」;
也就是,当执行插入、更新、删除操作,需要先对表加上「意向共享锁」,然后对该记录加独占锁。而普通的 select 是不会加行级锁的,普通的 select 语句是利用 MVCC 实现一致性读, 是无锁的。不过, select 也是可以对记录加共享锁和独占锁的,具体方式如下:
### MVCC
```sql
**MVCC**主要是通过**版本链**和**ReadView**来实现的。在Mysql的InnoDB引擎中, 只有**已提交读(READ COMMITTD)**和**可重复读(REPEATABLE READ)**这两种隔离级别下的事务采用了MVCC机制。
-- 先在表上加上意向共享锁,然后对读取的记录加独占锁
select ... lock in share mode;
-- 先表上加上意向独占锁,然后对读取的记录加独占锁
select ... for update;
```
**意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(\*lock tables … read\*)和独占表锁(\*lock tables … write\*)发生冲突。**
表锁和行锁是满足读读共享、读写互斥、写写互斥的。如果没有「意向锁」,那么加「独占表锁」时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢。那么有了「意向锁」,由于在对记录加独占锁前,先会加上表级别的意向独占锁,那么在加「独占表锁」时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录。所以,**意向锁的目的是为了快速判断表里是否有记录被加锁**。
- ** 版本链**
在InnoDB引擎表中, 它的每一行记录中有两个必要的隐藏列:
### AUTO-INC锁(自增长锁)
- `DATA_TRX_ID` :表示插入或更新该行的最后一个事务的事务标识符,同样删除在内部被视为更新,在该更新中,行中的特殊位被设置为将其标记为已删除。行中会有一个特殊位置来标记删除。
- `DATA_ROLL_PTR` :存储了一个指针,它指向这条记录的上一个版本的位置,通过它来获得上一个版本的记录信息。
在为某个字段声明 `AUTO_INCREMENT` 属性时,之后可以在插入数据时,可以不指定该字段的值,数据库会自动给该字段赋值递增的值,这主要是通过 AUTO-INC 锁实现的 。
** 作用**:解决了读和写的并发执行。
AUTO-INC 锁是特殊的表锁机制,锁**不是再一个事务提交后才释放,而是再执行完插入语句后就会立即释放**。**在插入数据时,会加一个表级别的 AUTO-INC 锁**,然后为被 `AUTO_INCREMENT` 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉。
- **ReadView**
AUTO-INC 锁再对大量数据进行插入的时候,会影响插入性能,因为另一个事务中的插入会被阻塞。因此, 在 MySQL 5.1.22 版本开始, InnoDB 存储引擎提供了一种**轻量级的锁**来实现自增。一样也是在插入数据的时候,会为被 `AUTO_INCREMENT` 修饰的字段加上轻量级锁,**然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁** 。
ReadView主要存放的是当前事务操作时, 系统中仍然活跃着的事务( 事务开启后, 没有提交或回滚的事务) 。
InnoDB 存储引擎提供了个 innodb_autoinc_lock_mode 的系统变量,是用来控制选择用 AUTO-INC 锁,还是轻量级的锁。
- **ReadView数据结构** : ReadView是MySQL底层使用C++代码实现的一个结构体,主要的内部属性如下:
- `trx_ids` : 数组, 存储的是创建readview时, 活跃事务链表里所有的事务ID
- `low_limit_id` : 存储的是创建readview时, 活跃事务链表里最大的事务ID
- `up_limit_id` : 存储的是创建readview时, 活跃事务链表里最小的事务ID
- `creator_trx_id` : 当前readview所属事务的事务版本号
- 当 innodb_autoinc_lock_mode = 0, 就采用 AUTO-INC 锁
- **ReadView创建策略** : 对于读提交和可重复读事务隔离级别来说, ReadView创建策略是不同的, 这样才能保证隔离性不同
- 当 innodb_autoinc_lock_mode = 2, 就采用轻量级锁
- `可重复读隔离级别` :事务开启后,第一次查询的时候创建,之后一直不变,直到事务结束
- 当 innodb_autoinc_lock_mode = 1, 这个是默认值, 两种锁混着用, 如果能够确定插入记录的数量就采用轻量级锁, 不确定时就采用 AUTO-INC 锁
- `读提交隔离级别` :事务开启后,每一次读取都重新创建
不过,当 innodb_autoinc_lock_mode = 2 是性能最高的方式,但是会带来一定的问题。因为并发插入的存在,在每次插入时,自增长的值可能不是连续的,**这在有主从复制的场景中是不安全的** 。
也就是说已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView, 而可重复读隔离级别则在第一次读的时候生成一个ReadView, 之后的读都复用之前的ReadView 。
@ -911,7 +1010,7 @@ kill trx_mysql_thread_id;
### 脏读
### 脏读
脏读指的是不同事务下,当前事务可以读取到另外事务未提交的数据。
** 脏读指的是不同事务下,当前事务可以读取到另外事务未提交的数据** 。
例如: T1 修改一个数据, T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。
例如: T1 修改一个数据, T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。
@ -921,7 +1020,7 @@ kill trx_mysql_thread_id;
### 不可重复读
### 不可重复读
不可重复读指的是同一事务内多次读取同一数据集合,读取到的数据是不一样的情况。
** 不可重复读指的是同一事务内多次读取同一数据集合,读取到的数据是不一样的情况** 。
例如: T2 读取一个数据, T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
例如: T2 读取一个数据, T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
@ -937,13 +1036,13 @@ kill trx_mysql_thread_id;
### 幻读
### 幻读
幻读是指在同一事务下,连续执行两次同样的 sql 语句可能返回不同的结果,第二次的 sql 语句可能会返回之前不存在的行。幻影读是一种特殊的不可重复读问题。
** 幻读是指在同一事务下,连续执行两次同样的 sql 语句可能返回不同的结果,第二次的 sql 语句可能会返回之前不存在的行** 。幻影读是一种特殊的不可重复读问题。
### 丢失更新
### 丢失更新
一个事务的更新操作会被另一个事务的更新操作所覆盖。
** 一个事务的更新操作会被另一个事务的更新操作所覆盖** 。
例如: T1 和 T2 两个事务都对一个数据进行修改, T1 先修改, T2 随后修改, T2 的修改覆盖了 T1 的修改。
例如: T1 和 T2 两个事务都对一个数据进行修改, T1 先修改, T2 随后修改, T2 的修改覆盖了 T1 的修改。
@ -997,31 +1096,7 @@ kill trx_mysql_thread_id;
`InnnoDB` 有很多 log, 持久性靠的是`redo log`。持久性肯定和写有关,`MySQL` 里经常说到的 `WAL` 技术,`WAL`的全称是`Write-Ahead Logging`,它的关键点就是先写日志,再写磁盘。
`InnnoDB` 有很多 log, 持久性靠的是`redo log`。持久性肯定和写有关,`MySQL` 里经常说到的 `WAL` 技术,`WAL`的全称是`Write-Ahead Logging`,它的关键点就是先写日志,再写磁盘。
当事务提交时,会调用`fsync`接口对`redo log`进行刷盘。如果`MySQL`宕机,重启时可以读取`redo log`中的数据,对数据库进行恢复。`redo log`采用的是WAL( `Write-ahead logging`,预写式日志),所有修改先写入日志,再更新到`Buffer Pool`, 保证了数据不会因MySQL宕机而丢失, 从而满足了持久性要求。而且这样做还有两个优点:
**redo log**
当有一条记录要更新时,`InnoDB`引擎就会先把记录写到`redo log`(并更新内存),这个时候更新就算完成了。在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。`redo log`有两个特点
- 大小固定,循环写
- `crash-safe`
对于`redo log`是有两阶段的:`commit` 和 `prepare` 如果不使用“两阶段提交”,数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。
**Buffer Pool**
InnoDB还提供了缓存, `Buffer Pool`中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:
- 当读取数据时,会先从`Buffer Pool`中读取,如果`Buffer Pool`中没有, 则从磁盘读取后放入Buffer Pool
- 当向数据库写入数据时,会首先写入`Buffer Pool`, `Buffer Pool`中修改的数据会定期刷新到磁盘中
Buffer Pool 的使用大大提高了读写数据的效率,但是也带了新的问题:如果`MySQL`宕机,而此时 `Buffer Pool` 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。
**所以加入了 redo log。** 当数据修改时,除了修改`Buffer Pool`中的数据,还会在`redo log`记录这次操作。当事务提交时,会调用`fsync`接口对`redo log`进行刷盘。如果`MySQL`宕机,重启时可以读取`redo log`中的数据,对数据库进行恢复。
`redo log` 采用的是WAL( `Write-ahead logging`,预写式日志),所有修改先写入日志,再更新到`Buffer Pool`, 保证了数据不会因MySQL宕机而丢失, 从而满足了持久性要求。而且这样做还有两个优点:
- 刷脏页是随机`IO`, `redo log` 顺序`IO`
- 刷脏页是随机`IO`, `redo log` 顺序`IO`
- 刷脏页以Page为单位, 一个Page上的修改整页都要写; 而redo log 只包含真正需要写入的,无效 IO 减少
- 刷脏页以Page为单位, 一个Page上的修改整页都要写; 而redo log 只包含真正需要写入的,无效 IO 减少
@ -1030,10 +1105,6 @@ Buffer Pool 的使用大大提高了读写数据的效率,但是也带了新
## 隔离级别
## 隔离级别
数据库事务隔离级别有4种, 由低到高为: **Read uncommitted** 、**Read committed** 、**Repeatable read** 、**Serializable** 。
**事务并发问题**
**事务并发问题**
在事务的并发操作中,不做隔离操作则可能会出现 ** 脏读、不可重复读、幻读** 问题:
在事务的并发操作中,不做隔离操作则可能会出现 ** 脏读、不可重复读、幻读** 问题:
@ -1041,23 +1112,10 @@ Buffer Pool 的使用大大提高了读写数据的效率,但是也带了新
- ** 脏读**: **事务A中读到了事务B中未提交的更新数据内容**。然后B回滚操作, 那么A读取到的数据是脏数据
- ** 脏读**: **事务A中读到了事务B中未提交的更新数据内容**。然后B回滚操作, 那么A读取到的数据是脏数据
- ** 不可重复读**: **事务A读到事务B已经提交后的数据**。即事务A多次读取同一数据时, 返回结果不一致
- ** 不可重复读**: **事务A读到事务B已经提交后的数据**。即事务A多次读取同一数据时, 返回结果不一致
- ** 幻读**: 事物A执行select后, 事物B**增或删**了一条数据, 事务A再执行同一条SQL后发现多或少了一条数据
- ** 幻读**: 事物A执行select后, 事物B**增或删**了一条数据, 事务A再执行同一条SQL后发现多或少了一条数据
- ** 第一类丢失更新:** A事务撤销事务时, 覆盖了B事务提交的事务( 现代关系型数据库中已经不会发生)
- ** 第二类丢失更新:** A事务提交事务时, 覆盖了B事务提交的事务( 是不可重复读的特殊情况)
**小结**:不可重复读的和幻读很容易混淆,**不可重复读**侧重于**修改**, **幻读**侧重于**新增或删除**。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。
**小结**:不可重复读的和幻读很容易混淆,**不可重复读**侧重于**修改**, **幻读**侧重于**新增或删除**。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。查看 `mysql` 事务隔离级别:`show variables like 'tx_iso%';`。
**默认隔离级别**
- Oracle仅有Serializable(串行化)和Read Committed(读已提交)两种隔离方式,默认选择**读已提交**的方式
- MySQL默认为**Repeatable Read( 可重读) **
**数据库中的锁**
- ** 共享锁( Share locks简记为S锁) **:也称**读锁**, 事务A对对象T加s锁, 其他事务也只能对T加S, 多个事务可以同时读, 但不能有写操作, 直到A释放S锁。
- ** 排它锁( Exclusivelocks简记为X锁) **:也称**写锁**, 事务A对对象T加X锁以后, 其他事务不能对T加任何锁, 只有事务A可以读写对象T直到A释放X锁。
- ** 更新锁( 简记为U锁) **: 用来预定要对此对象施加X锁, 它允许其他事务读, 但不允许再施加U锁或X锁; 当被读取的对象将要被更新时, 则升级为X锁, 主要是用来防止死锁的。因为使用共享锁时, 修改数据的操作分为两步, 首先获得一个共享锁, 读取数据, 然后将共享锁升级为排它锁, 然后再执行修改操作。这样如果同时有两个或多个事务同时对一个对象申请了共享锁, 在修改数据的时候, 这些事务都要将共享锁升级为排它锁。这些事务都不会释放共享锁而是一直等待对方释放, 这样就造成了死锁。如果一个数据在修改前直接申请更新锁, 在数据修改的时候再升级为排它锁, 就可以避免死锁。
@ -1070,6 +1128,8 @@ Buffer Pool 的使用大大提高了读写数据的效率,但是也带了新
| Repeatable Read(可重复读) | 不可能 | 不可能 | 不可能 | 不可能 | ** 可能** |
| Repeatable Read(可重复读) | 不可能 | 不可能 | 不可能 | 不可能 | ** 可能** |
| Serializable(串行化) | 不可能 | 不可能 | 不可能 | 不可能 | 不可能 |
| Serializable(串行化) | 不可能 | 不可能 | 不可能 | 不可能 | 不可能 |
### Read Uncommitted(读未提交)
### Read Uncommitted(读未提交)
**即读取到了其它事务未提交的内容**。在该隔离级别,**所有事务都可以看到其他未提交事务的执行结果**。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为**脏读( Dirty Read) **。
**即读取到了其它事务未提交的内容**。在该隔离级别,**所有事务都可以看到其他未提交事务的执行结果**。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为**脏读( Dirty Read) **。
@ -1135,9 +1195,7 @@ Buffer Pool 的使用大大提高了读写数据的效率,但是也带了新
## Spring事务
## Spring事务机制
查看 `mysql` 事务隔离级别:`show variables like 'tx_iso%';`。
### 实现方式
### 实现方式
@ -1151,7 +1209,7 @@ Buffer Pool 的使用大大提高了读写数据的效率,但是也带了新
### 提交方式
### 提交方式
**默认情况下,数据库处于自动提交模式**。每一条语句处于一个单独的事务中,在这条语句执行完毕时,如果执行成功则隐式的提交事务,如果执行失败则隐式的回滚事务。
**默认情况下,数据库处于自动提交模式**。每一条语句处于一个单独的事务中,在这条语句执行完毕时,如果执行成功则隐式的提交事务,如果执行失败则隐式的回滚事务。
对于正常的事务管理,是一组相关的操作处于一个事务之中,因此必须关闭数据库的自动提交模式。不过,这个我们不用担心,spring会将底层连接的自动提交特性设置为false。也就是在使用spring进行事物管理的时候, s pring会将是否自动提交设置为false, 等价于JDBC中的 `connection.setAutoCommit(false);` ,在执行完之后在进行提交,`connection.commit();` 。
对于正常的事务管理,是一组相关的操作处于一个事务之中,因此必须关闭数据库的自动提交模式。不过,这个我们不用担心,Spring会将底层连接的自动提交特性设置为false。也就是在使用Spring进行事物管理的时候, S pring会将是否自动提交设置为false, 等价于JDBC中的 `connection.setAutoCommit(false);` ,在执行完之后在进行提交,`connection.commit();` 。
@ -1168,7 +1226,7 @@ public void addGoods(){
枚举类Isolation中定义了五种隔离级别:
枚举类Isolation中定义了五种隔离级别:
- `DEFAULT` :默认值。表示使用底层数据库的默认隔离级别。对大部分数据库而言 ,通常这值就是**READ_COMMITTED**
- `DEFAULT` :默认值。表示使用底层数据库的默认隔离级别。对大部分数据库,通常这值就是**READ_COMMITTED**
- `READ_UNCOMMITTED` :该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别
- `READ_UNCOMMITTED` :该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别
- `READ_COMMITTED` :该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值
- `READ_COMMITTED` :该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值
- `REPEATABLE_READ` :该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读
- `REPEATABLE_READ` :该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读
@ -1201,8 +1259,8 @@ public void addGoods(){
### 事务回滚规则
### 事务回滚规则
指示s pring事务管理器回滚一个事务的推荐方法是在当前事务的上下文内抛出异常。s pring事务管理器会捕捉任何未处理的异常, 然后依据规则决定是否回滚抛出异常的事务。
指示S pring事务管理器回滚一个事务的推荐方法是在当前事务的上下文内抛出异常。S pring事务管理器会捕捉任何未处理的异常, 然后依据规则决定是否回滚抛出异常的事务。
默认配置下,s pring只有在抛出的异常为运行时unchecked异常时才回滚该事务, 也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚), 而抛出checked异常则不会导致事务回滚。
默认配置下,S pring只有在抛出的异常为运行时unchecked异常时才回滚该事务, 也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚), 而抛出checked异常则不会导致事务回滚。
可以明确的配置在抛出那些异常时回滚事务, 包括checked异常。也可以明确定义那些异常抛出时不回滚事务。
可以明确的配置在抛出那些异常时回滚事务, 包括checked异常。也可以明确定义那些异常抛出时不回滚事务。
@ -1243,16 +1301,6 @@ public void addGoods(){
### 事物注意事项
- 要根据实际的需求来决定是否要使用事物,最好是在编码之前就考虑好,不然到以后就难以维护
- 如果使用了事物,请务必进行事物测试,因为很多情况下以为事物是生效的,但是实际上可能未生效
- 事物@Transactional的使用要放再类的**公共(public)方法**中,需要注意的是在 protected、private 方法上使用 @Transactional 注解,它也不会报错(IDEA会有提示),但事务无效
- 事物@Transactional是不会对该方法里面的子方法生效! 也就是你在公共方法A声明的事物@Transactional, 但是在A方法中有个子方法B和C, 其中方法B进行了数据操作, 但是该异常被B自己处理了, 这样的话事物是不会生效的! 反之B方法声明的事物@Transactional, 但是公共方法A却未声明事物的话, 也是不会生效的! 如果想事物生效, 需要将子方法的事务控制交给调用的方法, 在子方法中使用`rollbackFor`注解指定需要回滚的异常或者将异常抛出交给调用的方法处理。一句话就是在使用事物的异常由调用者进行处理
- 事物@Transactional由spring控制的时候, 它会在抛出异常的时候进行回滚。如果自己使用catch捕获了处理了, 是不生效的, 如果想生效可以进行手动回滚或者在catch里面将异常抛出, 比如`throw new RuntimeException();`
### 失效场景
### 失效场景
- ** @Transactional 应用在非 public 修饰的方法上**
- ** @Transactional 应用在非 public 修饰的方法上**
@ -1264,45 +1312,6 @@ public void addGoods(){
### select for update
`for update` 是一种`行级锁`,又叫`排它锁`。一旦用户对某个行施加了行级加锁,则该用户可以查询也可以更新被加锁的数据行,其它用户只能查询但不能更新被加锁的数据行。
- ** 修改sql**:在 `select` 的 `sql` 尾部添加 `for update` 。如:`select * from job_info where id = 1 for update;`
- ** 启用事务**:为 `service` 添加注解 `@Transactional`
**只有当出现如下之一的条件,才会释放共享更新锁:**
1. 执行提交( COMMIT) 语句
2. 退出数据库( LOG OFF)
3. 程序停止运行
假设有个表单products , 里面有id 跟name 二个栏位, id 是主键。
```mysql
-- 例1: 明确指定主键, 并且有此数据, row lock
SELECT * FROM products WHERE id='3' FOR UPDATE;
-- 例2: 明确指定主键, 若查无此数据, 无lock
SELECT * FROM products WHERE id='-1' FOR UPDATE;
-- 例2: 无主键, table lock
SELECT * FROM products WHERE name='Mouse' FOR UPDATE;
-- 例3: 主键不明确, table lock
SELECT * FROM products WHERE id< >'3' FOR UPDATE;
-- 例4: 主键不明确, table lock
SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;
```
**注意**
- FOR UPDATE 仅适用于InnoDB, 且必须在事务区块(start sta/COMMIT)中才能生效
- 要测试锁定的状况, 可以利用MySQL 的Command Mode ,开二个视窗来做测试
# 索引机制
# 索引机制
索引是为了加速对表中数据行的检索而创建的一种分散存储的(不连续的)数据结构,硬盘级的。
索引是为了加速对表中数据行的检索而创建的一种分散存储的(不连续的)数据结构,硬盘级的。
@ -1317,8 +1326,6 @@ SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;
- ** 可以提高数据检索的效率( 将随机I/O变成顺序I/O) , 降低数据库的I/O成本**
- ** 可以提高数据检索的效率( 将随机I/O变成顺序I/O) , 降低数据库的I/O成本**
- ** 通过索引列对数据进行排序, 降低数据排序的成本, 降低了CPU的消耗**
- ** 通过索引列对数据进行排序, 降低数据排序的成本, 降低了CPU的消耗**
**劣势**
**劣势**
- ** 索引会占据磁盘空间**
- ** 索引会占据磁盘空间**
@ -1328,11 +1335,6 @@ SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;
## 数据结构
## 数据结构
索引的数据结构:
- Hash表
-
### Hash索引
### Hash索引
![Hash索引 ](images/Database/Hash索引.png )
![Hash索引 ](images/Database/Hash索引.png )