MySQL事务与并发之:`事务`的`隔离级别`:`MySQL`默认`隔离级别`的底层实现。

MySQL事务与并发:默认隔离级别及其底层实现

大家好!今天我们要深入探讨MySQL事务并发控制中的一个核心概念:隔离级别,特别是MySQL默认隔离级别的底层实现。理解这些机制对于编写健壮、可靠的数据库应用至关重要。

事务的隔离级别回顾

首先,快速回顾一下事务隔离级别的概念。事务隔离级别定义了多个并发事务之间的隔离程度,它决定了一个事务能够看到其他事务已修改但未提交的数据的程度。SQL标准定义了四个隔离级别:

  • 读未提交(Read Uncommitted): 最低的隔离级别。一个事务可以读取到其他事务未提交的数据,可能导致脏读。

  • 读已提交(Read Committed): 一个事务只能读取到其他事务已经提交的数据,可以避免脏读,但可能出现不可重复读。

  • 可重复读(Repeatable Read): 保证在同一个事务中多次读取同一数据时,结果一致。可以避免脏读和不可重复读,但可能出现幻读。

  • 串行化(Serializable): 最高的隔离级别。强制事务串行执行,可以避免所有并发问题,包括脏读、不可重复读和幻读,但并发性能最低。

MySQL的默认隔离级别:可重复读(Repeatable Read)

MySQL的默认隔离级别是可重复读(Repeatable Read)。这意味着,在同一个事务中,无论其他事务如何修改数据并提交,当前事务读取到的数据始终保持一致。这避免了脏读和不可重复读的问题。

但是,可重复读隔离级别并不能完全避免并发问题,它仍然可能出现幻读。幻读是指,在同一个事务中,两次执行相同的查询,但第二次查询的结果集包含第一次查询未出现的新行。

可重复读的底层实现:MVCC(多版本并发控制)

MySQL通过MVCC(Multi-Version Concurrency Control)机制来实现可重复读的隔离级别。MVCC的核心思想是,为每一行数据维护多个版本,每个版本对应一个事务。当一个事务需要读取数据时,它会读取符合其可见性的版本,而不是直接读取最新的版本。

具体来说,MySQL使用以下几个关键技术来实现MVCC:

  • 隐藏列: 每行数据都包含两个隐藏列:trx_idroll_ptr

    • trx_id:创建或最后一次修改该行的事务ID。
    • roll_ptr:指向Undo Log中该行的前一个版本的指针。
  • Undo Log: 用于存储行的历史版本。当事务修改一行数据时,MySQL会将修改前的旧版本数据写入Undo Log。Undo Log形成一个链表,通过roll_ptr连接不同的版本。

  • Read View: 每个事务在启动时都会创建一个Read View。Read View包含以下信息:

    • trx_ids:当前活跃事务(未提交)的ID列表。
    • low_limit_id:当前系统中最大的事务ID + 1。
    • up_limit_id:当前系统中最小的活跃事务ID。
  • 版本链: 一行数据的多个版本通过Undo Log的roll_ptr链接起来,形成一个版本链。

MVCC的工作流程:数据读取

当一个事务需要读取一行数据时,MySQL会根据以下规则选择合适的版本:

  1. 找到最新的版本: 首先找到该行数据的最新版本(当前版本)。

  2. 判断可见性: 根据Read View和最新版本的trx_id判断该版本对当前事务是否可见。

    • 如果trx_id小于up_limit_id,说明该版本是在当前事务启动之前创建的,对当前事务可见。
    • 如果trx_id大于或等于low_limit_id,说明该版本是在当前事务启动之后创建的,对当前事务不可见。
    • 如果trx_idup_limit_idlow_limit_id之间,则需要判断trx_id是否在trx_ids列表中。
      • 如果在列表中,说明该版本是当前活跃事务创建的,对当前事务不可见。
      • 如果不在列表中,说明该版本是在当前事务启动之前,由已提交的事务创建的,对当前事务可见。
  3. 遍历版本链: 如果最新版本不可见,则沿着roll_ptr遍历版本链,找到第一个对当前事务可见的版本。

  4. 返回可见版本: 返回找到的可见版本的数据。

举例说明:

假设有三个事务T1、T2和T3,事务ID分别为10、11和12。

  • 初始状态下,表users有一行数据,id = 1, name = 'Alice', trx_id = 8, roll_ptr = NULL

  • T1启动,创建Read View,trx_ids = [11, 12], low_limit_id = 13, up_limit_id = 11

  • T2将name修改为Bob并提交,id = 1, name = 'Bob', trx_id = 11, roll_ptr = 指向旧版本。旧版本的数据为id = 1, name = 'Alice', trx_id = 8, roll_ptr = NULL

  • T3将name修改为Charlie但未提交,id = 1, name = 'Charlie', trx_id = 12, roll_ptr = 指向'Bob'版本

现在T1读取id = 1的数据,过程如下:

  1. 找到最新版本,name = 'Charlie', trx_id = 12

  2. 判断可见性:trx_id (12)up_limit_id (11)low_limit_id (13)之间,且在trx_ids ([11, 12])中,因此不可见。

  3. 遍历版本链,找到name = 'Bob', trx_id = 11

  4. 判断可见性:trx_id (11)up_limit_id (11)low_limit_id (13)之间,且在trx_ids ([11, 12])中,因此不可见。

  5. 遍历版本链,找到name = 'Alice', trx_id = 8

  6. 判断可见性:trx_id (8)小于up_limit_id (11),因此可见。

  7. T1读取到的数据是name = 'Alice'

