《数据密集型系统应用》笔记(六)

64 阅读9分钟

《数据密集型系统应用》笔记(六)

本章节主要介绍数据的事务

事务

数据不一致的来源:

1) 软硬件故障 2) 软件运行bug 3) 网络 4) 并发

解决方案: 事务,定义: 事务中的所有读写操作被视作单个操作来执行:整个事务要么成功(提交(commit))要么失败(中止(abort),回滚(rollback))。

ACID:

ACID (原子性 , 一致性, 隔离性, 持久性 durability)

  • A : 无法进一步切割分解;(即不能是一般成功)
  • C: 对数据的一组特定陈述必须始终成立,不变性; 即h是就是是,不是就是不是,无论用什么时间什么节点查;
  • I: A隔离性意味着,同时执行的事务是相互隔离的:比如不能互相看见各自的更新(在实现上和隔离级别有关)
  • D: 数据已被写入非易失性存储设,变更不能被丢失;

许多分布式数据存储已经放弃了多对象事务

事务的处理错误和异常

中止的重点就是允许安全的重试 ,重试的风险:

  • 因为网络错误, 而错误执行两次;
  • 错误是负载过大, 重试导致恶性循环
  • 其他副作用
  • 死锁等

隔离级别

  • 读未提交;实现:不做事务控制,
  • 读已提交; 实现方式: 行锁,
  • 可重复度;实现方式: mvcc txid + 回滚段(undo chain)维护read-view读视图; 快照隔离

image-20240619143304067.png

  • 串行化 实际上串行化在现在的数据场景中是普遍存在的,比如说使用同一个分片id进行hash, 然后同一个id下就是串行的。

CAS

又是也可以使用CAS 进行类似原子化的操作

幻读

发生在一个事务在两次执行相同的查询时,可能会看到不一致的结果集,因为在这两次查询之间,另一个事务插入了新的行,从而影响了查询结果集的大小。 事务 T1 执行一个查询,读取符合某个条件的多行数据。 事务 T2 插入(或删除)一些数据,使得这些数据符合(或不符合)事务 T1 的查询条件。 事务 T1 再次执行相同的查询,却发现结果集中多了(或少了)一些行,这些行被称为“幻行”。

2PC

阶段 1:准备阶段(Prepare Phase)

事务协调者(Coordinator) 向所有参与者(Participants)发送准备请求(Prepare Request),询问他们是否可以准备提交事务。 参与者 接收到准备请求后,执行事务预处理并记录事务日志,但不提交事务。 参与者 向协调者发送响应,表明自己是准备提交(Yes Vote)还是不能提交(No Vote)。

阶段 2:提交阶段(Commit Phase)

事务协调者 收到所有参与者的响应后,如果所有参与者都同意提交事务(所有参与者都发送了 Yes Vote),协调者向所有参与者发送提交请求(Commit Request);否则,发送回滚请求(Rollback Request)。 参与者 接收到提交请求后,提交事务并释放资源;接收到回滚请求后,回滚事务并释放资源。 参与者 完成操作后,向协调者发送确认消息(Acknowledgment),通知事务处理完毕。

阻塞问题:如果协调者在提交阶段崩溃,参与者会一直阻塞等待,无法释放资源。 单点故障:协调者的故障会影响整个事务的进程。 性能开销:2PC 需要多次网络通信和日志记录,增加了系统的开销

对应于innodb

innodb

事务日志(redo log):在准备阶段,事务的修改记录在 redo log 中,并被持久化到磁盘。 undo log:用于记录事务的回滚操作,确保在提交或回滚时能够恢复一致的数据库状态。 两阶段操作: 准备阶段确保所有参与者都准备好提交事务,并记录状态。 提交阶段根据协调者的决策执行提交或回滚操作。

顺便一提:

Binlog 写入发生在 2PC 的准备阶段和提交阶段之间:在 InnoDB 的 2PC 过程中,事务的修改首先被写入 redo log 并刷盘,然后写入 binlog 并刷盘,最后提交事务。
确保一致性和持久性:通过在提交事务之前确保 binlog 的写入和持久化,MySQL 保证了在崩溃恢复和复制场景下的事务一致性和持久性。
内部二阶段提交:MySQL 的内部二阶段提交机制确保了 binlog 和 InnoDB 事务的原子性操作,防止了 binlog 和事务日志的不一致。

间隙锁

如果活跃事务持有很多锁,检查匹配的锁会非常耗时。因此,大多数使用2PL的数据库实际上实现了索引范围锁,

原先的理解中间隙锁是插空,这里的描述上间隙锁是对所有中间值的合并;

作用是防止幻读和写入偏差;

原理

在 MySQL 的 InnoDB 存储引擎中,间隙锁(Gap Lock)是一种为了实现一致性读(可重复读隔离级别)和防止幻读现象的锁机制。间隙锁不仅锁住具体的行数据,还锁住行数据之间的间隙,以确保事务的一致性和隔离性。下面是关于间隙锁的设计目的和工作原理的详细介绍。

