Mysql如何实现隔离级别 - 可重复读和读提交 源码分析

本文详细探讨MySQL的四种隔离级别:读未提交、读已提交、可重复读和串行化,以及它们如何通过多版本并发控制(MVCC)机制实现。通过分析源代码,解释了ReadView和版本链在不同隔离级别下的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Abstract

本文会(1) 演示Mysql的两种隔离级别.  (2) 跟着mysql的源代码来看看它是怎么实现这两种隔离级别的.

 

Mysql的隔离级别

当有多个事务并发执行时, 我们需要考虑他们之间的相互影响. 比如 事务A写了数据d, 事务B是否应该看见呢.

Mysql的事务级别包括: read uncommitted -> read commited -> repeatable read -> serializable.

Serializable是最严格的一种隔离级别. 类似的也是性能最差的.

read committed 指当前事务A可以看到已经提交的事务B的所有修改. 不管B是否在事务A开始时提交, 还是A开始后, B提交的. 

repeateable read 保证事务A只能看到事务A开始时, 其他事务已经提交的修改和本事务A内的修改.

read uncommitted 可以读取到未提交的数据.

 

不同隔离级别可能遇到的问题

 

读未提交

 读提交

 可重复读

 串行化

脏读(读到没提交的数据)

不可重复读(对读取的某一行发现它的数据变了 比如select * from t where id=1)

幻读(谓词带有where条件的发现读取的多个记录不一样带来的读取不一致)

 

例子 : 某事务中, 同一个查询在不同的时候产生了不同的sets of rows.  
    举例来说t表id分别有90 和 102的2行.  id列有索引. 比如select * from t where id > 100 for update
             第一次返回了1行, 第二次返回了2行.

无 Mysql中没有. PostgreSQL可能有

这里可以看到幻读和不可重复读的区别.

 

具体的例子

mysql -uroot -p // 连接
start transaction;
set global transaction isolation level xxxxlevel; // 设置全局
set session transaction isolation level xxxlevel;  // 设置事务隔离级别
// do some ops
commit; // 提交事务
SELECT @@GLOBAL.transaction_isolation, @@SESSION.transaction_isolation; // 查看事务级别

具体的来说可以打开两个mysql窗口测试. 

 

MySQL如何实现这些隔离级别?

(1) 存储多个版本 --- 多版本 版本链

通用的思想:  因为可重复读可能读到的是事务开始时的数据. 那么我们知道 数据一定在数据库中存在了多个版本.

Mysql通过每行的trx_id和roll_pointer实现. Mysql记录的每一行都会有如下3列:

在内部呈现出如下的一个版本链, 那么当事务需要看到某个版本的时候就可以看到了.(至少有地方存了这些可能还没提交的数据)

(2) 读正确的版本 --- ReadView

当我们有了这个版本链后, 我们需要确保事务看到的版本数据满足对应的隔离级别.  

比如有A事务(ID=81), B事务(ID=83), 都是读提交. 然后上图中 80, 81, 82已经提交了, 但是200还没提交.

那么如果A是可重复读, 那么A只能看到82版本的数据 而看不到200版本的数据.

如果A是读提交, 那么A可以看到82版本数据 (最新最近提交的).

而这就是通过ReadView实现的.  ReadView对象中存储了m_ids[]  存储当前还未提交的事务id.  后面也有介绍.

(3) 防止错误的版本写入  --- 锁

ReadCommitted 级别 数据只会锁住读取的数据. 但是在Repeatable Read中为了防止幻读出现, mysql还会锁住其中的gap.

 

 

源码解读

源码用的我fork的代码, (主要是添加了一些方便debug和查看的日志). 编译方法在这里

按照官网的说法, 读提交会每次重新创建一个快照.

Each consistent read, even within the same transaction, sets and reads its own fresh snapshot. For information about consistent reads, see Section 15.7.2.3, “Consistent Nonlocking Reads”.

但是可重复读只会在事务开始时创建:

