|
|
|
@ -600,6 +600,99 @@ WHERE A.EMP_SUPV_ID = B.EMP_ID;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 数据库锁
|
|
|
|
|
|
|
|
|
|
从粒度上来说就是**表锁、页锁、行锁**。表锁有意向共享锁、意向排他锁、自增锁等。行锁是在引擎层由各个引擎自己实现的。但并不是所有的引擎都支持行锁,比如 `MyISAM`引擎就`不支持行锁`。
|
|
|
|
|
|
|
|
|
|
## 行锁(Record Locks)
|
|
|
|
|
|
|
|
|
|
在 `InnoDB` 事务中,行锁通过给索引上的索引项加锁来实现。即只有通过索引条件检索数据,`InnoDB` 才使用行级锁,否则将使用表锁。行级锁定同样分为两种类型:`共享锁` 和`排他锁`,以及加锁前需要先获得的 `意向共享锁` 和 `意向排他锁`。
|
|
|
|
|
|
|
|
|
|
行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
|
|
|
|
|
|
|
|
|
|
### 共享锁(Shared Locks)
|
|
|
|
|
|
|
|
|
|
共享锁又称为 `S锁` 或 `读锁`。若事务T对数据对象A加上 `S锁`,则事务T `只能读A`;其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
|
|
|
|
|
|
|
|
|
|
- `select ... lock in share mode`: 会加`共享锁`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 排它锁(Exclusive Locks)
|
|
|
|
|
|
|
|
|
|
排它锁又称为 `X锁` 或 `写锁`。若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。在更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁。
|
|
|
|
|
|
|
|
|
|
注意:排他锁会阻止其它事务再对其**锁定的数据**加读或写的锁,但是不加锁的就没办法控制了。
|
|
|
|
|
|
|
|
|
|
- `insert`、`update`、`delete`、`select ... for update`:会加`排它锁`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 行锁实现算法
|
|
|
|
|
|
|
|
|
|
### Record Lock(行锁)
|
|
|
|
|
|
|
|
|
|
单个行记录上的锁,总是会去锁住索引记录。
|
|
|
|
|
|
|
|
|
|
行锁,顾名思义,是加在`索引行`(是索引行,不是数据行)上的锁。比如:
|
|
|
|
|
|
|
|
|
|
`select * from user where id=1 and id=10 for update`,就会在`id=1`和`id=10`的索引行上加Record Lock。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Gap Lock(间隙锁)
|
|
|
|
|
|
|
|
|
|
间隙锁,它会锁住两个索引之间的区域。比如:
|
|
|
|
|
|
|
|
|
|
`select * from user where id>1 and id<10 for update`,就会在id为(1,10)的索引区间上加Gap Lock。
|
|
|
|
|
|
|
|
|
|
想一下幻读的原因,其实就是行锁只能锁住行,但新插入记录这个动作,要更新的是记录之间的“`间隙`”。所以加入间隙锁来解决幻读。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Next-Key Lock(间隙锁)
|
|
|
|
|
|
|
|
|
|
也叫间隙锁,它是 Record Lock + Gap Lock 形成的一个闭区间锁。比如:
|
|
|
|
|
|
|
|
|
|
`select * from user where id>=1 and id<=10 for update`,就会在id为[1,10]的索引闭区间上加 Next-Key Lock。
|
|
|
|
|
|
|
|
|
|
这样组合起来就有:行级共享锁,表级共享锁;行级排它锁,表级排它锁。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## MVCC
|
|
|
|
|
|
|
|
|
|
**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。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 事务
|
|
|
|
|
|
|
|
|
|
**什么叫事务?**
|
|
|
|
@ -608,27 +701,88 @@ WHERE A.EMP_SUPV_ID = B.EMP_ID;
|
|
|
|
|
|
|
|
|
|
## 事务特性(ACID)
|
|
|
|
|
|
|
|
|
|
- **原子性(Atomicity)**:事务是最小的执行单位,不允许分割。原子性确保动作要么全部完成,要么完全不起作用。
|
|
|
|
|
### 原子性(Atomicity)
|
|
|
|
|
|
|
|
|
|
**事务是最小的执行单位,不允许分割。原子性确保动作要么全部完成,要么完全不起作用。**
|
|
|
|
|
|
|
|
|
|
原子性是依赖于回滚日志(`undo log`)实现的。当事务对数据库进行修改时,`InnoDB`会生成对应的 `undo log`;如果事务执行失败或调用了 `rollback`,导致`事务需要回滚`,便可以利用 `undo log` 中的信息将数据回滚到修改之前的样子。`undo log`属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB 会根据 `undo log` 的内容做与之前相反的工作:
|
|
|
|
|
|
|
|
|
|
- 对于每个`insert`,回滚时会执行`delete`
|
|
|
|
|
- 对于每个 `delete`,回滚时会执行`insert`
|
|
|
|
|
- 对于每个 `update`,回滚时会执行一个相反的 `update`,把数据改回去
|
|
|
|
|
|
|
|
|
|
以`update`操作为例:当事务执行`update`时,其生成的`undo log`中会包含被修改行的主键、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到`update`之前的状态。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 一致性(Consistency)
|
|
|
|
|
|
|
|
|
|
**事务开始前和结束后,数据库的完整性约束没有被破坏**。比如A向B转账,不可能A扣了钱,B却没收到。
|
|
|
|
|
|
|
|
|
|
一致性是事务追求的最终目标,原子性、持久性和隔离性其实都是为了保证数据库状态的一致性。当然,都是数据库层面的保障,一致性的实现也需要应用层面进行保障。也就是你的业务,比如购买操作只扣除用户的余额,不减库存,肯定无法保证状态的一致。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 隔离性(Isolation)
|
|
|
|
|
|
|
|
|
|
**并发访问数据库时,一个事务不被其他事务所干扰。**
|
|
|
|
|
|
|
|
|
|
隔离性是通过 `锁` 和 `MVCC`(多版本并发控制) 实现。InnoDB采用的MVCC实现方式是:在需要时,通过undo日志构造出历史版本。
|
|
|
|
|
|
|
|
|
|
- **一致性(Consistency)**:执行事务前后,数据保持一致。
|
|
|
|
|
|
|
|
|
|
一个事务执行前后,应该使数据库从一个一致性状态转换为另一个一致性状态。比方说假设A、B两个人,共有5000元。那么无论A给B转多少钱,转多少次,总数仍然是5000没有改变。
|
|
|
|
|
|
|
|
|
|
- **隔离性(Isolation)**:并发访问数据库时,一个事务不被其他事务所干扰。
|
|
|
|
|
### 持久性(Durability)
|
|
|
|
|
|
|
|
|
|
- **持久性(Durability)**:一个事务被提交之后。对数据库中数据的改变是持久的,即使数据库发生故障。
|
|
|
|
|
**一个事务被提交之后。对数据库中数据的改变是持久的,即使数据库发生故障。**
|
|
|
|
|
|
|
|
|
|
`InnnoDB`有很多 log,持久性靠的是`redo log`。持久性肯定和写有关,`MySQL` 里经常说到的 `WAL`技术,`WAL`的全称是`Write-Ahead Logging`,它的关键点就是先写日志,再写磁盘。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**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`
|
|
|
|
|
- 刷脏页以Page为单位,一个Page上的修改整页都要写;而redo log 只包含真正需要写入的,无效 IO 减少
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 隔离级别
|
|
|
|
|
|
|
|
|
|
数据库事务隔离级别有4种,由低到高为:**Read uncommitted** 、**Read committed** 、**Repeatable read** 、**Serializable** 。而且,在事务的并发操作中可能会出现 **脏读、不可重复读、幻读** 问题。不做隔离操作则会出现:
|
|
|
|
|
数据库事务隔离级别有4种,由低到高为:**Read uncommitted** 、**Read committed** 、**Repeatable read** 、**Serializable** 。
|
|
|
|
|
|
|
|
|
|
- **脏读**:事务A中读到了事务B中未提交的更新数据内容
|
|
|
|
|
- **不可重复读**:读到其它事务已经提交后的**更新**数据,即一个事务范围内两个相同的查询却返回了不同数据
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**事务并发问题**
|
|
|
|
|
|
|
|
|
|
在事务的并发操作中,不做隔离操作则可能会出现 **脏读、不可重复读、幻读** 问题:
|
|
|
|
|
|
|
|
|
|
- **脏读**:**事务A中读到了事务B中未提交的更新数据内容**。然后B回滚操作,那么A读取到的数据是脏数据
|
|
|
|
|
- **不可重复读**:**事务A读到事务B已经提交后的数据**。即事务A多次读取同一数据时,返回结果不一致
|
|
|
|
|
- **幻读**:事物A执行select后,事物B**增或删**了一条数据,事务A再执行同一条SQL后发现多或少了一条数据
|
|
|
|
|
- **第一类丢失更新**:A事务撤销时,把已经提交的B事务的更新数据覆盖了
|
|
|
|
|
- **第二类丢失更新**:A事务提交时,把已经提交的B事务的更新数据覆盖了
|
|
|
|
|
|
|
|
|
|
**小结**:不可重复读的和幻读很容易混淆,**不可重复读**侧重于**修改**,**幻读**侧重于**新增或删除**。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -664,8 +818,8 @@ WHERE A.EMP_SUPV_ID = B.EMP_ID;
|
|
|
|
|
|
|
|
|
|
**读未提交的数据库锁情况**
|
|
|
|
|
|
|
|
|
|
- 事务中读取数据:**未加锁**
|
|
|
|
|
- 事务中更新数据:**只对数据增加行级共享锁**
|
|
|
|
|
- 读取数据:**未加锁**
|
|
|
|
|
- 写入数据:**只对数据增加行级共享锁**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -677,8 +831,8 @@ WHERE A.EMP_SUPV_ID = B.EMP_ID;
|
|
|
|
|
|
|
|
|
|
**读已提交的数据库锁情况**
|
|
|
|
|
|
|
|
|
|
- 事务中读取数据:**加行级共享锁(读到时才加锁),读完后立即释放**
|
|
|
|
|
- 事务中更新数据:**在更新时的瞬间对其加行级排它锁,直到事务结束才释放**
|
|
|
|
|
- 读取数据:**加行级共享锁(读到时才加锁),读完后立即释放**
|
|
|
|
|
- 写入数据:**在更新时的瞬间对其加行级排它锁,直到事务结束才释放**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -703,8 +857,8 @@ WHERE A.EMP_SUPV_ID = B.EMP_ID;
|
|
|
|
|
|
|
|
|
|
**可重复读的数据库锁情况**
|
|
|
|
|
|
|
|
|
|
- 事务中读取数据:**开始读取的瞬间对其增加行级共享锁,直到事务结束才释放**
|
|
|
|
|
- 事务中更新数据:**开始更新的瞬间对其增加行级排他锁,直到事务结束才释放**
|
|
|
|
|
- 读取数据:**开始读取的瞬间对其增加行级共享锁,直到事务结束才释放**
|
|
|
|
|
- 写入数据:**开始更新的瞬间对其增加行级排他锁,直到事务结束才释放**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -716,8 +870,8 @@ WHERE A.EMP_SUPV_ID = B.EMP_ID;
|
|
|
|
|
|
|
|
|
|
**可序列化的数据库锁情况**
|
|
|
|
|
|
|
|
|
|
- 事务中读取数据:**先对其加表级共享锁 ,直到事务结束才释放**
|
|
|
|
|
- 事务中更新数据:**先对其加表级排他锁 ,直到事务结束才释放**
|
|
|
|
|
- 读取数据:**先对其加表级共享锁 ,直到事务结束才释放**
|
|
|
|
|
- 写入数据:**先对其加表级排他锁 ,直到事务结束才释放**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|