专栏首页鸿的学习笔记闲话聊聊事务处理(中)

闲话聊聊事务处理(中)

上面提到了multi-object事务,但是要完美的处理multi-object事务并不容易。因为我们必须要面对并发问题导致的bug,而隔离性要求数据系统必须要向使用者把并发问题隐藏起来,让使用者因为只有自己一个人使用。在实践中,这个并不容易做到,完美的隔离性要付出相当大的性能代价,所以大多数的数据库提出了Weak Isolation Level的概念,虽然弱化版的隔离性还是会导致各种潜在的问题,但是这个代价相对于性能的巨大提升是可以接受的。那我们来看看几种不同的Weak Isolation Level。

  1. Read Commited

ReadCommited实际上可以分为两点,一是没有脏读(在你向数据库发送读请求时,你只能读到已经commit的数据),二是没有脏写(在你向数据库写入数据时,你只能覆盖已经commit的数据)。这不是非常严格的隔离了两个并发操作,仅仅只是要求读和写操作只能针对已经commit的数据。如果不解决脏读,对于类似update-after-read的事务,意味着可能会导致不正常的操作,并且如果一个事务失败了,需要roll back,那么发生了脏读的事务该如何处理呢?同样的,如果不解决脏写,两个事务针对同一个object进行了修改,两个事务都没有commit,那么最终结果该选谁?

既然脏读和脏写会出现这些问题,我们的解决办法该是如何呢?最常见的避免脏写的方式是选择一个low-level锁:当事务试图修改一个特定的object时,首先得获得一把锁,直到事务commit或者abort,锁才会被释放。其他的事务想修改这个object,必须等待持有锁的事务释放锁。同样的,脏读也可以通过这样的锁去解决这个问题,不过,显得有些过于杀鸡用牛刀了,更好的办法是采用版本的概念,将数据简单的区分为没有commit和commit了的,最后进行合并。

  1. Repeatable Read

Read Commited避免了事务读写其他事务的中间过程,但是对于并发bug依然存在。比

如,有两张表A和B,A存入了数据500,B存入了数据500,现在A向B转入了100,这样我们需要使得A减去100,B增加100,此时有一个观察者C,在A减去100时,向A发送请求,读到A=400,又同时向B发送请求,问题来了,B增加100的请求在B还没生效,于是读到B=500。在观察者而言,第一次读到的A+B=900,但是过了一会儿,再次发送请求,会发现A+B=1000。这种现象便就是Read Skew。

初看之下,这个似乎没什么大的问题,只要retry,数据便会正常了,但是我们要注意到遇上某些情况,Read Skew这种暂时状态便会无法令人接受了。例如,对于数据备份,我们会备份一个错误的数据,那么数据恢复也就不可靠了,对于数据分析而言,一次性会读入数据然后进行分析,读入错误数据也会导致数据分析失败。

这个问题的解决办法就是snapshot isolation。简单来讲,每个事务的数据读取都来源于数据库的consistent snapshot,换句话说,在每个事务开始前,都会读入数据库里所有已经commit的数据。即使某个数据会被另一个事务修改,每个事务都会载入数据库某个时间节点的老数据。

在前面的脏写使用了锁的概念,而snapshotisolation的核心在于读不会阻塞写,写不会阻塞读。最为出名的实现snapshot isolation的技术就是多版本并发控制(MVCC)。通过区分不同的版本来处理数据。

  1. Preventing Lost Updates

Read Commited和snapshot isolation都只是保证了只读的事务在并发情况下不会出问题,

而写的事务,我们也仅仅时讨论了脏写的情况,而对于并发写的情况,还有一种叫做Lost Update,这个的问题会出现的如下的情况:如果一个事务需要先读取数据,然后修改数据,再将数据写回(read-modify-write cycle),倘若两个事务并发进行,那么其中一个修改需要被放弃,保留一个数据。在现实生活,这个很常见,比如共同编辑同一个文件,给购物车添加东西。

解决办法呢?最简单的办法就是Atomic write,通过排他锁,锁住需要修改的object,直到修改完毕。或者稍微放松点条件,强制使得两个事务有序,让一个事务等待另一个事务执行完毕。如果不使用锁的话,还可以让两个事务同时进行,在一个事务commit时,告诉它这个老数据已经被修改了,这样的操作需要数据系统提供一个检测,识别出并发写正在进行。锁的操作也仅仅限于在只有一个备份的情况,如果数据是多备份的,加锁会严重影响性能,也不好实现,更多的是采取检测并发写和多版本管理,将问题抛出来,由应用端解决,或者retry。

  1. Write Skew and Phantoms

除了上面讨论的情况,我们再看看一些并发bug的情况。想象我们有这么两个事务A和B,由应用端发起的,两个事务模式是一样的,假设有三条数据a,b,c等于1,事务中要保证a,b,c三条数据中至少有两条等于1,A和B都会先select所有数据判断a,b,c有多少等于1,如果发现a,b,c都等于1的话,再将对应的a或b置为0。现在A和B同时发送select语句给了数据系统,发现a,b,c都等于1,A和B便将对应的a,b置为0了,问题来了,前提条件是a,b,c至少有两条等于1,现在因为并发写导致a,b都为0,前提条件失效了。

与前面讨论的不同,这是两个事务修改两个不同的object,这种现象在计算机里被称之为write skew。对于write skew,面临的情况更严重,1.原子写是没用的,因为你需要锁死所有object,不仅仅是一个object,而这对性能的影响,忘了它吧。2.如果使用自动检测并发冲突,需要真正的序列化隔离,而不是简单的判断单个的object。我们又不想锁死所有object,那么该如何解决呢?