### 间隙锁的设计目的

1. **防止幻读**   - 幻读是指一个事务在读取数据集时,另一个事务插入了新行,导致前一个事务在重新读取数据集时,看到的数据集大小发生了变化。间隙锁通过锁住行与行之间的间隙,防止其他事务在间隙中插入新行,从而避免幻读现象。

2. **确保可重复读**   - 在可重复读隔离级别下,间隙锁确保在事务的生命周期内,同一查询返回的结果集保持一致。通过锁住间隙,防止其他事务在间隙中插入或删除数据,确保查询结果的一致性。

### 间隙锁的工作原理

间隙锁作用于索引记录之间的间隙,而不仅仅是索引记录本身。它的工作原理如下:

1. **锁定间隙**   - 当一个事务执行一个范围查询时,InnoDB 会锁定满足查询条件的记录以及这些记录之间的间隙。这样可以防止其他事务在这些间隙中插入新记录。

2. **间隙锁类型**   - **纯间隙锁(Gap Lock)**:仅锁住记录之间的间隙,而不锁住实际的记录。例如,对于索引记录 (100, 200),纯间隙锁会锁住 (100, 200) 之间的间隙。
   - **临键锁(Next-Key Lock)**:结合行锁和间隙锁,锁住索引记录以及记录前后的间隙。例如,对于索引记录 100,临键锁会锁住 (100, 200) 之间的间隙,包括索引记录 100 本身。

3. **锁的实现**   - 间隙锁通过 B+ 树索引实现,锁住索引记录及其间隙。当一个事务需要锁住某个范围时,InnoDB 会在 B+ 树中找到该范围的起始和结束位置,并锁住这些位置之间的所有间隙。

### 示例

假设有一个表 `employees`,其索引为 (100, 200, 300),两个事务并发执行:

1. **事务 T1** 开始:

``sql
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM employees WHERE employee_id BETWEEN 100 AND 200 FOR UPDATE;
-- 锁住索引 100 和 200 之间的记录及其间隙
``

2. **事务 T2** 尝试插入数据:

``sql
START TRANSACTION;
INSERT INTO employees (employee_id, name, salary) VALUES (150, 'Charlie', 7000);
-- 因为间隙锁,事务 T2 被阻塞,无法插入记录 150
``

3. **事务 T1 提交或回滚后**:

``sql
COMMIT;
-- 事务 T1 释放锁后,事务 T2 才能继续执行插入操作
``

### 间隙锁的应用场景和限制

1. **应用场景**   - 间隙锁在可重复读(Repeatable Read)隔离级别下广泛使用,用于防止幻读和确保查询结果的一致性。
   - 它适用于需要对范围查询结果进行一致性控制的场景,如银行系统中的账户余额查询和更新。

2. **限制和注意事项**   - 间隙锁会导致锁定粒度较粗,可能导致更多的锁竞争和阻塞,影响系统性能。
   - 在读已提交(Read Committed)隔离级别下,InnoDB 默认不使用间隙锁,以提高并发性能。

### 总结

间隙锁通过锁住索引记录之间的间隙,防止其他事务在间隙中插入或删除数据,从而避免幻读和确保查询结果的一致性。这是实现事务隔离性和一致性的关键机制之一,特别是在可重复读隔离级别下。然而,间隙锁也可能增加锁竞争和阻塞,需要在应用设计中平衡一致性和性能。

悲观与乐观的并发控制

悲观乐观就是一般意义上的,类似锁; 悲观: 如果有事情可能出错(如另一个事务所持有的锁所表示的),最好等到情况安全后再做任何事情;

乐观: 如果存在潜在的危险也不阻止事务,而是继续执行事务,希望一切都会好起来

总结:

几个术语:

  • 脏读: 一个客户端读取到另一个客户端尚未提交的写入。读已提交或更强的隔离级别可以防止脏读。

  • 脏写:一个客户端覆盖写入了另一个客户端尚未提交的写入。几乎所有的事务实现都可以防止脏写。

  • 更新丢失: 两个客户端同时执行读取-修改-写入序列。其中一个写操作,在没有合并另一个写入变更情况下,直接覆盖了另一个写操作的结果。所以导致数据丢失。

  • 读取偏差(不可重复读):在同一个事务中,客户端在不同的时间点会看见数据库的不同状态。快照隔离经常用于解决这个问题,它允许事务从一个特定时间点的一致性快照中读取数据。快照隔离通常使用多版本并发控制(MVCC) 来实现。(感觉总是和幻读混起来)

  • 幻读 事务读取符合某些搜索条件的对象。另一个客户端进行写入,影响搜索结果。快照隔离,gaplock可以防止直接的幻像读取;

  • 写偏差;一个事务读取一些东西,根据它所看到的值作出决定,并将决定写入数据库。但是,写作的时候,决定的前提不再是真实的。只有可序列化的隔离才能防止这种异常。