「如何实现流动式软件发布」线上课堂开课啦,快来报名参与课堂抽奖吧~ 了解详情
写点什么

数据库内核杂谈 (十):事务、隔离、并发(1)

2020 年 4 月 24 日

数据库内核杂谈(十):事务、隔离、并发(1)

本篇文章选自数据库内核杂谈系列文章。


在之前的文章,我们和大家分享了基本的数据库优化器和执行器。这篇文章,我们要分享一个很重要的概念:事务及其相关实现。


事务(transaction)和 ACID

事务的定义是:一个事务是一组对数据库中数据操作的集合。无论集合中有多少操作,对于用户来说,只是对数据库状态的一个原子改变。


单从概念定义来理解,可能有些晦涩难懂,我们举个例子来讲解:数据库中有两个用户的银行账户 A:100 元; B:200 元。假设事务是 A 转账 50 元到 B,可以理解为这个事务由两个操作组成:1) A-= 50; 2) B+=50。对于用户来说,数据库对于这个事务只有两个状态:执行事务前的初始状态,即 A:100 元; B:200 元,以及执行事务后的转账成功状态:A:50 元;B:250 元,不会有中间状态,比如钱从 A 已经扣除,却还没转到 B 上:A:50 元; B:200 元。


一个事务的所有操作要么全部执行,要么一个都不执行。如果在执行事务的过程中,因为任何原因导致事务失败,已经执行的操作都要被回滚(rollback)。这种“all-or-none"的属性就是所谓的事务的原子性(atomicity)。


当一个事务被认定执行成功后,即代表这个事务的操作被数据库持久化。因此,即使数据库在此时奔溃了,比如进程被杀死了,甚至是服务器断电了,这个事务的操作依然有效,这就是事务的另一个属性,持久性(durability)。


假定数据库的初始状态是稳定的,或者说对用户来说是一致的。由于事务执行的原子性,即执行失败就回滚到执行前的状态,执行成功就变成一个新的稳定状态。因此,事务的执行会保持数据库状态的一致性(consistency)。


数据库系统是多用户系统。多个用户可能在同一时间执行不同的事务,称为并发。如果想要做到事务的原子性,那么数据库就必须做到并发的事务互不影响。从事务的角度出发,在执行它本身的过程中,不会感知到其他事务的存在。从数据库的角度出发,即使同一时间有多个事务并发,从微观尺度上看,它们之间也有先来后到,必须等一个事务完成后,另一个事务才开始。这种并发事务之间的不感知就是所谓的事务隔离性(isolation)。


总之,一个事务是一组对数据库中数据操作的集合。事务,对于数据库系统,具有原子性(atomicity),一致性(consistency),隔离性(isolation),以及持久性(durability)。曾经听过这样一个观点,事务的出现主要是针对并发。其实不然,ACID 属性中只有隔离性是针对并发事务的。所以,即使数据库系统是一个单用户系统,我们依然希望事务具有原子性、一致性和持久性。


隔离级别(Isolation Level)

如果让你来实现事务的隔离性,最容易的办法,你会想到什么?我想绝大部分的读者都会想到,给数据库加一个全局的操作锁,在同一时间里只允许一个用户对数据库进行操作,这就保证了隔离性。


的确,这样可以保证隔离性,但也限制了并发性,对数据库的性能产生了极大的影响。在实际情况中,没有数据库会这么去实现。并且这个世界并非非黑即白,隔离性也并不是有或者没有。数据库一般会提供多种隔离性的级别,供用户选择:越严格的隔离级别越接近全局锁,越宽松的隔离级别越能提高并发。天下没有免费的午餐,宽松的隔离级别也会随之带来一些问题。


我们结合并发事务可能带来的问题,来讲述一下不同的隔离级别。


首先,我们定义一个相对简单的事务模型,方便后续讨论各种隔离级别和可能遇到的数据问题。虽然数据库支持各种复杂的操作,但归根到底就是对数据基本单元的读写操作,对于任一给定数据单元 A,我们定义 read(A),write(A, val)分别为读取和写入操作。 同时,对于事务,提供 begin(开启事务), commit(提交事务), rollback(回滚事务)操作。


先从最宽松的隔离级别开始,read uncommitted(读未提交)。顾名思义,读未提交就是在一个事务中,允许读取其他事务未提交的数据。下图示例很清晰地诠释了读未提交:



