海量数据-事务一致性2

上次我们聊到,大家对于在数据库中的一致性和在分布式体系中的一致性理解上存在差异,那么这两个看似不同,但又相同的概念,是否能够得到统一呢?

让我来尝试一下吧~

 

要讨论这件事,我们首先需要先忘记一致性和隔离性等概念,先来把问题重新定义一下。

 

之前我们也讲过,数据库底层的核心,是映射(mapping),也就是一大堆的keyvalue

针对这些映射数据,我们其实只能做两件事,要么读取他,要么写入他,

而事务,则是将多个原始映射内的数据,临时的组织在一起,来满足某种业务需要。

举个例子,假设我们有一笔事务,是将Pk=4的账号内的钱转账给pk=5的人。

同时,有另外一笔事务,是将pk=6的人的钱转给pk=5的人,那么在这时候,两个事务单元就共享pk=5这个数据。而这种动态的将pk=6,pk=5,pk=4的数据组织起来来满足某一个功能的过程,就是事务。

 

从逻辑上来看,上面的各类操作其实应该是一个操作,但物理上却因为计算机本身的限制,而不得不分步骤进行。那么,我们就势必需要利用计算机所能提供的工具,来"模拟"成一个操作。

在模拟的过程中,我们会面临三个问题:

 

首先,不是每一步操作都会成功的,如果进行到一半,出现了问题,应该怎么办?

 

其次,因为计算机实际上只是个打字机,他一次只能针对一个映射进行一个操作,然而逻辑上又要让我们的用户看起来像是一个操作,这应该是需要高超的技术。

 

最后,并发性能也是个要考虑的因素。

 

其实,所有的关于原子性,一致性,隔离性,并发效率的相关问题,最后都可以归结到上面这三个问题上,而针对这三个问题的不同权衡点,就形成了各种一致性、隔离性的方案了。

 

下面,让我们深入的做一下分析:

原子性这个问题,我们在原子性的章节已经做过简要分析(后面我再调整顺序吧:)http://blog.sina.com.cn/s/blog_693f08470101rnfk.html

这次我们主要来聊的就是这个一致性和隔离性~

在概述一章,我们已经详细的描述过这台计(da)(zi)机是如何模拟并发的。对应到实际的实现中,其实就是加锁,确保单位时间内只会有一个进程进行操作,就可以确保多步操作不会错乱了~

比如这样:

在每次进行数据读写的时候,都先加锁阻止其他线程的读写,让所有读写都顺序进行就好了。

这样,就可以做到事务隔离级别中的"可序列化"级别哦~

但明显的,这种模式会直接面临我们碰到的第三个问题:并行度上不来,速度会受到影响。

自然的,就需要想办法提高速度,毕竟更高更快更强是我们对系统的不懈追求嘛。

分析一下问题,很容易就会发现,核心的问题是一把大锁让所有请求都会相互冲突。

但很明显,事务单元1与事务单元2之间完全没有冲突,那么我们可以利用锁分离加上读写锁两个优化方式,来提升并行度。

使用这种锁分离加上读写分开的锁实现,能够实现两个不同的隔离级别:1 如果要做到"可重复读"的话,让发生过读的数据维持一把读锁,这样其他的写进程就无法获取该数据的读锁,从而保证了可重复读级别。

但这样还会维持读锁,阻塞了部分写(就像在事务单元1中的读那样),并行度还不能做到最高。

2)如果我们可以再放松一些隔离级别,在读发生的时候不维持读锁,那么我们的并行度就能进一步的得到提升~这就是"读已提交"这个隔离级别。

但代价就是读过的数据再去读,可能会发现数据已经变化了(不可重复读)

 

如果再往前走一步,放弃全部锁,那就是最快,但基本没保证的读未提交级别啦。

看起来挺快,但人类对于更快的追求是无止境的,大牛们还是绞尽脑汁想到了更快的方法:MVCC(多版本并发控制),至今,我还清楚的记得,在《Oracle专家高级编程》这本书里,Oracle的技术副总裁Thomas在书的开始就在炫耀他们的数据库比其他的数据库更快,并行度更好,其原因就是因为Oracle是率先使用MVCC模型进行事务管理的数据库中的一个~

MVCC事务管理模型能够做到的效果如下图:

和我们刚才看到的,使用读写锁的方式相比,一个最大的不同就是读和写是可以完全并行的了。于是并行度就能得到进一步的提升。MVCC模型,也能够实现读未提交和读已提交两个不同的隔离级别。

然而MVCC也有代价,其中最大的代价就是,实现更为复杂,也更容易出错。

同时,因为会记录当时同时进行的所有事务处理中的临时版本,会占用更多的磁盘和内存空间。

