InnoDB的并发控制

概述

为什么要进行并发控制

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

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

  • 锁(Locking)

    • 普通锁:操作数据前,锁住,实施互斥,不允许其他的并发任务操作;操作完成后,释放锁,让其他任务执行;

      • 并发太低,读操作也是串行的

    • 共享锁与排他锁:读读可以并行,但写读,写写不可以并行,即写事务没有提交,读相关数据的select也会被阻塞

  • 数据多版本(Multi Versioning)

    • 进一步提高并发即使写任务没有完成,其他读任务也可能并发。

数据多版本

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

(1)写任务发生时,将数据克隆一份,并以版本号区分;

(2)写任务操作新克隆的数据,直至提交;

(3)并发读任务可以继续读取旧版本的数据,不至于阻塞;

如上图:

  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;

这里的显式加锁是指:

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

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

TODO

参考

InnoDB存储引擎MVCC实现原理

InnoDB并发如此高,原因竟然在这?

数据库事务特征、数据库隔离级别,各级别数据库加锁情况(含实操)--read committed && MVCC

【mysql】关于innodb中MVCC的一些理解

Last updated