# InnoDB的并发控制

## 概述

**为什么要进行并发控制**

并发的任务对同一个临界资源进行操作，如果不采取措施，可能导致数据不一致，故必须进行**并发控制**（Concurrency Control）。

技术上，通过并发控制保证数据一致性的常见手段有：

* 锁（Locking）
* * 普通锁：操作数据前，锁住，实施互斥，不允许其他的并发任务操作；操作完成后，释放锁，让其他任务执行；
  * * 并发太低，读操作也是串行的
  * 共享锁与排他锁：读读可以并行，但写读，写写不可以并行，即*写事务没有提交，读相关数据的select也会被阻塞*
* 数据多版本（Multi Versioning）
* * 进一步提高并&#x53D1;**，**&#x5373;使写任务没有完成，其他读任务也可能并发。

**数据多版本**

数据多版本是一种能够进一步提高并发的方法，它的思路如下：（类似与Java中COPY-ON-WRITE容器的做法）：

（1）写任务发生时，将数据克隆一份，并以版本号区分；

（2）写任务操作新克隆的数据，直至提交；

（3）并发读任务可以继续读取旧版本的数据，不至于阻塞；

![](http://img.chuansong.me/mmbiz_png/YrezxckhYOxqYZaEWwXRwibTg8vNtNIPG7Hfiat5wx6D353IIxWmOKJawOcJ84QFu2WYicElTUeTsy9Am0MQZpcyg/640?wx_fmt=png)

如上图：

1. 最开始数据的版本是V0；
2. T1时刻发起了一个写任务，此时将数据clone一份，进行修改，版本变为V1，但任务还未完成；
3. T2时刻并发了一个读任务，依然可以读V0版本的数据；
4. T3时刻又并发了一个读任务，依然不会阻塞；

可以看到，数据多版本，通过“读取旧版本数据”能够极大提高任务的并发度。

提高并发的演进思路，就在如此：

* **普通锁**，本质是串行执行
* **读写锁**，可以实现读读并发
* **数据多版本**，可以实现读写并发

## InnoDB中多版本的实现

InnoDB中多版本控制的实现是依赖如下三个特性实现的：redo日志，undo日志，回滚段（rollback segment）**。**

**redo日志：负责事务提交操作**

数据库事务提交后，必须将更新后的数据刷到磁盘上，以保证ACID特性。磁盘随机写性能较低，如果每次都刷盘，会极大影响数据库的吞吐量。InnoDB的优化方式是：将修改行为先写到redo日志里（此时变成了顺序写，**即将随机写优化为顺序写**），再定期将数据刷到磁盘上，这样能极大提高性能。

假如某一时刻，数据库崩溃，还没来得及刷盘的数据，在数据库重启后，会重做redo日志里的内容，以保证已提交事务对数据产生的影响都刷到磁盘上。

即redo日志用于保障已提交事务的ACID特性。

**undo日志：负责事务回滚操作**

数据库事务未提交时，会将事务修改数据的镜像（即修改前的旧版本）存放到undo日志里，当事务回滚时，或者数据库崩溃时，可以利用undo日志，即旧版本数据，撤销未提交事务对数据库产生的影响。

*对于insert操作，undo日志记录新数据的PK(ROW\_ID)，回滚时直接删除；*

*对于delete/update操作，undo日志记录旧数据row，回滚时直接恢复；*

即undo日志用于保障，未提交事务不会对数据库的ACID特性产生影响。

**回滚段**

回滚段：存储undo日志的地方。

了解了上述三个概念，我们来看InnoDB中MVCC的具体实现。

首先，InnoDB存储引擎会对数据库每行数据的后面添加三个字段：

* 事务ID(`DB_TRX_ID`)
* * 标记最新更新这条行记录的transaction id，每处理一个事务，其值自动+1；
  * 此外，删除在内部被视为一个更新，其中行中的特殊位被设置为将其标记为已删除
* 回滚指针(`DB_ROLL_PTR`)
* * 指向当前记录项的回滚段的undo日志， 找之前版本的数据就是通过这个指针。
* `DB_ROW_ID`
* * 当由innodb自动产生聚集索引时，聚集索引包括这个DB\_ROW\_ID的值，否则聚集索引中不包括这个值，这个用于索引当中。如果我们的表中有主键或合适的唯一索引，也就是无法生成聚簇索引的时候, InnoDB会帮我们自动生成聚集索引, 但聚簇索引会使用DB\_ROW\_ID的值来作为主键; 如果我们有自己的主键或者合适的唯一索引, 那么聚簇索引中也就不会包含 DB\_ROW\_ID 了 。

**修改操作**

在InnoDB中，事务以排他锁的形式修改原始数据，把修改前的数据存放于undo日志，并通过回滚指针与主数据关联，修改成功时什么都不做，失败则恢复undo日志中的数据（rollback）。

**快照读**（Snapshot Read）

回滚段里的数据，其实是历史数据的快照（snapshot），这些数据是不会被修改，select可以肆无忌惮的并发读取他们，这就是快照度。快照读这种一致性不加锁的读，就是InnoDB并发如此之高的核心原因之一。

> 这里的一致性是指，事务读取到的数据：
>
> 要么是事务开始前就已经存在的数据（当然，是其他已提交事务产生的），
>
> 要么是事务自身插入或者修改的数据。

除非显式加锁，普通的select语句都是快照读，例如：select \* from t where id>2;

这里的显式加锁是指：

```sql
select ... ...  lock in share mode;
select ... ... for update;
```

**即快照读不加锁，可以并发读，所有的普通select都是快照读。**

TODO

## 参考

[InnoDB存储引擎MVCC实现原理](https://liuzhengyang.github.io/2017/04/18/innodb-mvcc/)

[InnoDB并发如此高，原因竟然在这？](http://chuansong.me/n/2487104646019)

[数据库事务特征、数据库隔离级别，各级别数据库加锁情况(含实操)--read committed && MVCC](https://www.jianshu.com/p/fd51cb8dc03b)

[【mysql】关于innodb中MVCC的一些理解](https://www.cnblogs.com/chenpingzhao/p/5065316.html)