在事务 T1 中,读取 A 得到结果是 5,是因为事务 T2 修改了 A 的值,虽然当时 T2 还未提交,甚至最后 T2 回滚了。读未提交导致的问题就是 dirty read(脏读)。脏读的定义就是,一个事务读取了另一个事务还未提交的修改。虽然可能大多数情况下,我们都会认为脏读产生了不正确的结果。但是,抛开业务谈正确性都是耍流氓。或许,某些用户的某些业务,为了支持更大地并发,允许脏读的出现。因为,对于读未提交,完全不需要对操作进行加锁,自然并发性更高。


如何避免脏读呢?数据库引入了第二层的隔离级别,read committed(读提交)。读提交就是指在一个事务中,只能够读取到其他事务已经提交的数据。


在读提交的隔离级别下,再回看上面的例子,T1 中读取 A 的值就应该还是 10,因为当时 T2 还没有提交。沿着上面的例子,接着往下看,如果最后 T2 提交了事务,而 T1 在之后又读取了一次 A,这时候的值就变为 5 了。



这又出现了什么问题呢?在 T1 事务中,先后读取了两次 A,两次的值不一样了。回顾最早提及的事务的隔离性,两次读取同一数据的值不一样,其实违反了隔离性。因为隔离性定义了一个事务不需要感知其他事务的存在,但显然,由于值不同,说明在这个过程中另一个事务提交了数据。这类问题就被定义为 nonrepeatable read(不可重复度读):在一个事务过程中,可能出现多次读取同一数据但得到的值不同的现象。


如何避免不可重复度这个问题呢?数据库引入了第三层隔离级别,根据上面的经验,你可能已经猜出来了,名称就叫做 repeatable read(可重复读)。可重复读指的是在一个事务中,只能读取已经提交的数据,且可以重复查询这些数据,并且,在重复查询之间,不允许其他事务对这些数据进行写操作。虽然我们还没讲到实现,但不难想象,对读数据加读锁锁就能实现。


对于可重复读级别来说,上述例子中的两次读取都会得到数据是 10。读者可能会有疑问,那彼时 T2 的 commit 会失败吗?如果是加锁实现的可重复读,那 T2 的 commit 就会 hold 在那,直至 T1 结束,取决于 T1 最后有没有更新 A,如果有,T2 就会失败。


可重复读,似乎看上去很完美,解决了所有并行事务带来的不确定性。其实不然,我们通过下面这个 SQL 语句的例子来看:


T1:BEGIN;SELECT * FROM students WHERE class_id = 1;  // (1)... SELECT * FROM students WHERE class_id = 1;  // (2)...COMMIT;
复制代码


上面示例中的查询语句(1)和(2),在可重复读隔离级别下,应该返回相同的结果吗?乍一看,应该觉得,没错啊。但可重复读隔离级别只是规定对被已经读取的数据,禁止其他事务进行修改。那如果是下面这个事务呢?


T2:BEGIN;INSERT INTO students (1 /* class_id */, ...);COMMIT; 
复制代码


T2 事务并没有修改现有数据,而是新增了一条新数据,恰巧 class_id = 1。如果这条插入介于(1)和(2)之间,(2)的结果会改变吗?答案是,会的。语句(2)会比(1)多显示一条记录,即 T2 插入的。这个问题被称为 phantom read(幻读),指的是,在一个事务中,当查询了一组数据后,再次发起相同查询,却发现满足条件的数据被另一个提交的事务改变了。


如何才能避免幻读呢?数据库系统只能推出最保守的隔离机制,serializable(可有序化),即所有的事务必须按照一定顺序执行,直接避免了不同事务并发带来的各种问题。


数据库系统针对不同需求,推出了不同的隔离级别,由宽到紧分别是:


1)读未提交:在一个事务中,允许读取其他事务未提交的数据。


2)读提交:在一个事务中,只能够读取到其他事务已经提交的数据。


3)可重复读:在一个事务中,只能读取已经提交的数据,且可以重复查询这些数据,并且,在重复查询之间,不允许其他事务对这些数据进行写操作。


4)可有序化:所有的事务必须按照一定顺序执行。