不过,瑕不掩瑜,在数据库系统中,锁冲突是性能的最大敌人,因此,谁能解决这个问题,哪怕只是往前走了一小步, 都会是一个巨大的进步。

好啦,介绍完了在传统事务模型里面的一致性和隔离性。我们再来看看在分布式系统中的一致性:

 

在分布式系统中,除了传统的跨机ACID事务之外,人们又发现有了新的课题需要被攻克,这就是多机的数据同步,从映射的角度而言,其实就是将一个映射内的数据,一字不落的复制到另外的一个映射内。

 

同样的这种复制也需要处理上面的三个问题:

  1. 这是两步操作,第一个成功,第二个失败应该怎么办?(原子性问题)

第一个问题,还请参照原子性章节,http://blog.sina.com.cn/s/blog_693f08470101rnfk.html

 

  1. 这两步操作,如何能够让用户看起来像是一步操作?(一致性、隔离性问题),并且性能尽可能好?

    如果对第二个问题进行一下抽象,归并到上面的事务模型中,其实可以认为是一个两个完全冲突的事务单元之间进行事务的场景。

在现实的生产系统中,我也经常能够看到,一些使用Oracle/MSSQLServer的同学,会使用分布式事务同时更新两个库,用这种方式来实现数据复制和高可用,其实就是利用了上面的这种解法。

与这个类似的,还有我们刚刚介绍过的各类索引,其本质就是将原始映射内的数据,复制一份,按照新的维度进行存储。

然而,一般用分布式事务来解决高可用问题的,一般都会发现,整个数据库的TPS下降一半以上等严重的性能问题。这又是因为什么呢?

其实,一切的问题就在"锁"上,原来在单机,加锁操作延迟在纳秒级别,而如果这个加锁操作走了网络,那最少也是毫秒级别,延迟一下增加了这么多,能不慢么?呵呵,而且一定是慢到无法忍受啊。

于是,又得想办法去锁了~hoho,于是请各位随我一起,进行一下去锁分析吧:

很容易想到的一个优化的点是:并非所有的读写都需要用到两个集合内的数据,比如,如果我保证默认只读一个主集合的数据,那么备份集合的数据就完全不需要读取,既然他不需要被读取和写入,那么就不用在备份数据上维持额外的锁了。这样,效率不就又恢复了?你看咱多聪明~~~这小伙子有前途啊~

 

可惜,这种思路是不成立的,因为之所以要进行额外一份的数据写入,其核心目的就是为了保证当一台机器挂掉的时候,另外的一台机器能够快速顶上不是?

然而我们无法预知何时需要另外一台机器顶上,因此,我们只能假定任何时刻都会出现。。于是,前面的假设就不成立了。

 

既然锁去不掉,范围也无法缩小,那我们该怎么办呢?

哎,只有放弃锁这一条路了。。放弃锁或缩小锁的范围,最直接的代价就是用户会看到脏数据,比如某些已经在主映射完成的更新,还没有来得及在备份映射内写入,这时候用户去读备份映射,就会读到脏数据了。

这种会存在读到脏数据可能的一致性/隔离性选择,我们就管他叫"最终一致性"

呵呵,其实会发现,这最终一致性,是一种比"读已提交"隔离级别更低的隔离级别。

然而,在造新词儿的时候,也不知道为啥(或许是觉得隔离性太难理解了吧XD),于是就放弃了扩展隔离性,而选择了扩展一致性这个选项,于是就形成了"最终一致性"这个名词。

然而从本质而言,问题产生的核心就是是否加锁,锁所影响的范围,以及锁的读写限制。

所以,我认为,所谓一致性和隔离性,无非都是针对上述核心问题的几个方便理解的展现形态而已。

而从一致性的定义而言,我认为是清晰而明确的,其核心就是针对多个临时或永久的数据集,进行多次跨数据集的读写操作,他能够保证以固定顺序发送到这组事务集内的读写操作的Happen-before关系(例如,如果针对某个数据的写先于读请求被提交,那么后发的读取就一定能够读取到先发写入所写的那行数据)

这也就是为什么,我认为一致性的核心是"看"的原因了:)

 

而我个人之所以倾向于放弃"隔离性"这个定义,而建议重新以一致性这个领域来理解这个问题。主要的原因就在于我认为隔离性是对一致性定义的一种破坏,从整体而言,体系显得不够漂亮,而且,在新的分布式场景下,人们也已经针对一致性这个问题进行了多次的扩展,比如因果一致性,最终一致性,读自己所写一致性等。

如果全部选择扩展一致性,显得清晰和漂亮了很多啊。

 

Anyway , 这只是概念的争夺,如果想清晰的理解一致性、隔离性和并行度之间的关系,最简单的方式,还是看看我这篇文章即可。哈哈哈

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值