代码示例(伪代码,用于说明MVCC原理):

class Row:
    def __init__(self, data, trx_id, roll_ptr):
        self.data = data
        self.trx_id = trx_id
        self.roll_ptr = roll_ptr

class ReadView:
    def __init__(self, trx_ids, low_limit_id, up_limit_id):
        self.trx_ids = trx_ids
        self.low_limit_id = low_limit_id
        self.up_limit_id = up_limit_id

    def is_visible(self, trx_id):
        if trx_id < self.up_limit_id:
            return True
        elif trx_id >= self.low_limit_id:
            return False
        else:
            return trx_id not in self.trx_ids

def read_data(row, read_view):
    current_version = row
    while current_version:
        if read_view.is_visible(current_version.trx_id):
            return current_version.data
        current_version = current_version.roll_ptr
    return None

# 模拟数据
initial_row = Row({"id": 1, "name": "Alice"}, 8, None)
bob_row = Row({"id": 1, "name": "Bob"}, 11, initial_row)
charlie_row = Row({"id": 1, "name": "Charlie"}, 12, bob_row)

# 模拟事务T1的ReadView
t1_read_view = ReadView([11, 12], 13, 11)

# 模拟T1读取数据
data = read_data(charlie_row, t1_read_view)
print(data) # 输出: {'id': 1, 'name': 'Alice'}

MVCC的工作流程:数据写入

当一个事务需要写入一行数据时,MySQL会执行以下操作:

  1. 获取排他锁: 首先获取该行数据的排他锁,防止其他事务同时修改该行数据。

  2. 创建新的版本: 将修改前的旧版本数据写入Undo Log,并更新roll_ptr指向旧版本。

  3. 修改当前版本: 将新的数据写入当前版本,并更新trx_id为当前事务ID。

  4. 释放排他锁: 事务提交后,释放排他锁。

解决幻读:Next-Key Locks

虽然MVCC可以避免脏读和不可重复读,但它仍然可能出现幻读。为了解决幻读问题,MySQL在可重复读隔离级别下使用了Next-Key Locks

Next-Key Locks是Record Locks(锁定记录本身)和Gap Locks(锁定记录之间的间隙)的组合。

  • Record Locks: 锁定一行记录,防止其他事务修改或删除该记录。

  • Gap Locks: 锁定一个范围内的间隙,防止其他事务在该范围内插入新的记录。

Next-Key Locks锁定一个范围,包括记录本身和记录之前的间隙,从而防止其他事务在该范围内插入新的记录,从而避免幻读。

举例说明:

假设表users有以下数据:

id | name
---|------
1  | Alice
3  | Bob
5  | Charlie

如果事务T1执行以下查询:

SELECT * FROM users WHERE id > 1 AND id < 5;

MySQL会使用Next-Key Locks锁定以下范围:

  • (1, Alice]:锁定id为1的记录和id大于1小于等于1的间隙。
  • (3, Bob]:锁定id为3的记录和id大于3小于等于3的间隙。

这样,其他事务就无法在id为2或4的位置插入新的记录,从而避免幻读。

代码示例(伪代码,用于说明Next-Key Lock):

class NextKeyLock:
    def __init__(self, lower_bound, upper_bound, record_locked):
        self.lower_bound = lower_bound
        self.upper_bound = upper_bound
        self.record_locked = record_locked

    def is_locked(self, value):
        if self.lower_bound < value <= self.upper_bound:
            return True
        return False

# 模拟表users的数据和Next-Key Locks
data = {
    1: "Alice",
    3: "Bob",
    5: "Charlie"
}

locks = [
    NextKeyLock(0, 1, True), # (0, Alice]
    NextKeyLock(1, 3, True), # (1, Bob]
    NextKeyLock(3, 5, True), # (3, Charlie]
    NextKeyLock(5, float('inf'), False) # (5, infinity]
]

def can_insert(value, locks):
    for lock in locks:
        if lock.is_locked(value):
            return False
    return True

# 模拟插入id为2的记录
if can_insert(2, locks):
    print("可以插入id为2的记录")
else:
    print("不能插入id为2的记录,已被Next-Key Lock锁定") # 输出此行

# 模拟插入id为4的记录
if can_insert(4, locks):
    print("可以插入id为4的记录")
else:
    print("不能插入id为4的记录,已被Next-Key Lock锁定") # 输出此行

# 模拟插入id为6的记录
if can_insert(6, locks):
    print("可以插入id为6的记录") # 输出此行
else:
    print("不能插入id为6的记录,已被Next-Key Lock锁定")

总结:MVCC与Next-Key Locks的协同

MySQL使用MVCC来实现可重复读的隔离级别,通过Read View和版本链来保证事务读取到一致的数据。为了解决幻读问题,MySQL使用了Next-Key Locks,锁定记录本身和记录之间的间隙,防止其他事务插入新的记录。MVCC和Next-Key Locks协同工作,共同保证了可重复读隔离级别的实现。

思考:性能与隔离级别的权衡

不同的隔离级别对数据库的并发性能有不同的影响。更高的隔离级别可以提供更强的并发控制,但也可能降低数据库的并发性能。在实际应用中,需要根据业务需求和性能要求,选择合适的隔离级别。如果对数据一致性要求不高,可以选择较低的隔离级别,以提高并发性能。如果对数据一致性要求很高,可以选择较高的隔离级别,以保证数据的一致性。在大多数情况下,可重复读是一个比较好的选择,它既能保证一定的数据一致性,又能提供较好的并发性能。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注