1、聊聊事务的概念与特性
记忆中第一次准确了解到“事务”这个词的时候,是在大学数据库的课堂上吧。
数据库作为数据存储持久化的地方,如何保证一次操作的整体性,使得这次操作具有现实意义,应该是数据库必须要具备的能力。这么一看,在数据库的课堂上接触到“事务”这个词,是理所应当的事情嘛。
让我给“事务”下一个定义的话:
一个包含多个动作的事情,且这个事情只有执行完成或没有执行的状态。当其中的一系列动作没有按照步骤完整地成功执行,就意味着事情执行失败,并应当恢复到事情开始执行之前的状态。
这么一说不难看出“事务”一个非常重要的特征:原子性(atomicity)。为啥叫原子性,大概是人们认为原子是不可切割的最小个体吧(提问:夸克呢?嗯,就你话多)
再看看上面下的定义,还涉及到关于状态变更的说明。对于事情的执行,它对于周围的状态影响,应该只有执行前和执行后。简单地说,事情只会使得周围从一个状态转换到另外一个状态。当然这是建立在原子性这一特性上面的。如果事情不存在原子性,即其中操作能够被部分执行,那么就会出现其他的中间状态。对于这个特性被称作:一致性(consistency)。关于这个一致性,也是否意味着只要这个事情进行执行了,就肯定会发生从状态A转移到状态B的情况,这个是个有趣的说法,我觉得这个问题大家也能去想一下。
上面的这两个特性,我更愿意把它们划分为是“事务”的内部特性,即这是能够从一个单独的事务中就能体现出来的。所以我们不妨引出一个问题,如果有多个不同的事务,那么又会有怎样的特性呢。事务1能够从状态A转换到状态B,事务2能够从状态C转换到状态D。在状态A、B和状态C、D都是可以重叠不冲突的情况下,这两个事务的同时执行,是不是能够把周围的从A、C态转换到B、D态呢,这个我理解是没有什么争议的。我们不妨来换一个情况,如果状态A可以转换成为状态B或者状态C,那么事务1是完成从A到B,事务2是完成从A到C的。在事务1、事务2同时执行的情况下,我们就会发现问题了,那么最后是变成状态B还是状态C?第一种情况能够完成的本质原因,是因为两个事务并没有对状态这个资源进行抢占,没有抢占的原因是,他们独立且并不冲突。但是在事务们对同一个状态转换存在冲突的时候呢?所以为了保证事务的顺利执行,需要引入一个用于执行的特性:隔离性(isolation)。隔离性为我们提供一个保证:事务的执行应当是互不干扰的。对于互不干扰,最好的解决方式就是串行执行了吧。但是不可能都会串行执行啊,效率太低了啊,对吧。所以我们不妨关注一下后续的相关内容,看看对于隔离性,这些解决方案都是怎么解决的。
隔离性告诉了我们事务执行的过程的特性了,那么执行完了之后呢?那不就是从状态A变成了状态B嘛。我理解这个状态的变换是遵从“牛顿第一定律”的,即在没有外力的情况,它将一直保持静止或匀速直线运动(好像有点串场了)。所以这个状态的变换是会一直保存下去的,我们称之为:持久性(durability)。
到此为止,事务的四个特性就引出来了,但是看上去,我认为这个更像是四个要求,就是这个四个特性组合在一起就诞生了“事务”一个概念,而不是“事务”本身就具备的。看上去这个思考的过程虽然有点长,但是是顺其自然的,能够很清晰地说明事务本身本必备的特性。主要是我一直觉得ACID这个简称记忆起来也太死记硬背,不利于知识的理解,对吧。
2、“坏情况”盘点
回应上文,对于“隔离性”的保障是事务执行过程中必须要关注的,否则就丧失了“事务”给我们来的的意义了。
在提到说到这些保障措施的时候,我们其实更应该先来看看,可能出现的各种错误情形。上面引出隔离性的时候,比较模糊地用状态A、B等来替代,未免也太不专业了。所以下面的说明,会用更完整的操作和状态样例,来说明“坏情况”。先清楚知道“坏情况”及其来源,才能够引导我们思考,为什么要提出这样的措施,也就是隔离方案来避免这样的情况。
“坏情况”一览:
(1)脏读(dirty read)
抄来的样例情景:
假设我们现在有这样一张表(T),里面记录了很多牛人的名字
第一天,事务A访问了数据库,它干了一件事情,往数据库里加上了新来的牛人的名字,但是没有提交事务。
insert into T values (4, '牛D');
这时,来了另一个事务B,他要查询所有牛人的名字。
select Name from T;
这时,如果没有事务之间没有有效隔离,那么事务B返回的结果中就会出现“牛D”的名字。这就是“脏读(dirty read)”。
场景特点:对一条数据仅读了一次,就读到了不正确的脏数据。
(2)不可重复读(unrepeatable read)
前文再续书接上一回的情景:
第二天,事务A访问了数据库,他要查看ID是1的牛人的名字,于是执行了
select Name from T where ID = 1;
这时,事务B来了,因为ID是1的牛人改名字了,所以要更新一下,然后提交了事务。
update T set Name = '不牛' where ID = 1;
接着,事务A还想再看看ID是1的牛人的名字,于是又执行了
select Name from T where ID = 1;
结果,两次读出来的ID是1的牛人名字竟然不相同,这就是不可重复读(unrepeatable read)。
场景特点:对于同一条已有的数据,前后读了两次,但是读了两次的数据不一样。
(3)幻读(phantom problem)
再一次前文再续书接上一回的情景:
第三天,事务A访问了数据库,他想要看看数据库的牛人都有哪些,于是执行了
select * from T;
这时候,事务B来了,往数据库加入了一个新的牛人。
insert into T values(4, '牛D');
这时候,事务A忘了刚才的牛人都有哪些了,于是又执行了。
select * from T;
结果,第一次有三个牛人,第二次有四个牛人。
相信这个时候事务A就蒙了,刚才发生了什么?这种情况就叫“幻读(phantom problem)”。
场景特点:针对某一张表的数据进行的列表查询,两次数量不一致。
(4) 更新丢失
对不起,隔壁抄来的没有这个场景。是的,所以也没有英文名称
这种情况其实非常容易理解,就是两个事务同时去更新同一条数据,比如事务A需要对该数据进行“+100”的操作,而事务B需要对数据进行“+200”的操作,那么在没有对该数据加上“排他写锁”,即同时只允许一个事务对这个数据的写操作权限的情况下,事务A、B执行完成后可能并没有出现预期的数据“+300”的操作,也就是出现了更新丢失的情况,可能数据仅会出现“+100”或者“+200”的任一情况。
总结一下
再来三种“坏情况”进行一下回顾:
情况 | 原因导致 |
---|---|
脏读(dirty read) | 单次事务在未执行提交完成时,有其他人进行读取并获取了未执行完成的数据 |
不可重复读(unrepeatable read) | 一个事务对同一条数据进行两次读取时,有其他事务在期间进行了数据更新,导致前后不一致 |
幻读(phantom problem) | 一个事务对一张表数据进行两次读取时,有其他事务对表数据进行了数据的插入或者删除,导致前后数量不一致 |
更新丢失 | 多个事务同时对同一个数据进行更新,导致只有部分执行结果被保留,其余执行结果被覆盖丢失 |
由此,我们引出几种等级的隔离策略,来试图解决上面出现的几种“坏情况”。
3、解决方案:事务隔离
对于上述的四种“坏情况”,不可能只有两种隔离策略来说要么四个种情况我都能完美避免,或者四种情况我都不闻不问,应当存在一些中间的分级策略,来解决部分的情况,来达到最低限度的要求,毕竟我们还是要兼顾性能的。
(1)读未提交(Read uncommitted)
最低级别的隔离策略。
如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读此行数据,该隔离级别可以通过“排他写锁”,但是不排斥读线程实现。
在该隔离等级下,解决了更新丢失的情况,因为同一时间仅会有一个事务拥有对该数据的写权限。其余事务需要等待拥有写权限的事务完成后再重新发起占据之后才进行数据的更新。
从名字上来看我们也知道,在这种隔离粗略下,能够读到未提交事务的数据,也就是会出现脏读的情况。在该隔离等级下,仅解决了更新丢失的情况。
(2)读提交(Read committed)
如果是一个读事务(线程),则允许其他事务读写,如果是写事务将会禁止其他事务访问该行数据。
跟上一个等级比较,很明显的差异就是,在写事务执行的时候,是禁止其他事务访问该数据,即连读取也是不允许的。在该隔离等级情况下,数据的更新将在完成之后才会被读取到,不会出现未执行完成的数据被读取,所以能够避免脏读的情况。但是由于读事务并不应该写事务对同一数据的执行,即在读的过程中,忽然有写事务执行,在写事务执行完成后开始的读事务再次读取同一数据时,仍然会出现不一致的情况。所以不可重复读在该隔离等级还是会出现的。
(3)可重复读(Repeated Read)
可重复读取是指在一个事务内,多次读同一个数据,在这个事务还没结束时,其他事务不能访问该数据(包括了读写)。
和“读提交”相比,“可重复读”采取了更进一步的要求,在该事务还没有结束时,即包括读事务在内,其他事务都不允许对该数据进行读写,这样子就能够有效避免同一事务中两次读取相同数据的差异,即不会发生不可重复读的情况了。
(4)串行化(Serializable)
提供严格的事务隔离,它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。
呼应上文中,对于隔离性最好的解决方案就是,事务的顺序执行,这样子就不会发生对同一数据,甚至表格内容的抢占了。能够避免上文中提到的所有“坏情况的发生”。
总结一下
隔离级别 | 解决情况 | 备注 |
---|---|---|
读未提交(Read uncommitted) | 更新丢失 | 最低等级的隔离策略,不建议在生产环境使用 |
读提交(Read committed) | 更新丢失、脏读 | 最常用的隔离等级,是SQL Server和Oracle的默认隔离级别 |
可重复读(Repeated Read) | 更新丢失、脏读、不可重复读 | MySql的默认隔离级别 |
串行化(Serializable) | 全部解决 | 效率最为底下 |
引用参考:
https://baijiahao.baidu.com/s?id=1611918898724887602&wfr=spider&for=pc