闲话聊聊事务处理(中)

上面提到了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)

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

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏IT大咖说

Sharding-JDBC:分布式微服务数据库访问框架的设计与实现

摘要 当当架构部总监分享分布式微服务数据库访问框架Sharding-JDBC的设计与实现。 ? 互联网领域数据库面临的问题 我们在互联网领域数据库面临的问题主要...

97770
来自专栏腾讯移动品质中心TMQ的专栏

【浅谈Chromium中的设计模式(一)】——Chromium中模块分层和进程模型

“EP”(中文:工程生产力)是目前项目中提升研发能力的一个很重要的衡量指标。笔者重点学习了Chromium产品是如何从代码和设计层面来保证快速高效的工程生产力。...

53980
来自专栏程序你好

一个微服务架构的简单示例

50830
来自专栏杨建荣的学习笔记

Oracle 12C打补丁的简单尝试(r10笔记第55天)

最近在服务器盘点的时候,发现测试环境还是值得整合一下,因为服务器资源老旧,整体配置不高,服务器资源使用率不高,业务要求不高,多个实例分散在多台服务器上,要考虑灾...

39980
来自专栏熊二哥

ASPNET_WEBAPI快速学习02

这部分内容的学习,已经放了大半年时间了,果断补充上,尽早将过去遗留的老技术坑都补上。首先将介绍服务幂等性的概念和相关解决方案,这部分也将是本文的理解难点,由于W...

21860
来自专栏web前端教室

【亲测】前端如何写满你的硬盘?

今天偶然在网上看到一篇文章,说是前端如何机智的搞坏电脑。大意就是通过node搞一个服务,然后以get请求的方式通过localStorage,大量的向用户浏览器缓...

16240
来自专栏杨建荣的学习笔记

巧用外部表备份历史数据(r5笔记第62天)

在很多的系统中,随着时间的推移,都会沉淀大量的历史数据。一般数据量达到一定程度都会考虑使用分区表来处理。根据业务规则,可能有些历史数据隔一段时间就需要做清理了,...

381120
来自专栏我的安全视界观

[一起玩蛇】Python代码审计中的器II

32570
来自专栏张善友的专栏

MongoDB核心贡献者:不是MongoDB不行,而是你不懂!

近期MongoDB在Hack News上是频繁中枪。许多人更是声称恨上了MongoDB,David mytton就在他的博客中揭露了MongoDB许多现存问题。...

257100
来自专栏程序你好

微软放大招?Windows 10 将加入原生虚拟机支持

据外媒报道,微软已经启动内部代号为 “19H1” (也称 Redstone 6)的下一个重大 Windows 10 更新的研发工作。Windows 10 19H...

8720

扫码关注云+社区

领取腾讯云代金券