目录
1. MySQL中的并发问题
在多用户数据库系统中,并发操作是不可避免的。然而,并发操作可能会导致一些问题,主要包括:
-
脏读:一个事务读取了另一个未提交事务修改过的数据:
A事务读取B事务修改后的数据返回,然而B事务发生了回滚
- 不可重复读:在同一事务内,多次读取同一数据返回的结果有所不同:
A事务多次读取同一个数据,B事务修改了该数据,A事务读到的数据不一样
- 幻读:在同一事务内,多次查询返回的结果集不一致:
A事务多次读取某些数据(比如查询表的数据总数),B事务插入或者删除了数据,A读取的数据数量发生了变更,像幻觉一样
注意区分幻读和不可重复读
为了解决这些问题,数据库系统引入了事务隔离级别的概念。
2. 数据库的隔离级别
SQL标准定义了四种隔离级别,从低到高分别是:
- 读未提交(Read Uncommitted)
- 读已提交(Read Committed)
- 可重复读(Repeatable Read)
- 串行化(Serializable)
让我们用一个图表来展示这些隔离级别如何解决并发问题:
MySQL的InnoDB存储引擎默认使用**可重复读(Repeatable Read)**隔离级别。他引入了MVCC来解决这个不可重复读的问题(读已提交也有MVCC,两者之间只是创建快照的时机不同,后面再提)
3. MVCC(多版本并发控制)机制
MVCC是MySQL InnoDB存储引擎用于提高并发性能的一种机制。它的核心思想是:
保存数据在某个时间点的快照,形成一个版本链,使得事务可以看到在事务开始时数据的一致性视图。
3.1 MVCC的实现原理
InnoDB的MVCC主要通过以下几个部分来实现:
-
隐藏列:每行数据都有两个隐藏列
DB_TRX_ID
:最后一次插入或更新该行的事务IDDB_ROLL_PTR
:回滚指针,指向该行的上一个版本(存储在undo log中)
-
Undo Log:保存了数据被修改前的旧值,用于回滚和MVCC的数据读取。可以认为undo log存储数据的历史版本,通过指针会形成一个版本链。
-
Read View:事务进行快照读操作时产生的读视图,用于判断当前事务能够看到哪个版本的数据。
让我们用一个图表来展示MVCC的基本结构:
3.2 Read View详解
Read View是MVCC实现的核心,它包含以下重要信息:
m_ids
:活跃事务列表,在生成Read View时活跃的读写事务ID列表,就是记录当前还未提交的事务列表m_low_limit_id/(m_min_limit_id)
:活跃事务的最小事务id,就是活跃列表的最小值m_up_limit_id/(m_max_limit_id)
:下一个事务应该被分配的事务idm_creator_trx_id
:生成该Read View的事务ID
注意:事务id是递增分配的
当一个事务要读取一行数据时,InnoDB会将该行的DB_TRX_ID
(前面提到的,表示这行数据最近一次发生修改的事务id)与Read View中的信息进行比较,以决定是否可以看到该版本的数据:
1. DB_TRX_ID == m_creator_trx_id
:当前读取的事务就是创建快照读的事务,说明就是该事务最近修改了这条数据,因此是可读的;
2. DB_TRX_ID < m_low_limit_id
:当这条数据最新更改的事务id小于最小活跃事务id,就说明该事务id是已经提交的事务id,该条数据就可读
3. DB_TRX_ID >= m_up_limit_id
:这条数据的最新记录是在快照建立之后执行的,显然属于不可读,根据回滚指针找到合适的历史版本数据进行返回
4. m_low_limit_id < DB_TRX_ID < m_up_limit_id
:这条数据最新更改的事务id在最小活跃事务id和最大事务id(下一次分配的事务id)之间,那就需要去扫描活跃事务列表,判断是否有这个id存在:
- 有就说明该数据还没有被提交是不可读的,那么他就会根据DB_ROLL_PTR去找到该数据的历史版本,同样会进行判断,直至找到符合可读条件的数据返回
- 如果列表没有就表明该事务已经被提交了,就是可读的
3.3 当前读与快照读
在InnoDB中,我们有两种读取数据的方式:
-
当前读(Current Read):
- 读取的是记录的最新版本
- 会对读取的记录进行加锁
- 常见的当前读操作:
SELECT ... FOR UPDATE
,SELECT ... LOCK IN SHARE MODE
,UPDATE, DELETE
等
-
快照读(Snapshot Read):
- 读取的是记录的快照版本
- 不会对读取的记录进行加锁
- 常见的快照读操作:不带锁的SELECT
让我们用一个图表来说明当前读和快照读的区别:
4. MVCC在不同隔离级别下的工作方式
MVCC主要工作在读已提交和可重复读这两个隔离级别下:
-
读已提交:
- 每次读取数据前都会生成一个新的Read View
- 能看到其他事务已经提交的修改
-
可重复读:
- 在事务开始时生成一个Read View,并在整个事务过程中使用这个Read View
- 保证在同一事务中多次读取数据时结果一致
让我们用一个例子来说明这两种隔离级别下MVCC的工作方式:
5. MVCC解决幻读问题
虽然MVCC可以有效解决不可重复读问题,但对于幻读,它只能提供部分解决方案。在可重复读隔离级别下,MVCC可以防止"快照读"出现幻读,但无法完全阻止"当前读"的幻读问题。
为了进一步解决幻读问题,InnoDB引入了Next-Key Lock(临键锁)机制,它是记录锁(Record Lock)和间隙锁(Gap Lock)的组合。
Next-Key Lock不仅锁定查询涉及的索引记录,还锁定这些记录之间的间隙,有效防止了其他事务在查询范围内插入新记录,从而解决了幻读问题。
6. MVCC的优缺点
优点:
- 提高并发性:允许读写并发,读操作不会阻塞写操作,写操作也不会阻塞读操作。
- 解决一致性问题:提供了一致性的数据视图,避免了脏读和不可重复读问题。
- 降低锁竞争:通过版本链和快照读,减少了锁的使用范围,从而提高了系统的整体性能。
- 支持时间点查询:可以查询某个时间点的历史数据,对于某些应用场景非常有用。
缺点:
- 存储开销:需要额外的存储空间来保存多个版本的数据。
- 复杂性:实现和维护MVCC机制相对复杂,增加了数据库系统的复杂度。
- 清理问题:需要定期清理无用的历史版本,否则可能导致存储空间的浪费。
- 事务大小限制:长事务可能会导致版本链过长,影响性能。
7. MVCC在实际应用中的注意事项
- 合理设置隔离级别:根据应用需求选择适当的隔离级别,避免过度使用高隔离级别带来的性能开销。
- 控制事务大小:尽量使用短事务,避免长事务导致的版本链过长问题。
- 定期维护:对于频繁更新的表,需要定期进行维护操作,如优化表结构、清理无用的历史版本等。
- 索引优化:合理设计索引可以提高MVCC的效率,减少不必要的版本链遍历。
- 监控与调优:定期监控系统性能,关注MVCC相关的指标,如undo log的大小、长事务的数量等,及时进行调优。