黄东旭关于基础软件产品价值的思考
891
2023-10-23
事务在当今的企业系统无处不在,即使在高并发环境下也可以提供数据的完整性。一个事务是一个只包含所有读/写操作成功的集合。如下图:
一、事务本质上有四个特点ACID:
Atomicity 原子性
Consistency 一致性
Isolation 隔离性
Durability 耐久性
原子性
原子性任务是一个独立的操作单元,是一种要么全部是,要么全部不是的原子单位性的操作。
一致性
一个事务可以封装状态改变(除非它是一个只读的)。事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少。
一致性有下面特点:
如果一个操作触发辅助操作(级联,触发器),这些也必须成功,否则交易失败。
如果系统是由多个节点组成,一致性规定所有的变化必须传播到所有节点(多主复制)。如果从站节点是异步更新,那么我们打破一致性规则,系统成为“最终一致性”。
一个事务是数据状态的切换,因此,如果事务是并发多个,系统也必须如同串行事务一样操作。
在现实中,事务系统遭遇并发请求时,这种串行化是有成本的, Amdahl法则描述如下:它是描述序列串行执行和并发之间的关系。
“一个程序在并行计算情况下使用多个处理器所能提升的速度是由这个程序中串行执行部分的时间决定的。”
大多数数据库管理系统选择(默认情况下)是放宽一致性,以达到更好的并发性。
隔离性
事务是并发控制机制,他们交错使用时也能提供一致性。隔离让我们隐藏来自外部世界未提交的状态变化,一个失败的事务不应该破坏系统的状态。隔离是通过用悲观或乐观锁机制实现的。
耐久性
一个成功的事务将永久性地改变系统的状态,所以在它结束之前,所有导致状态的变化都记录在一个持久的事务日志中。如果我们的系统突然受到系统崩溃或断电,那么所有未完成已提交的事务可能会重演。
尽管一些数据库系统提供多版本并发控制 MVCC, 他们的并发控制都是通过锁完成,因此,锁会增加执行的串行性,影响并发性。
二、SQL标准规定了四个隔离水平:
READ_UNCOMMITTED
READ_COMMITTED
REPETABLE_READ
SERIALIZABLE
脏读
脏读发生在:当一个事务允许读取一个被其他事务改变但是未提交的状态时,这是因为并没有锁阻止读取,如上图,你看到第二个事务读取了一个并不一致的值,不一致的意思是,这个值是无效的,因为修改这个值的第一个事务已经回滚,也就是说,第一个事务修改了这个值,但是未提交确认,却被第二个事务读取,第一个事务又放弃修改,悔棋了,而第二个事务就得到一个脏数据。
不可重复读
反复读同一个数据却得到不同的结果,这是因为在反复几次读取的过程中,数据被修改了,这就导致我们使用了stale数据,这可以通过一个共享读锁来避免。这是隔离级别READ_COMMITTED会导致可重复读的原因。设置共享读锁也就是隔离级别提高到REPETABLE_READ。
幻读
当第二个事务插入一行记录,而正好之前第一个事务查询了应该包含这个新纪录的数据,那么这个查询事务的结果里肯定没有包含这个刚刚新插入的数据,这时幻影读发生了,通过变化锁和predicate locking避免。
三、下图是主流数据库的默认隔离级别:
Database Default isolation Level
*** READ_COMMITTED
MySQL REPETABLE_READ
Microsoft *** READ_COMMITTED
*** READ_COMMITTED
*** CURSOR STABILITY (a.k.a READ_COMMITTED)
READ_COMMITED 是正确的选择,因为SERIALIZABLE虽然能在不同事务发生时避免stale数据,也就是避免上述丢失刚刚修改的数据,但是性能是最低的,因为是一种最大化的串行。
MySQL 是如何实现 ACID 的?
我们都知道,事务具有 ACID 四个特性——原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。但你知道 MySQL 是通过什么技术手段来实现的吗?
ACID 简介
先来简单回顾一下 ACID 的定义:
原子性:事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
一致性:事务开始前和事务结束后,数据库的完整性没有被破坏。即写入的数据必须完全符合所有的预设约束、触发器、级联回滚等。
隔离性:多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
持久性:已被提交的事务对数据库的修改应该永久保存在数据库中。即使系统挂了,数据也不会丢。
我们按照:持久性 -> 原子性 -> 隔离性 -> 一致性 的顺序来讨论。
持久性
我们知道程序修改数据的时候,是先将数据从磁盘加载到内存,然后修改完再由内存写回磁盘。持久化其实就是将内存里的数据写入磁盘。因此,持久性的关键就在于如何保证数据可以由内存顺利写入磁盘。
我们有以下几个方案:
方案一:
加载数据到内存
修改内存
然后写回磁盘
提交事务
方案二:
加载数据到内存
修改内存
提交事务
后台写回磁盘
第一种方案,靠谱是靠谱,但性能太低,会严重拖累 MySQL 的吞吐量。
第二种方案虽然性能上来了,但如果在第四步时宕机了,而系统认为事务已提交,这时候就会丢失数据了。
那怎么办呢?MySQL 给出的方案是 WAL(Write Ahead Log)机制。WAL 翻译过来就是先写日志的意思。这个日志就是 redo log。具体做法是:
加载数据到内存
修改内存
写入 redo log
提交事务
后台写回磁盘
如果第五步时系统宕机,也可以通过 redo log 来恢复。
你可能有疑问:写入 redo log 不也有磁盘 I/O 吗?这不是脱了那啥再那啥,多此一举吗?写 redo log 和写表的区别就在于随机写和顺序写。MySQL 的表数据是随机存储在磁盘中的,而 redo log 是一块固定大小的连续空间。而磁盘顺序写入要比随机写入快几个数量级。
因此,这种方案即保证了数据的安全,性能上也能够接受。
原子性
假如一个事务做了如下操作:
插入一条数据 insert into user values(‘1’,‘小刘’,‘18’)
更新一条数据 update user set name = ‘小水’ where id = 2
删除一条数据 delete from user where id = 3
根据原子性的规定,这三个操作要么都成功,要么都失败。那么问题就来了,如何保证 3 失败的情况下,让 1,2 也回退呢?
答案就是 undo log。
每个事务操作(增删改)都会记录一条与之对应的 undo log:
insert 记录插入的主键,回滚则根据该主键删除记录
update 记录记录主键和被修改列的当前值,回滚则根据主键和之前的值覆盖
delete 为记录添加删除标志,即 MySQL 内部的逻辑删除,回滚根据主键恢复
隔离性
数据库事务有四种隔离级别,不同的级别可能会出现各种各样的问题(脏读、幻读、不可重复读),关系如下:
MySQL 中 RR 级别已经解决了幻读问题。
并发的情况才需要隔离,而并发有三种组合:
读读
读写
写写
「读读」的情况,不需要隔离;「读写」通过 MVCC 隔离;「写写」只能通过锁来隔离。
MVCC(Multi Version Concurrency Control,多版本并发控制)作用于 RC 和 RR 级别。可以为事务中的读操作创建一个快照(Readview),从而来避免被其他事务干扰。
RC 级别下,一个事务中的每次(同参数)读都会创建一个 Readview。
RR 级别下,一个事务中只在第一次读时创建 Readview,后面再次读,仍然读取该 Readview。
「写写」的情况通过三种锁来实现隔离:Record Lock、Gap Lock 和 Next Key Lock(前两者的组合)。
Record Lock 锁住一条数据,从而使其他事务无法修改和删除;Gap Lock 锁住一个范围,从而使其他事务不能在该区间插入数据;Next Key Lock 锁住具体数据和区间,从而使其他事务无法更新、删除和在该区间插入数据。
MVCC + 锁 使得 MySQL 在 RR 级别避免了幻读问题。
一致性
很多人聊到一致性,很喜欢拿转账的业务举例,但这明显是原子性的范畴——A 账户扣钱,B 账户加钱,两个 Update 操作,要么都成功,要么都失败。
一致性更侧重是,数据的完整性:主外键约束、唯一索引、列完整等。MySQL 中保证一致性主要靠 CR(Crash Recovery)和 DWB(Doublewrite Buffer)来保证的。
这两个特性比较复杂,一篇文章根本讲不完,如果你感兴趣可以去看官方文档,或者留言告诉我,我来安排。
最后
一致性是一个比较特殊的存在,它和原子性、隔离性有一层「你中有我,我中有你」的暧昧关系。比如转账的业务场景,如果说它属于一致性的范畴,也能够说得通,可以叫「用户自定义一致性」;另外,隔离性使得事务之间互不影响的最终效果也是保证了数据的一致。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。