Update ch07.md

This commit is contained in:
谈笑风生间 2023-01-06 01:11:36 +08:00 committed by muniao
parent d8616bf4c0
commit b134de6473

37
ch07.md
View File

@ -70,7 +70,7 @@
设有一个计数器,且数据库没有内置原子的自增操作,有两个用户,各自读取当前值,加 1 后写回。如图,期望计数器由 42 变为 44但由于并发问题最终变成了 43 。
ACID 中隔离性是指,每个事务的执行是互相隔离的,每个事务都可以认为自己是系统中唯一正在运行的事务,因此传统上,教科书将事务隔离形式称为:**可串行化Serializability**。即,如果所有事务都串行执行,则任意时刻必然只有一个事务在执行,从而在根本上消除任何并发问题。
ACID 中隔离性是指,每个事务的执行是互相隔离的,每个事务都可以认为自己是系统中唯一正在运行的事务,因此传统上,教科书将事务隔离形式称为:**可串行化Serializability**。即,如果所有事务都串行执行,则任意时刻必然只有一个事务在执行,从而在根本上消除任何并发问题。
但在实践中,很少用这么强的隔离性,实际上隔离性强弱类似于一个光谱,数据库系统提供商一般会实现其中几个,用户可以根据业务情况在隔离性和性能间进行选择。
@ -99,7 +99,7 @@ ACID 中隔离性是指,每个事务的执行是互相隔离的,每个事务
## 单对象和多对象操作
总结来说,在 ACID 中,原子性和隔离性是数据库对用户进行多个写入时需要提供的保证,并且们通常假设一个事务中会同时修改多个对象rows、documents 和 records )。相比**单对象事务**single-object transaction这种多**对象事务**multi-objects transaction是一种更强的保证且更常用因为通常多个写入不会针对单个对象。
总结来说,在 ACID 中,原子性和隔离性是数据库对用户进行多个写入时需要提供的保证,并且们通常假设一个事务中会同时修改多个对象rows、documents 和 records )。相比**单对象事务**single-object transaction这种多**对象事务**multi-objects transaction是一种更强的保证且更常用因为通常多个写入不会针对单个对象。
设有电子邮件情景,邮箱首页需要如下语句来展示未读邮件数:
@ -126,7 +126,7 @@ SELECT COUNT(*) FROM emails WHERE recipient_id = 2 AND unread_flag = true
实际中基本上使用第二种方法。
有一些非关系型数据库,虽然提供 Batch 操作接口,但们并不一定有事务语义,即可能有些对象成功,另外一些对象操作却失败。
有一些非关系型数据库,虽然提供 Batch 操作接口,但们并不一定有事务语义,即可能有些对象成功,另外一些对象操作却失败。
### 单对象写入
@ -184,7 +184,7 @@ SELECT COUNT(*) FROM emails WHERE recipient_id = 2 AND unread_flag = true
## 读已提交
性能最好的隔离级别就是不上任何锁,但会存在**脏读**和**脏写**的问题。为了避免脏写,可以给要更改的对象加长时写锁,但读数据时并不加锁,此时的隔离级别称为**读未提交**RURead Uncommitted。但此时仍然会有脏读为了避免脏读可以对要读取的对象加短时读锁此时的隔离级别是**读已提交**RCRead Committed他提供了两个保证
性能最好的隔离级别就是不上任何锁,但会存在**脏读**和**脏写**的问题。为了避免脏写,可以给要更改的对象加长时写锁,但读数据时并不加锁,此时的隔离级别称为**读未提交**RURead Uncommitted。但此时仍然会有脏读为了避免脏读可以对要读取的对象加短时读锁此时的隔离级别是**读已提交**RCRead Committed他提供了两个保证
1. 从数据库读取时只能读到已经提交的数据即没有脏读no dirty reads
2. 往数据库写入时只能覆盖已经提交的数据即没有脏写no dirty writes
@ -233,7 +233,7 @@ SELECT COUNT(*) FROM emails WHERE recipient_id = 2 AND unread_flag = true
![Untitled](img/ch07-fig06.png)
这种异常被称为**不可重复读**non-repeatable read或者**读倾斜**read skewskew 有点被过度使用)。在读已提交的隔离级别下,不可重复读时允许的,如上述例子,每次读取到的都是已提交的内容。
这种异常被称为**不可重复读**non-repeatable read或者**读倾斜**read skewskew 有点被过度使用)。读已提交的隔离级别允许出现不可重复读问题,如上述例子,每次读取到的都是已提交的内容。
例子中的不一致情况,只是暂时的。但在某些情况下,这种暂时的不一致也是不可接受的:
@ -246,7 +246,7 @@ SELECT COUNT(*) FROM emails WHERE recipient_id = 2 AND unread_flag = true
### 快照隔离的实现
和读已提交一样,快照隔离也使用加锁的方式来防止脏写,但在进行读取不能用使用锁。快照隔离的一个关键原则是“读不阻塞写,写不阻塞读”,从而允许用户在进行长时间查询时,不影响新的写入。
和读已提交一样,快照隔离也使用加锁的方式来防止脏写,但在进行读取不使用锁。快照隔离的一个关键原则是“读不阻塞写,写不阻塞读”,从而允许用户在进行长时间查询时,不影响新的写入。
为了实现快照隔离,保证读不阻塞写,且避免脏读,数据库需要对同一个对象保留多个已提交的版本,我们称之为**多版本并发控制****MVCCmulti-version concurrency control**)。
@ -255,7 +255,7 @@ SELECT COUNT(*) FROM emails WHERE recipient_id = 2 AND unread_flag = true
具体来说,使用 MVCC 流派,也可以实现读未提交、读已提交、快照隔离、可串行化等隔离级别。
1. **读已提交**在查询语句粒度使用单独的快照,快照粒度更小,因此性能更好。
2. **快照隔离**在事务粒度使用相同的快照(主要解决**可重复读**)。
2. **快照隔离**在事务粒度使用相同的快照(主要解决**不可重复读**问题)。
MVCC 的基本要点为:
@ -272,7 +272,7 @@ MVCC 的基本要点为:
- 个人认为不使用 delete by 也能达到标记删除的效果?
新的版本数据存在后,自动就使得老版本不可见。之后,只要确定没有事务正在访问老版本数据,即可进行 gc。如何判定没有事务访问了min(current tx) > latest version 即可
新的版本数据存在后,自动就使得老版本不可见。之后,只要确定没有事务正在访问老版本数据,即可进行 gc。通过min(current tx) > latest version 即可判定没有事务访问了
### 可见性规则
@ -286,7 +286,7 @@ MVCC 的基本要点为:
3. 具有较晚事务 ID 的事务所做的任何写入都会被忽略。
4. 剩余其他的数据,对此事务都可见。
如果事务 txid 一致是自增的,则可以理解为,对于 txid = x 的事务来说:
如果事务 txid 是严格自增的,则可以理解为,对于 txid = x 的事务来说:
1. 对于所有 txid < x 的事务如果已经中止或**正在进行**则其所写数据不可见
2. 对于所有 txid > x 的事务,所写数据皆不可见。
@ -306,7 +306,7 @@ MVCC 的基本要点为:
在实践中,有很多优化。如 PostgreSQL 的一个优化是,如果某个对象更新前后的数据都在一个物理页中,则对应的索引指向可以不用更新。
CouchDB、Datomic 和 LMDB 中使用一种 **仅追加 / 写时拷贝append-only/copy-on-write**的 B 树变体,是一种**多版本技术**的变体。[boltdb](https://www.qtmuniao.com/2020/11/29/bolt-data-organised/) 参考了 LMDB也可以归为此类此类 B 族树每次修改,都会引起叶子节点(所有数据都会落到叶子节点)到根节点的一条路径的全部修改(叶子节点变了,其父节点内容——指针也要修改,从而引起级联修改),如果发生引起分裂或者合并,这会引起更大范围的更新和修改
CouchDB、Datomic 和 LMDB 中使用一种 **仅追加 / 写时拷贝append-only/copy-on-write**的 B 树变体,是一种**多版本技术**的变体。[boltdb](https://www.qtmuniao.com/2020/11/29/bolt-data-organised/) 参考了 LMDB也可以归为此类此类 B 族树每次修改,都会引起叶子节点(所有数据都会落到叶子节点)到根节点的一条路径的全部修改(叶子节点变了,其父节点内容——指针也要修改,从而引起级联修改),如果发生节点的分裂或合并,会引起更大范围的更新
这种方式在更新时不会覆盖老的页,每个数据修改都会新生成一个树根,每个树根所代表的树可以视作一个版本的快照。使用某个树根就相当于使用某个版本快照,其所能访问到的数据都属于同一个版本,而无须再进行版本过滤。当然, 这类系统也需要后台常驻的 compaction 和 GC。
@ -316,7 +316,7 @@ CouchDB、Datomic 和 LMDB 中使用一种 **仅追加 / 写时拷贝append-o
因此,虽然快照隔离级别很有用,尤其是只读事务,但很多数据库虽然实现了快照隔离,但却另有称谓。比如 Oracle 将 SI 称为 **可串行化Serializable** PostgreSQL 和 MySQL 将 SI 称为 **可重复读repeatable read**。因为这样可以符合 SQL 标准要求,以号称兼容 SQL 标准。
但严格来说, SQL 对隔离级别的定义是有问题的,比如标准依赖于实现、几个隔离级别不连续、模糊不精确。很多数据库都号称实现了**可重复读**级别,但们提供的保证却存在着很大差异。虽然一些文献中有对可重复读进行了精确定义,但大部分实现并不严格满足此定义。到最后,没有人知道可重复读的真正含义。
但严格来说, SQL 对隔离级别的定义是有问题的,比如标准依赖于实现、几个隔离级别不连续、模糊不精确。很多数据库都号称实现了**可重复读**级别,但们提供的保证却存在着很大差异。虽然一些文献中有对可重复读进行了精确定义,但大部分实现并不严格满足此定义。到最后,没有人知道可重复读的真正含义。
## 防止更新丢失
@ -427,9 +427,10 @@ UPDATE wiki_pages SET content = 'new content' WHERE id = 1234 AND content = 'old
### 写偏序的特点
上述异常称为**写偏序**write skew他显然不属**脏写**和**更新丢失**,因为这两个事务在更新不同的对象,这里的竞态条件稍微有点不明显,但他们的确存在竞态条件,因为如果顺序执行,不可能出现没人值班的后果。另一个常见的例子是黑白棋翻转。
上述异常称为**写偏序**write skew它显然不属**脏写**和**更新丢失**,因为这两个事务在更新不同的对象,这里的竞态条件稍微有点不明显,但
的确存在竞态条件,因为如果顺序执行,不可能出现没人值班的后果。另一个常见的例子是黑白棋翻转。
从单对象到多对象的角度来看,写偏序可以算作是更新丢失的一种泛化。写偏序本质也是 read-modify-write虽然是涉及多个对象但本质仍然是**一个事务的写入会导致另外一个事务读取到信息失效**。补充一句,写偏序是由 MVCC 实现的快照隔离级别的特有的缺陷,它是由于读依赖同一个不变的快照引起的。
从单对象到多对象的角度来看,写偏序可以算作是更新丢失的一种泛化。写偏序本质也是 read-modify-write虽然是涉及多个对象但本质仍然是**一个事务的写入会导致另外一个事务读取到信息失效**。补充一句,写偏序是由 MVCC 实现的快照隔离级别的特有的缺陷,它是由于读依赖同一个不变的快照引起的。
解决更新丢失的很多手段,都难以直接用到解决写偏序上:
@ -485,11 +486,11 @@ COMMIT;
**多人棋类游戏**
之前提到的多人棋类游戏,对棋子对象加锁,虽然可以防止两个用户同时移动同一个棋子,却不能避免两个玩家将不同棋子移到一个位置。
之前提到的多人棋类游戏,对棋子对象加锁,虽然可以防止两个玩家同时移动同一个棋子,却不能避免两个玩家将不同棋子移到一个位置。
**抢注用户名**
在每个用户具有唯一用户名的网站上,两个用户可能会并发的尝试创建具有相同名字的账户。如果使用检查是否存在改名字→没有则注册改名字流程,在快照隔离级别下,是没法避免两个用户注册到相同用户名的。当然,可以通过对用户名列加唯一性约束来保证该特性,这样,第二个事务在提交时会因为违反唯一性约束而终止。
在每个用户具有唯一用户名的网站上,两个用户可能会并发的尝试创建具有相同名字的账户。如果使用检查是否存在该名字→没有则注册该名字流程,在快照隔离级别下,是没法避免两个用户注册到相同用户名的。当然,可以通过对用户名列加唯一性约束来保证该特性,这样,第二个事务在提交时会因为违反唯一性约束而终止。
**防止一钱多花**
@ -552,7 +553,7 @@ VoltDB/H-Store, Redis, and Datomic 实现了物理上的串行执行事务。由
### 将事务封装成存储过程
在数据库发展早期阶段,人们试图将数据库事务设计成为包含整个用户交互流程。如果整个交互流程都从属于一个事务,那么们就可以被原子的提交,这么抽象看起来很干净。
在数据库发展早期阶段,人们试图将数据库事务设计成为包含整个用户交互流程。如果整个交互流程都从属于一个事务,那么们就可以被原子的提交,这么抽象看起来很干净。
但人的交互所引入延迟远大于计算机 CPU 时钟周期甚至 IO 延迟,因此 OLTP 型数据库多会避免在单个事务中包含人的交互,以求单个事务能够较快的执行结束。在 Web 上,这意味着,不能让单个事务跨多个请求。但如果只允许单次请求执行一个语句,一个完整流程通常会包含多个语句,从而包含多次 RPC\HTTP 请求,会在通信上耗费太多时间。
@ -608,7 +609,7 @@ TDOO2PL 并非拿到所有的锁,才开始进行读写操作?而是按需
为了和书中保持一致,下面仍然称 2PL。
> 2PL 和 2PC 听起来很像,但们不是一个东西,只是恰好都有两个阶段而已。
> 2PL 和 2PC 听起来很像,但们不是一个东西,只是恰好都有两个阶段而已。
>
在防止脏写一节,提到了锁。但 2PL 中的锁会严格一些:
@ -681,7 +682,7 @@ WHERE room_id = 123 AND
前面小节详细聊了下数据库中隔离级别的图景:
1. 在光谱一侧,我们有很好的隔离级别——可串行化,但其实现要么性能差(两阶段锁),要么不可扩展(物理上串行执行)。
2. 在光谱另一个侧,我们有一些相对较弱的隔离级别,们性能较好,但会有各种竞态条件(更新丢失、写偏序、幻读等等)。
2. 在光谱另一个侧,我们有一些相对较弱的隔离级别,们性能较好,但会有各种竞态条件(更新丢失、写偏序、幻读等等)。
难道说强隔离级别和高性能两者不可得兼吗?