This is the default isolation level for InnoDBConsistent reads within the same transaction read the snapshot established by the first read. 

快照在代码中就是ReadView, 它包含几个比较重要的值:

 /** The read should not see any transaction with trx id >= this
  value. In other words, this is the "high water mark". */
  trx_id_t m_low_limit_id;

  /** The read should see all trx ids which are strictly
  smaller (<) than this value.  In other words, this is the
  low water mark". */
  trx_id_t m_up_limit_id;

  /** trx id of creating transaction, set to TRX_ID_MAX for free
  views. */
  trx_id_t m_creator_trx_id;

这些值分别决定了当前ReadView/快照中的改动对哪些事务可见. 哪些事务不可见. 

后面我们会根据mysql的代码来看看是不是这样的.

一条SQL执行顺序大概是这样: 解析语法 -> 执行 -> 返回.

1. 开始执行:

2. 获取锁, 检查隔离级别.  此时trx (事务) 绑定的 read_view还是NULL

3. 然后for循环读取表

可以看到MVCC相关的代码在row_search_mvcc方法中.

4. 事务还没绑定ReadView. 申请一个新的

ReadView *trx_assign_read_view(trx_t *trx) /*!< in/out: active transaction */
{
  ut_ad(trx->state == TRX_STATE_ACTIVE);

  if (srv_read_only_mode) {
    ut_ad(trx->read_view == NULL);
    return (NULL);

  } else if (!MVCC::is_view_active(trx->read_view)) {
    trx_sys->mvcc->view_open(trx->read_view, trx);
  }

  return (trx->read_view);
}

新生成的ReadView的前面提到的都设置为当前最大的事务ID:

m_low_limit_no = m_low_limit_id = m_up_limit_id = trx_sys->max_trx_id;

5. 然后就是根据ReadView需要读取数据了.

这里 lock_clust_rec_cons_read_sees 会根据当前行的修改的最新事务id(row_get_rec_trx_id)来比较当前事务绑定ReadView是否可以看到最新的修改. 如果不能看到则根据undo日志恢复到当时的版本返回.

/** Checks that a record is seen in a consistent read.
 @return true if sees, or false if an earlier version of the record
 should be retrieved */
bool lock_clust_rec_cons_read_sees(
    const rec_t *rec,     /*!< in: user record which should be read or
                          passed over by a read cursor */
    dict_index_t *index,  /*!< in: clustered index */
    const ulint *offsets, /*!< in: rec_get_offsets(rec, index) */
    ReadView *view)       /*!< in: consistent read view */
{
  // ......

  /* NOTE that we call this function while holding the search
  system latch. */

  trx_id_t trx_id = row_get_rec_trx_id(rec, index, offsets);

  return (view->changes_visible(trx_id, index->table->name));
}

 

判断某个ReadView对某个事务的改动是否可见:

读提交不涉及. 因为后面可以看到读提交的ReadView每次都会重新创建.

6. 读取完成后释放锁.  这里可以看到<=读提交的会释放视图. 那么事务再有请求的时候就得重新assign view了. 可以看到他们具体的int值. 可重复读隔离级别视图不会close. 那么我们知道同一个事务,  读提交会在最后关闭read view. 但是可重复读不会.

7. 经过这样我们就知道了mysql这2个并发控制的隔离级别到底是怎么实现的了.

 

总结

1. 为了在性能和一致性上取得平衡或者有倾向性的选择, 定义了各种隔离级别.

2. 为了实现读提交和可重复读, Mysql通过多版本链 + ReadView来实现这2种隔离级别. MVCC的实现方式相比直接用锁优势很大, 读不需要获取锁或者因为写操作而被阻塞.

3.可重复读 -- 事务开始时创建ReadView.  读提交 -- 事务每个语句都创建新的ReadView

4.Mysql的可重复读并不会发生幻读现象. (通过索引记录锁 + gap锁实现的一种next record锁实现)

 

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值