而后三种隔离级别分别为了解决前一种隔离级别遇到的问题:


1)脏读:一个事务读取了另一个事务还未提交的修改。


2)不可重复度:在一个事务过程中,可能出现多次读取同一数据但得到不同值的现象。


3)幻读:在一个事务中,当查询了一组数据后,再次发起相同查询,却发现满足条件的数据被另一个提交的事务改变了。


下方列出了一张表格,更直观地展现它们之间的关系。


隔离级别脏读不可重复度幻读
读未提交可能出现可能出现可能出现
读提交不能可能出现可能出现
可重复读不能不能可能出现
可有序化不能不能不能


总结

这篇文章主要覆盖了事务的定义、ACID 属性以及对于隔离性,数据库推出的不同隔离级别。虽然并没有提到很多的实现,不过,理清这些概念对于理解和学习事务的实现是很有必要的。预告一下,下篇文章我们会分享事务的实现。


2020 年 4 月 24 日 08:345831

评论 4 条评论

发布
用户头像
同意  哈哈

抛开业务谈正确性都是耍流氓

2021 年 02 月 22 日 19:08
回复
用户头像
深入浅出……高手

数据库一般会提供多种隔离性的级别,供用户选择:越严格的隔离级别越接近全局锁,越宽松的隔离级别越能提高并发

2021 年 02 月 22 日 18:48
回复
用户头像
作者来留个言,一言难尽的2020终于过去了,新年快乐,希望2021对所有人,都充满惊喜。数据库内核杂谈也已经更新到第十四期了,再次感谢各位的关注。如果你是内核杂谈的真粉丝,觉得内核杂谈对你有收获,愿意听我多扯扯,欢迎加入我的知识星球:Dr.ZZZ 聊技术和管理:https://t.zsxq.com/VrZbeAE(由于不知道能输出多少,也不知道有多少粉丝,所以还是先免费吧)
2021 年 01 月 01 日 02:44
回复
用户头像
期待ing
2020 年 05 月 12 日 19:02
回复
没有更多了
发现更多内容

第四周总结

不在调上

【架构师训练营】第四周作业

Mr.hou

极客大学架构师训练营

第四周总结

赵龙

架构师训练营第4周作业

不谈

极客大学架构师训练营

架构师训练营-week4-作业

晓-Michelle

极客大学架构师训练营

互联网架构总结

Lane

极客大学架构师训练营

互联网系统架构设计概览

dony.zhang

架构师训练营第四周 架构分析

suke

极客大学架构师训练营

Week 04 学习总结

卧石漾溪

极客大学架构师训练营

《了不起的我》:关于「改变」的心理学

强劲九

心理 读书 书籍推荐 看书

【架构师训练营】第四周总结

Mr.hou

极客大学架构师训练营

Mybatis执行过程源码分析

编号94530

Java 源码分析 mybatis

浅谈比特币匿名的意义

CECBC区块链专委会

通用编程风格

顿晓

Java 学习 编程风格

大型互联网应用系统的技术方案和手段

周冬辉

程序员如何提升自己横向能力?

Boss.Guo

团队建设 能力提升 人才培养 个人总结

假想 一个进销存软件是如何发展的

不在调上

架构师训练营 - 第四周 - 作业

韩挺

Week4  互联网系统的技术和手段

TiK

极客大学架构师训练营

架构师训练营 - 第四周 - 学习总结

韩挺

架构师训练营第四周学习总结

fenix

第四周作业

赵龙

区块链技术打通信用壁垒赋能租赁业务

CECBC区块链专委会

去中心 区块链技术 防篡改 去信任

「架构师训练营」第 4 周作业 - 一个典型的大型互联网应用系统使用了哪些技术方案和手段,主要解决什么问题

guoguo 👻

极客大学架构师训练营

Redis(一)分布式缓存的作用

奈何花开

Java redis 分布式缓存

架构师训练营第4周学习总结

不谈

架构师训练营 No.4 作业

连增申

可复用架构之分离关注点

松花皮蛋me

java面试 Java 分布式 可复用架构

第四周作业

王鑫龙

极客大学架构师训练营

架构师训练营第四周作业

张锐

架构师训练营-第四章-学习总结

而立

极客大学架构师训练营

数据库内核杂谈(十):事务、隔离、并发(1)-InfoQ