zbcaicai 2019-10-21
我曾不止一次听说过这句话:
“十个女人无法在一个月内生出孩子”
我明白这句话的意思,用来形容我们的开发工作需要循序渐进,没有办法简单的增加人员就能加快研发速度。
这句话也经常被用于反驳产品经理或者老板,试图让他们明白我们内心所表达的观点,老实说我也说过这样的话,当时还觉得挺有道理,现在想来可能有些一厢情愿了。
没错,在现实世界中,当然不可能在一个月内生出孩子,但我们毕竟是做产品写代码的,而不是真的要去生孩子,所以这种说法未免有点偷换概念。
我并不是较真,如果只是想让产品经理明白我们所要表达的观点,我们完全可以用其他的比喻,如实反馈存在的困难与问题即可。
言归正传,这句话与本文有什么关系呢?本文想要就“并发”所带来的问题进行探讨,相信看完后你会对此有一个感觉。
与我之前写的几篇文章一样,并发一词在本文中所表达的意思是:
“在分布式环境下,超过一个线程同时对同一个状态进行访问和变更所导致的一致性问题和可用性问题”
我无法给出一个百分比数据用以说明到底有多少后端应用程序在使用数据库,但我想国内涉及到增删查改之类的各种“管理系统”应该不在少数。
说到底,增删改查是落地,而怎么落地则取决于业务的需要,也就是说,业务规则以及流程表达了我们的逻辑,但终究离不开柴米油盐(增删改查)。
那么什么是状态?
它可以是文件,也可以是数据库,可以是一个变量,也可以是缓存,它代表了计算的结果或者依赖(中间结果),由于它是可变的,并且可以被超过1个以上的程序同时访问或者修改。
所以由此产生了两个问题:
一致性要求是必须的,无法满足一致性的情况要么是业务逻辑本身有问题,要么就是我们在编码过程中出现了BUG,而如果是我们的编码出现问题,很明显就不符合验收标准。
可用性要求则取决于运营的实际情况,随着系统使用规模的上升,我们需要保证系统始终处于用的状态下,因为业务方不希望服务被中断或者超时。
我来描述一下完整的逻辑:
接下来,我会分别就状态的一致性和可用性进行讨论。
在上一节中,我们说状态一致证明我们最终落地的数据是正确的,符合业务逻辑的。不一致是由于我们对业务的理解或者编码出现了BUG,而BUG是我们必须要解决的。
这里有两个层面的问题,我们分别来看一下。
如果在业务逻辑的层面上本身就存在悖论,存在漏洞,经验丰富的开发人员会在进行系统设计或者编码的时候就能察觉出来,道理很简单,因为无法被实现,不管怎么样都会存在BUG,而这种BUG是我们技术人员无法解决的,我们不能够去猜业务方到底想要什么,因为这极有可能不符合他们的期望,最后仍然有可能会导致返工,造成成本的上升。
就像我们拿本文开头的那个例子去怼产品经理一样,很多时候是由于我们不善表达,没有清楚表达出我们的疑惑,从而造成尴尬的场面。
沟通和管理是困难的,众口难调。如何跟业务人员进行高效的沟通一直以来都是一个难题,但我们必须要清楚一点的是:
“只有我们充分洞悉和理解所要实现的业务领域,才能够使我们更加轻松和增强信心,**因为只有这样,我们才能够选择最适合的技术和模型帮助我们灵活的完成任务。”**
我的意思并不是让咱们都成为该领域的专家,因为术业有专攻,分工配合才是重中之重,如果我们没有跟业务需求方达成紧密的一致,就有可能造成浪费。
有关这一块的内容,建议大家看一本书,叫做《领域驱动设计》,英文名是《Domain Driven Design》,简称DDD。
技术层面出现不一致的问题一般有两种情况:
如果是第一种,这个没什么好说的,返回业务层面与业务专家进行真诚层面的沟通,获取真正的需求。
如果是第二种,那么我们首先应该找出不一致的原因,分析不一致的原因有助于我们加深理解和避免此类问题。
如果是由于小失误造成的问题我们直接修复即可,这其实很常见,我们写的代码或多或少都要经过测试与修复。
如果是由于并发竞争造成的问题,那我们就需要用到相关的解决方案了,最常见的是使用数据库事务来保证状态落地的时候不会产生不一致,因为不一致会导致事务的回滚。
还有就是使用锁来限制资源的访问以及修改,这些都是很常见的技术,鉴于本文的重点是想说明产生这些问题的原因,所以不会这些解决方案进行详细的讲述,有兴趣的朋友可以翻看一下我之前的文章或者查阅相关资料文档。
可用性往往决定了系统架构的实现方式,可用性导致了我们最终将不得不使用分布式集群来应对大规模的访问需求。
可以说,产生并发问题的直接原因就是可用性,因为它让我们对状态的管理变得十分复杂。
如果没有可用性要求,最简单的我们甚至可能都不需要数据库,但现实中,对于一款成功的产品,我们不可能告诉我们的老板咱们无法实现对不对?
“量变导致质变,当可用性要求越来越高,系统规模越来越大,即便是再简单的增删改查都将不会再简单”
如何在提升可用性的同时还能保证状态的一致性?这真的不是技术能够解决的。
先冷静一下,我的意思是,由于网络分区的存在,状态的强一致会导致可用性降低,而可用性的提高又会造成分区状态的不一致,从而降低了一致性。
这就是著名CAP定理[1],我们要么取CP,降低系统的可用性,要么取AP,降低状态的一致性。
那么我们有没有什么办法来达到一个比较好的平衡呢?
答案是当然可以,但是,就如我一开始所说的,这并不是技术一个人就能够解决的问题。
事实已经很明显了,我们不可能取CP来降低系统的可用性,那样就没得玩了,所以我们只能够选择AP。
“在业务可以允许范围内,设计一种最终一致的中间流程步骤,来提高系统的可用性,同时,又得以让业务可以正常不受影响,处于预期的运转。”
因此基于BASE理论[2][3]的最终一致更贴近于现实与业务,CAP定理只是证明和告诉我们,哪些事情行不通,但是BASE理论告诉我们,上有政策下有对策,使用柔性事务,反脆弱的系统才能让我们更加的灵活。
所以我们要怎么做呢?我们要告诉产品经理,系统的可用性瓶颈必须要更改业务的流程才能得以实现。
我们要学会表达,告诉产品经理,这不是能不能做的问题,或者我能不能行的问题。
而是计算机科学目前的发展水平就是如此,也会存在极限,我们必须要学会相互适应,才能保证健康的发展。
尽量避免使用分布式事务,这是由衷的建议,事实证明分布式事务不仅不会提高性能,反而会拖垮高可用场景的系统。
如果你遇到了这个问题,那么说明状态分区隔离以及事务场景有可能存在不合理的地方。
如果你确实有这种需求,那么尽量避免使用分布式事务,或者将分布式改为本地事务,也就是说不要将它们放到不同的地方进行事务处理。
如果这样也不行的话,那就必须让事务这个概念被明确包含进业务范围,作为一个独立的实体存在,不要让它隐藏在技术细节中。
这样的话,我们就能够对这个定义明确的事务“句柄”或者“钩子”进行补偿或者回滚等最终一致幂等操作。
这也是我一直强调表达的,不要仅仅从技术层面来看待分布式事务,它可能是一个潜在的业务需求,存在生命周期的潜在概念。
就像我们对接支付宝一样,根据订单号来确保幂等,返回明确的信息给支付宝表明处理成功或失败,实现最终一致的交易处理。
这个问题要从两个方面来看,如果并行运行的程序没有相互依赖,没有状态、资源竞争,那么水平拓展是非常容易的。
比如MapReduce,将大规模的数据分发并行处理,最后并归计算结果,速度肯定快于串行执行。
相反,如果状态特征在进行并归的时候,前后依赖就产生了耦合,并行处理导致的切换加载开销就变得没有意义。
什么意思呢?
比如将一个个事件进行存储,因为状态本质上来讲就是就是一系列事件施加计算后的快照而已(详见https://www.cnblogs.com/xingxueliao/p/11561263.html#event_and_state)
所以如果我们想要计算某一个时间点上的快照,就需要从头将事件播放到指定的位置上,事件对状态的影响是严格按照先后顺序来的。
因为事件导致的状态结果是前后依赖的关系,因此并行运算并不会得到什么帮助,反而会因为切换导致无必要的状态加载以及卸载开销。
这种情况下,串行处理是唯一的方式,我们应该在这个上下文中,对输入的事件流进行持久批量的计算,而且不用考虑并发所带来的一致性问题。
因此,盲目的使用多线程或者多实例集群不仅不会让可用性得以提升,反倒因为竞争的关系导致可用性被降低。
对数据特征进行分析,如何隔离状态才是我们需要关注的重点,因为只有将没有依赖关系的状态隔离后我们才有可能提高整个系统的可用性。
这就完了?好像你没有提到任何关于如何解决这些问题“具体方案”。
没有具体的方案,因为这超出了本文的范畴,本文只是简单的讲述问题产生的原因,试图以比较清晰的一个视角来发现和看待问题是为什么存在的。