数据访问与管理技术的演进
数据访问和管理是软件设计需要解决的一项非常关键而又基础的问题;从早期的大型Unix应用开始,到基于C/S架构的商业应用, 乃至在互联网大潮中取得压倒性优势的基于B/S架构的企业应用,对于如何管理、访问、保存、检索、备份、维护数据这一基本问题, 无数先辈们创造了丰富多样的技术选项,然而随着行业潮流的变换,不同的技术依然在它各自适应的领域闪耀这光芒。
数据访问、管理技术的关键要素
到目前为止,几乎所有的软件依然是工作在冯诺依曼设定的计算机架构之上,因而从最微观的粒度看,运算器、控制器、存储设备互相配合完成形形色色的任务。 各色复杂的软件都要在此基础上做更高层次的抽象和封装,才能完成复杂度更高的任务。程序语言、编译器和各种各样的库函数、中间件等则都围绕着该基本模型来处理高层次的领域相关问题。
由于最底层的硬件要么具有访问速度快但易失性的特点,要么是访问速度慢但可以持续保存很长时间;同时硬件在长时间工作的情况下总会面临各种各样的失败。 面向业务问题的计算机软件必须要被仔细设计以隔离上述问题,使得用户觉得失败永远不可能发生(或者至少是现实意义上的不会失败)。 这对数据的访问和管理技术提出了很高的挑战
- 数据容量在需要的情况下能尽可能地大,甚至是可以按需动态扩展;传统上基于单机的数据存储模型被扩展到可以被存储在地理上分散的网络节点
- 数据访问的延时需要满足特定的场景需求;由于数据可能被存放在不同的设备上,不同的存储设备有不同的访问性能和时延特性
- 多用户同时访问的一致性:复杂的应用总会允许有逻辑上同时访问数据的需求,如果是访问同样的数据或者相互有逻辑关联的数据,用户不应该得到不一致的数据状态
- 数据的可靠性:在任何硬件失效的情况下,数据的丢失总是应该被尽量杜绝的
这些目标、挑战有些是互相矛盾的,因此在现有的技术条件下并没有出现过一种一招通吃的方案,各色各样的技术方案都会在某些方面有所取舍。 只是在同样的取舍考虑下,优秀的技术能比它的竞争对手们做的更为出色。
前“关系数据库”时期的数据结构和文件方案
在20世纪70年代关系数据库理论出现之前,软件已经被用于解决很多形形色色的问题了, 这个时期其实并没有专门针对数据管理的特定技术。
基于操作系统和基本库的思路
需要管理和访问数据的时候,最基本的方案就是依赖操作系统和编程语言提供的基础设施了。 这种场景下数据的访问基本是没有很强的结构化特性的,软件的规模也不是太大,因而直接依赖于底层的机制也算够用了。
数据组织上基本以常用的数据结构为主,从最简单的顺序数组,到稍微复杂一点的链表结构,乃至树形结构、跳表、哈希表等依赖于计算机内部存储(处理器寄存器乃至内存)的高效数据机构。这些数据组织方式特别适合于规模不大的数据管理; 其优点是数据的访问和存取都比较高效,然而其限制也比较明显,数据量比较大的时候,就必须找数据持久化存储的方法。
一种最简单的方式是将数据用文件的方式写入到外部存储设备中;然后在需要的时候在从外部设备加载回来。 数据量超过内部存储容量(或者超出预先分配的空间)的时候,可以用选择性的方式将某些数据换回到外部设备中。
问题和挑战
带来的挑战是如何选择合适的算法来确定什么时候需要将数据保存到外部存储设备,或者何时需要将数据加载到内存中。 另外一个难题是如果存储设备出错了,应该怎样应对这些错误?对操作系统和通用的库函数来说,这可是应用程序自己的责任。 由于应用程序的处理逻辑和数据管理、控制逻辑是放在一起的,这些异常处理和策略选择会使软件的逻辑复杂度大大提高。
数据抽象能力和校验
因为操作系统和函数库提供了非常基本的抽象,数据结构之上所承载的数据格式定义,以及数据内容之间的相互关联关系都需要应用程序自己负责维护。 当有大量类似关系的数据需要在多个地方处理和保存的时候,就会出现明显的代码重复并引入额外的维护想难题。
一个可行的方案是当程序的规模变得比较大的时候,提供统一的应用层封装库最为基础设施给开发人员使用; 不足之处是维护这样的通用基础设施的技术复杂度比一般的上层程序逻辑要复杂,需要考虑到很多负责的异常情况并有很强的适应性以匹配各种期望的场景。 还有一种考虑是可以购买第三方成熟的中间件系统以减小维护成本;不利之处是系统出了问题的时候,仍然需要对所使用的软件有很深入的了解才能快速解决复杂问题; 尤其当实际场景对安全性或者法律合规方面有比较打顾虑的时候,第三方软件的成熟度可能成为一个极大的问题, 有时候如果这些第三方软件的设计缺陷没有被及早发现的话,后果可能更加难以承受。
存储设备特性和性能考量
另外一个挑战是当数据容量变得特别巨大的时候,不可避免的需要将存储设备放在不同的物理机器上, 然后通过网络通信协议的方式(比如NFS)将远端机器上的文件系统挂载在本地机器上;使本地机器以为它是在控制“本地”的文件; 而微观上所有的操作都是通过一个网络通信通道(socket)加上对应的控制协议和数据搬运协议来完成。
从寄存器到内存到磁盘再到网络磁盘,数据的容量是越来越大,然而其性能和延迟也急速下降;对CPU寄存器的访问可能几个时钟周期就可以完成(虽然其容量很小), 对内存的一次读写需要的时间就会达到几十乃至数百倍,访问本地磁盘的延时往往是毫秒级甚至几十毫秒;当文件被用NFS的方式映射到本地的时候, 一次访问的时间可能在数十毫秒乃至数秒;因为网络层的封装可能是很厚重的,往往一个简单的操作可能后台需要好几次消息的来回才能完成。 如果网络出现拥堵或者中间设备出现故障,系统行为将变得更加难以预料。
这些时间上的开销给软件开发带来了诸多挑战 - 长时间的延时(要么是库函数调用要么是系统调用)导致同步调用变得效率底下,甚至抵消了多线程编程带来的便利。 由于太多阻塞的操作导致处理器使用率底下,更高的吞吐率难以达到,我们不得不借助于异步编程技术来分割应用程序的逻辑。
编程模型和协作的挑战
可惜的是,因为IO行为的差异引发的编程模型的转化是的软件的可维护行大大降低。 试想一个简单的业务逻辑因为需要访问外部存储设备而被分割成2个任务,丢在2个不同的线程调度器里,通过IO完成的callback事件关联在一起; 这比基于内存数据的顺序处理逻辑复杂多了。
虽然函数式编程模型或者Reactor模型可以应对这一设计挑战,但是仅仅因为数据访问的原因而变更熟悉的编程模型并不是在很多场景下都可以被轻易接受; 毕竟软件开发是依赖于人的集体智力活动。
对软件设计和架构的影响
这些非功能的属性会极大影响到应用程序的逻辑设计;软件设计人员不得不在开发的早期阶段谨慎选择,以免掉入性能不足的漩涡最后不得不重写代码。 本来软件的抽象是为了解决应用领域的复杂问题,隔离底层的细节,不幸的是当底层细节的不同影响到了程序整体的行为的时候,这种关注点分离的方式就很难凑效。 尽管有各种各样的限制,在很多复杂度不是很高的场景下,基于操作系统和基础设施的数据管理方案仍然是最直观且最优的选择。 只要切记在合适的时机依据需求的变化切换到更合适的方案上来即可;要么是变更的成本不大可以直接修改代码完成之,要么是一开始的时候设计好数据访问的隔离, 才与适配器的模式隔离数据访问部分。
基于严格结构化的关系数据库技术(SQL)
关系数据库技术自从上个世纪70~80年代诞生以来便得到了业界的青睐;其清晰的模型和严格的数学理论论证深刻地征服了各个主要的IT厂商, 其采用专门的属于来规范数据的定义和各种功能、非功能属性,学院式的精心设计预先考虑到了各种各样可能出现的问题。
基本机制和思路
关系数据库理论采用结构化的方式定义数据,严格的区分数据定义、处理的范围和边界。 数据库管理系统作为专门的软件系统隔离了数据的定义、操作;应用程序通过预先定义的接口JDBC/ODBC等接口访问数据库系统。
数据定义
数据的结构抽象用schema来表述,逻辑上所有的数据都是满足给定schema的二维表,每一行是一条记录;所有的记录都具有相同的列。 每一行数据的相同列具有相同的定义和约束;每一行数据中的某些列作为访问的关键列(称为键)。这些列有些可以为空有些则必须不能为空。 对数据的访问则以行为单位,可以进行增加、删除、修改等变更性操作和根据某些条件进行查询。 数据的完整性校验可以通过在数据发生变更的时候依靠数据库系统软件的自动检查来完成。
数据表之间可以有丰富多样的相互关联(通过主键、外键设置),从而支持在查询的时候跨越多个表进行复杂的检索。 为了便于应用逻辑处理,关系数据库系统还提供了虚拟的表(称之为视图),这些虚拟的表结构可以基于底层实际的”物理记录表”信息的变化而自动更新; 触发器则支持设置类似于应用程序指定的“自动操作”,当对应的数据被更新的时候,触发器会被自动运行。
通常情况下,复杂的业务数据可以依据不同的范围来隔离,可以放在不同的数据库中,也可以放在同一个数据库的不同数据表中。 多个数据表之间的逻辑关联关系用关系代数来描述,一般不同的数据表之间有相对的关联, 比如某个表的某一列也出现在另外一个表中作为另外一个表的主键,这样查询的时候,可以将两个表联合起来提供单个表不能提供的丰富信息。
数据表之间的关系用数据库范式来描述,严格的关系数据库理论定义了多种关系范式,用以描述信息的冗余度、结构化程度;不同的范式可以大概表征数据一致性的维护难度。 复杂的范式往往也会带来性能开销和处理复杂度,因此商业的应用程序中一般出于性能考虑不会采用很高的范式。
数据查询和SQL语言
关系数据库系统一个最引人注目的特性是提供了一个关于数据定义的结构化模型和接近于自然语言的领域特定语言SQL,并将其和关系代数理论紧密结合在一起。 SQL语言采用了类似于英语的语法,以数据查询为中心同时兼顾增加、删除和修改操作, 并支持对数据集的聚集、归并、分类、排序等常见的操作,实现了对数据管理和访问的细节隔离,使得应用程序仅仅需要关注于数据操作的意图即可。
SQL是面向数据操纵和访问用户的接口语言,数据库管理系统可以优化用户提供的SQL查询,转换成性能最好的内部实现。 这也是DSL的好处,整个数据库管理系统通过精心定义的查询语言将接口和实现分离开;应用程序可以专注于业务逻辑的实现,而数据库关系系统可以专注于数据的管理和维护。 这也是计算机科学一直采用的基本的分而治之的思路-分割问题领域,然后在各个击破之。 从这个意义上来说,关系数据库系统提供了良好的抽象和职责分离。
查询优化和索引
由于商业数据库的数据往往数据量非常大,可能被保存很长时间,因此大部分时候,绝大部分数据都是被保存在磁盘中(甚至是网络磁盘)并在需要的时候读取进来加载进内存中给用户使用。大部分时候,用户的查询操作比修改操作要多得多,因此查询的性能是至关重要的。
虽然数据很多,但是某个时候用户需要访问的可能只是一小部分数据,怎样快速地检索到对应的数据并提供给用户就变得至关重要。 关系数据库系统一般是通过在数据表上建立索引表来实现的,同时基于成熟的B+数数据结构,保证经过少数几次的磁盘IO读写操作就可以定位到具体的数据记录存储快,然后将对应的块读出操作系统内存中去。 这也潜在的要求数据库系统组织数据存储的时候,需要按照记录块的方式将其存储在外部设备中;并最好和操作系统的虚拟内存所支持的页面大小保持对齐,尽可能减少磁盘的读写次数和寻道时间。
当数据量特别大以至于简单的索引表也无法加载进内存的时候,可能需要建立多级索引结构,用一个新的结构指向某一部分数据的索引表在磁盘上存储的位置;而只是将最顶层的索引表放在内存中。 查询的时候,则需要先通过一级索引找到对应小范围数据记录对应的索引的存储位置,通过一次额外的磁盘操作加载二级索引,然后通过二级索引的数检索操作,找到对应的数据块的物理位置。
实际情况下,往往通过少数几次的IO操作(通过B+树的算法保证)便可定位到实际的数据并将其加载入内存中,减少慢速IO对性能的不利影响。
事务及ACID属性
作为数据的访问接口,SQL定义了复杂的查询接口以适应复杂的数据访问、管理需求;然而深入到最底层,细粒度的数据访问仍然要落实到二维数据表的某一个行记录上。 由于这些单一的行是操作的最小粒度,数据库处理系统必须要保证对记录的访问在外部用户看来总是处于合理的状态。
譬如一个用户打算修改某记录的一个列值,而另外一个用户可能同时打算删除该记录,那么两个用户可能是并行在使用数据库,数据库系统必须保证两者的操作返回之后,数据库系统人然处于一个自然的状态; 要么某个用户操作成功返回修改生效,另外一个用户得到失败的反馈,要么2者都失败需要某一方尝试重试,要么数据库系统在内部实现某种机制保证至少一个成功一个失败。 当实际操作的SQL语句牵扯到多个数据表操作的时候,一致性保证将变得更为困难。因为从并发控制的角度来看,只要牵扯到同一个数据的读写,既要保证读到的数据不能是中间状态,也要保证写入之后数据库中的状态需要恰好是刚刚写入的值。
关系数据库系统用事务作为数据操作的基本单元的抽象,一个事务用来封装一次对数据库的操作;关系数据库系统用内部机制保证事物操作满足四个基本的属性
- 原子性(Atomicy):事务操作必须像是在一个最基本的操作中完成的,执行过程中不会被另外一个打断
- 一致性(Consistency):一个事务被提交执行前后,数据库系统中所有的相关数据仍然处于一致性的状态,要么数据被按照事务期望的方式被修改(事务被执行),要么完全保持原样(事务撤销)
- 隔离性(Isolation):各个事务的执行和调度对应用程序是不可见的;从事务提交者的角度来看,好像就他自己在使用数据库系统
- 持久性(Durablility):事务操作的结果是持久有效的,即使是发生了故障或者重启,已经发生的修改必须被永久保存下去;不允许数据丢失情况的发生。
ACID是关系数据库系统的基本特征,甚至于不支持ACID特性的数据库软件都不被认为是关系数据库软件;它被各大数据库厂商广泛地支持和实现。
问题和挑战
ACID里边最为关键和复杂的就是一致性和持久性,尤其是当数据库系统的访问可能来自与网络的时候,由于延时的增加以及不可预期的复杂事务可能被远程调度执行,如何高效、公平地实现事务的一致性控制,又不引起性能的急剧恶化是个巨大的挑战。 从数据持久性角度来看,数据库系统作为一个应用软件本身是运行在操作系统的用户空间,而真正将数据的更新落地到非易失性存储设备需要经历上层的IO调度到操作系统内核空间的控制和数据搬运,操作系统内核的实现和磁盘驱动的读写等等一系列异步操作, 数据库系统需要始终保证任何一个环节出错,数据总是能恢复到正确的状态。
为了实现这些复杂特性,很多数据库厂商(譬如Oracle)会通过修改操作系统驱动甚至内核等方式在可用性和性能之间进行折中。 同时为了提高可用性并兼顾性能,某些数据库系统会要求有非常好的单机性能,使用具有高级特性的磁盘存储设备等。
和面行对象软件系统的集成难题
关系数据库理论早于面向对象技术出现,然而软件开发领域内面向对象技术则在80~90年代迅速席卷了行业的大部分角落。 应用程序上层用面向对象技术写成的特点注定了当它需要去访问数据的时候,中间提供隔离的层次需要协调两者模型上的不一致: 面向对象技术组织数据操作的方式是通过继承、封装和多态来提供抽象能力,而数据库技术的组合方式是基于SQL查询。
ORM技术被发明出来用以实现传统的企业应用架构中的对象和关系模型的映射,虽然有一些复杂的框架技术来缓解这一难题,总归是显得复杂而臃肿。 好在Java语言用类型注解的方式提供了相对优雅的解决方案,从而通过少数几行代码就可以注入约束和检查,大大减小了数据访问层的负担。
扩展性的挑战
由于商业数据库有很大的数据需要存储和使用,当系统需要动态扩展以支持更多的业务量的时候,单实例、中心化的数据库系统往往能为扩展性的瓶颈。
维护数据一致性的代价越来越大,因为分布式数据库系统的扩展和优化需要面对CAP理论的限制:要么保证可用性牺牲一致性,要么保证一致性牺牲可用性。 当有2个并发的用户访问数据库系统的时候,既保证数据的一致性,又保存高度的可用性会成为巨大的挑战,即如下的情况,在没有异常发生的时候N1的更新很容易通过网络同步、复制的方式被N2看到
当网络发生异常的时候,就会出现不一致
网络分割是无法妥协的,因为没有足够强大的机器来维持足够好的单机器性能;分布式应用基本无法在实际中妥协了。
No set of failures less than total network failure is allowed to cause the system to respond incorrectly.
类似于两阶段提交算法等分布式事务(2PC)处理机制从理论的角度看起来很美好,实际应用中却因为性能过于低下而被束之高阁。
成本和投入产出比也是需要考虑的显示因素,因为传统的关系数据库系统需要强大的专有硬件(如磁盘RAID、EMC专业存储方案)来保证较高的可靠性和可用性。 在互联网技术日益发展深入的背景下,企业需要尽量控制IT支撑环境的成本,并快速部署给用户,并需要技术架构和实现能随着业务量的增加而快速演进,传统的将数据库作为独立中间件部署的方式因为太厚重而慢慢被边缘化了。
非结构化数据和NoSQL技术
21世纪头十年互联网技术的严谨为提非结构化数据(即键值数据库系统)蓬勃发展的土壤。从关系数据库理论的角度来看,没有提供ACID支持的数据库甚至都不叫数据库系统; 然而新世纪IT技术对越来越多行业的深入改造带来了数量巨大的数据;而很多数据并不是天然具有很强的结构化特性;将他们强赛进关系数据库系统中会带来臃肿的设计、复杂的模型以及地下的性能。
基本思路
NoSQL技术从简化数据管理的角度来解决这一难题:一方面我们有大量的数据需要存储,这些数据之间没有非常强的相互关联;另外一方面对这些数据的访问可能有很高的并发需求和扩展性需求。 既然关系数据库提供的东西远远多于实际需要的,何不将这些复杂的功能向上移动到应用程序问题域内? 这样数据库系统仅仅需要管理没太多逻辑结构的数据,并提供足够好的并发访问性能、可扩展性、可靠性;而将怎样使用数据交给应用程序去处理? 毕竟最了解数据使用场景的还是上层应用程序。
数据管理特性和元数据
NoSQL其实不是一类技术的统称,而是不采用强结构化形式管理数据的一大堆数据管理技术的集合。 不同的方案提供不同的技术实现,并呈现出不同的特性;从简单的基于纯粹Key-Value对的大数据集合到能够提供一些稍微复杂一点的如关联映射、集合类型的多类型数据管理系统3(典型如Redis), 乃至提供一些比较复杂的面向文档的数据结构(相比SQL可以认为是半结构化)以及较强检索能力的中间型数据库(典型的如MongoDB提供了复杂的json互操作特性);总体上可谓是百花齐放,各有千秋。
亮点
大部分NoSQL技术都提供了很高的性能和强大的集群管理能力,其处理能力想对于传统的关系数据库有很大的提升。 之所以如此,是因为它从技术上打破了传统SQL要求的复杂限制,专注于自身特定的数据定义模型和特点,有针对性地最大化使用计算机硬件。
同时大部分NoSQL技术设计的时候就考虑到了可能出错的情况,不再假设底层的硬件本身具有很高的性能,而是通过自身软件的设计达到较高的可用性和可靠度。譬如Redis本身常常被作为缓存系统使用,而其自身的持久化机制在合理配置的情况下,可以保证数据在1秒内内一定会被写入磁盘,从而尽可能减少数据丢失。
NoSQL的折中
基于纯粹的KV关系的数据库系统对数据的具体结构接近与一无所知;从应用程序的角度看,它似乎仅仅是提供了数据操作层面的虚拟关联表。 其好处是很容易达到很高的性能,但不足之处是,应用程序如何合理地管理键的模式是一个比较打的挑战。
提供复杂结构的NoSQL的好处是应用层程序的负担比较小,因为一些常见的结构化操作已经有了不错的支持,可以直接调用即可。 美中不足的是,这样的方案的性能往往也不是那么好,优化起来也会比较困难,因为数据的存储管理也相对比较复杂,如MongoDB采用树结构来保存数据,不深入其底层实现做针对性的优化就比较困难一些。
挑战
NoSQL自然也不是银弹,它也有它自己的问题需要应对;尤其是当它的用户企图将具有很强结构化特征的数据塞进去的时候:应用程序用来维护这些数据之间的关联关系就会变得异常复杂。
另外一个比较麻烦的事情是NoSQL的不同提供者具有不同的特性,应用程序想从一种切换到另外一种就远远没有传统的SQL来的方便;很可能你需要某个数据库的高级特性在另外一个数据库中就不存在了。 这也是缺乏类似于SQL语言这类DSL的不足;比如Redis定义了类似通配符的模糊查询键值的方式,并提供了类似于一个Key关联多个属性的数据控制原语;换到MongoDB上我们就需要不同的方式来解决类似的问题。
这些复杂的数据操作可能在应用程序层面来看又比较常见和繁琐,以至于使用纯粹的NoSQL会让人产生退回到SQL出现之前的混沌时代的错觉!这当然也不是NoSQL的错,因为从来就没有银弹;合适的方案才是最好的;而怎样决定什么样的方案才是最好的? 当然是需要一句具体的业务场景来确定了。
BASE理论
NoSQL解决数据一致性和高可用的CAP矛盾的方案是所谓的BASE方法,它常常被错误的想象为是ACID的对立面
- 基本可用而不是高可用(Base Availability) 强调数据库系统并不总是返回高度可用的数据,某些情况下可能出错或者不返回,或者返回不一致的数据;应用程序比系显式地处理这些情况
- 弱状态或不稳定状态(Soft state)表明数据的状态可能在不断发生变化,以至于传统关系数据库的这种事务回滚或者提交的状态则不复存在。这些都是为了应对最终一致性的目标。
- 最终一致性约束(Evenutal consistency)保证数据的更新经过一段时间的处理在没有后续输入的情况下可以达到一致状态;但是中间的不一致性状态仍然对应用程序是可见的。
基本上这些特性都是为最终一致性服务的;数据库系统不再强制保证很强的一致性,而是由应用程序本身负责中间不一致状态的处理。 好处是性能可以得到极大提高,因为数据库系统不需要采用很复杂的策略来保证每一项数据的高度可靠和一致,没有复杂的同步、锁策略,开销自然少; 不利之处是应用程序需要仔细处理失败的情况,这在很多时候也是一个公平的折中,因为最了解数据特性的毕竟是上层程序本身。
微服务架构下的数据演进和NewSQL
随着云计算技术的深入发展和容器技术的广泛应用,微服务风格的架构技术变得愈发流行起来。 按照微服务设计的思路,传统的分层、单体、集中式的架构方式被分布式去中心化、按照业务领域分隔以及轻量级的接口为特征的架构方式所取代。 同时鉴于以上这些NoSQL技术的种种不足并不能一招通吃,传统的SQL技术又被人们重新重视起来使用在合适的场景中; 在一个一个具体的服务内部,SQL所定义的查询语言作为完美的数据操作接口仍然有极大的魅力,我们依然可以继续享用它而不需要因为复杂的数据操作而增加应用程序的逻辑复杂度。
最显著的不同在于,服务是较为细粒度的、以业务逻辑功能为核心的一个一个小的独立的进程。数据也会根据此思路进行分割管理,而不再统一的放在一个大的数据库中。 从整个系统的角度看,数据的交换和同步是通过服务之间的协议接口进行的,因而这些协议要求必须是轻量级的,以减少数据序列化、反序列化的开销。 因为微服务的高可用是通过同时运行多个副本外加负载均衡等分布式技术来实现的,服务之间的耦合必须是很松散的;分布式一致性的问题采用的依然是最终一致性的思路。
这种新的结合了传统SQL接口来管理本地数据,用部分NoSQL思路来处理系统一致性的方式又被成为NewSQL,甚至于NoSQL又被重新宣称为是Not Only SQL的缩略词。 显然在计算机科学领域,从来就没有一招打遍天下的技术,数据管理领域也不外如是。
There is no single development, in either technology or management technique, which by itself promises even one order of magnitude [tenfold] improvement within a decade in productivity, in reliability, in simplicity.
– Fred Books, 1986
业界因为商业、宣传等各种原因,会在某个技术刚刚崭露头角的时候宣称可以包治百病(否则也不会引人注意得到发展); 经过比较长时间的洗礼最终会回到其本源应用到其最合适的场合,而怎样鉴别什么样的场合使用什么样的技术,依然是软件开发者、架构师最大的挑战之一。
Leave a Comment
Your email address will not be published. Required fields are marked *