首先,让我们分析下write skew的模式,在例子中,我们会注意这个问题的来源于select的判断失误,A和B的select获得了结果,然后应用端基于这个结果做出了判断,再进而对数据做出了修改。我们可以理解为A和B的select出现了“幻读”。Snapshot isolation只是避免了read-only的事务出现问题,而无法解决read-write的事务的问题。

借由4提出的问题,让我们进入到serializability的世界。Serializable Isolation是一种相当严格的隔离。它保证了即使事务出现了并发的情况,也能保证每个事务并不会受到其它事务的影响。说的这么美好,那么我们来看看要实现Serializable Isolation的具体方法。

1.真正的序列化执行

最简单的实现Serializable Isolation的方法就是移除并发,在每个时间段只在一个线程里只执行一个事务。当然听起来并不优雅,但是随着RAM性价比的提升,而且OLTP在每次只会修改少量数据,这个想法在Redis等数据库得到了实现。不过,在单个CPU上的运行,吞吐量终究会是个问题。

Redis等数据库还是很年轻的,那么在更早的数据系统该是如何改善的呢?一个很重要的方法就是存储过程,将一系列的操作包裹在一起执行,再commit。比如上述的write skew问题,可以将所有过程交由数据系统处理,而不用经过应用端的判断,这样的话,可以尽量避免磁盘IO和网络问题。存储过程听起来很美好,本身依然存在很多问题,比如每个数据系统都拥有自己独特的存储过程语言,而不像sql有着标准,并且存储过程运行在数据库之上,不受应用端管辖,那么出了问题也不好解决,最重要的是,数据系统在一般的应用中比应用端更重视性能,存储过程太过于依赖数据系统,性能会更容易遇到瓶颈。

还有一个解决单CPU的吞吐问题方案,那就是将数据分区,不过这是有需要一个协调者去处理分区问题,性能往往比不分区还差。

2.两阶段锁

这是实现Serializable Isolation最为广泛的算法。算法的思想很简单,1.如果事务A向读取一个object,事务B向修改那个object,那么B必须等待A事务commit或者abort才能开始运行。2.如果事务A想修改一个object,事务B想要读取那个object,那么事务B必须等待到A事务commit或者abort才能运行。

在具体的实现中,大部分的数据系统使用了共享锁来处理,每个事务要处理的时候必须要先获得这把锁。在后续的发展也进化出了Predicate锁和Index-range锁来减少两阶段锁带来的性能问题。

并发和性能问题看起来如此难以解决,在08年有人提出了Serializable Snapshot Isolation(SSI),来解决两阶段锁的性能问题。SSI使用了乐观锁和snapshot isolation的结合,在read-write事务中,SSI会先判断write是否过时了,再决定这个事务是否可以commit。由于应用的还不是太广泛,大家可以参考相关的文献来更加深入了解SSI。

本文分享自微信公众号 - 鸿的学习笔记(shujuxuexizhilu),作者:鸿

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-02-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 关于数据库的一些学习笔记

    一、锁、并发 一个很有趣的事实:容易理解的模型性能都不好,性能好的模型都不容易理解。(性能好,这就意味着锁的颗粒度很少,这样就需要更多的细节) 事务单元: 一个...

    哒呵呵
  • Spanner和一致性(待续)

    前几天读了一篇文章[一致性模型](https://www.jianshu.com/p/3673e612cce2),发现自己也有也有一些知识点遗漏了,遂写下此文作...

    哒呵呵
  • 闲话聊聊事务处理(上)

    如前面的一些文章写的,数据系统不可能保证是完全的可靠的,我们会遇上各种各样的问题,比如数据库或者应用突然崩溃,网络连接断了,并发读和并发写,诸如此类...

    哒呵呵
  • 快速学习-声明式事务管理

    大多数情况下声明式事务比编程式事务管理更好:它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。 事务管理代码的固定模式作为一种横切关注点,可以...

    cwl_java
  • Spring 事务(Transaction)

    疫情期间在家重新读了《Spring in Action》,每次翻阅总有一些收获,之后在网上看了一些关于Spring事务管理的文章,感觉都没有讲全,这里就将书上的...

    极客小智
  • 浅析spring声明式事务使用

    springboot中和注解形式的是在@Transactional注解中配置的(添加注解时添加这些):

    开发架构二三事
  • Oracle 事务操作

    在看本文之前,请确保你已经了解了Oracle事务和锁的概念即其作用,不过不了解,请参考数据库事务的一致性和原子性浅析和Oracle TM锁和TX锁 1、提交事务...

    郑小超.
  • 猿蜕变16——一文搞懂Spring事务花式玩法

    看过之前的蜕变系列文章,相信你对事务有了应用方面的认识。但是这些要完成你的蜕变还不够,考虑到大家的基础知识,我们继续回到spring的话题上来,我们一起聊一聊S...

    山旮旯的胖子
  • 数据库事务详解

    事务的产生是为了简化我们的编程模型,使我们在开发的过程中不用考虑各种潜在的错误和并发问题,而不是伴随着数据库系统天生就存在的。

    我的小熊不见了丶
  • 浅谈数据库事务

    原子性是指事务包含的所有操作要么全部成功,要么全部失败。 例小王要向小李转账200元。则账要么转账成功小王账户减200元,小李账户加200元,要么执行失败,两者...

    Java学习录

扫码关注云+社区

领取腾讯云代金券