描述
开 本: 16开纸 张: 胶版纸包 装: 平装-胶订是否套装: 否国际标准书号ISBN: 9787302501565

目录
第1章接口
1.1使用接口编程
1.2依赖反转原则
1.3如何实现
1.3.1工厂模式
1.3.2服务定位器模式
1.3.3依赖注入
1.4真的实现了吗
1.4.1依赖的传递性
1.4.2依赖的形式
1.5真正实现
1.5.1配置文件
1.5.2配置代码
1.5.3惯例先于配置
1.5.4元数据
1.5.5实现消除依赖的方法的本质
1.6有必要针对接口编程吗
1.6.1针对接口编程的成本
1.6.2接口的意义
1.6.3何时针对接口编程
第2章事件
2.1控制反转
2.2观察者模式
2.3Java中的事件编程
2.3.1通用的事件发布者和收听者
2.3.2通用事件收听者的问题
2.3.3Swing用户界面里的事件编程
2.3.4专用事件收听者的问题
2.3.5彻底地面向对象
2.3.6Java 8带来的福音
2.3.7这一切背后仍然是对象
2.4C#中的事件编程
2.4.1代理
2.4.2事件
2.5JavaScript中的事件编程
2.6事件编程的其他细节
2.6.1收听者的执行顺序
2.6.2收听者是否在单独的线程执行
2.6.3控件层次中的事件传播
第3章MVC
3.1输入、处理和输出
3.1.1冯·诺依曼架构
3.1.2矩阵运算器和IPO
3.1.3矩阵运算器和IPO的升级版
3.2程序与用户的交互
3.2.1三类应用程序
3.2.2持续交互带来的变化
3.2.3图形用户界面带来的变化
3.3设计理念
3.3.1关注点分离
3.3.2模型
3.3.3模型和视图的分离
3.3.4控制器
3.3.5模型视图
3.3.6事件发布者与收听者之间的依赖
3.3.7合作方式
3.4桌面应用程序与移动App
3.4.1控制器和视图在代码单元上独立
3.4.2控制器、视图和模型之间的相互引用
3.4.3控制器和视图合一
3.4.4移动App
3.5Web应用程序
3.5.1Web应用程序简史
3.5.2服务器端的MVC
3.5.3前端控制器与控制器
3.5.4视图
3.5.5模型
3.5.6依赖注入
3.5.7浏览器端的MVC
3.6类型转换、校验和数据绑定
3.7MVC的意义
第4章界面
4.1以用户界面为中心VS以业务逻辑为中心
4.2设计视图VS源代码视图
4.3自定义控件VS复合控件
4.4命令式语言VS声明式语言
4.5内容与外观的分离
4.6基于请求的框架VS基于组件的框架
4.7极简主义
4.7.1用户界面上的极简主义
4.7.2删减的对象
4.7.3方法和特征
4.7.4防止过度
第5章数据库
5.1多值与复合属性
5.1.1关系型数据库模式的范式和第二范式
5.1.2范式与复合、多值属性
5.1.3关系型数据库中的多值和复杂数据类型
5.2数据库模式
5.3数据建模
5.3.1抽象的数据建模
5.3.2针对具体数据库的建模
5.4视图
5.4.1索引
5.4.2关系型数据库中的视图
5.4.3文档型数据库中的视图
5.5可伸缩性
5.6可得性与BASE
5.7编程接口
5.8总结
第6章权限
6.1身份验证
6.1.1验证类型
6.1.2验证属性
6.1.3知识要素验证
6.2Web应用的验证
6.2.1验证与会话
6.2.2第三方身份验证
6.3授权
6.4基于角色的存取控制
6.4.1用户与权限
6.4.2群组与角色
6.4.3权限与操作
6.4.4实现
6.5基于属性的存取控制
6.5.1资源与存取方式
6.5.2从权限到属性
第7章异类
7.1快速开发
7.2Lotus Notes是什么
7.3技术架构
7.3.1数据库
7.3.2客户端与服务器
7.4应用程序开发
7.4.1两种路径
7.4.2用户界面驱动的快速开发
7.4.3事件驱动编程
7.4.4直接使用文档对象编程
7.4.5权限模型
7.4.6角色和隐藏公式
7.4.7三类应用程序
7.4.8多种编程语言
7.5Lotus Notes的衰亡及其教训
7.5.1对用户主观体验重视不够
7.5.2快速开发的缺陷
7.5.3嵌入式开发的缺陷
7.5.4数据库和应用程序合一
7.5.5创新乏力
7.6给现有Lotus Notes客户的建议
第8章兴衰
8.1软件的更新和生命
8.1.1兼容性
8.1.2兼容性与创新
8.2客户端的兴衰
8.2.1客户端与服务器
8.2.2远程过程调用和数据传输协议
8.2.3客户端的胖瘦趋势
8.2.4客户端与浏览器
8.2.5浏览器与App
8.2.6理想的客户端应用程序
8.2.7开发人员体验VS用户体验
8.3Lotus Notes的历史
8.3.1前身
8.3.2青少年: 版本1~3
8.3.3中年: 版本4~6
8.3.4老年: 版本7~9
参考文献
自序
开发软件离不开编写代码,但仅仅具备编程的技能也还不足以胜任开发软件的工作。这就好比一个人会烧砖、砌墙,但要造一间可供人居住的屋子,他还得了解屋子的结构、不同房间的功能、水电管线的敷设、墙面地面的装修等方面的知识。对软件开发人员来说,编程领域的知识往往是受关注的,它们确实也可以分为多个层次: 编程语言本身的知识(如C、Java),编程范式和思想,面向对象编程和函数式编程,开发框架的知识(如Spring、AngularJS),等等。一个新人若想以软件开发为职业,大概需要阅读的范围就会集中在以上方面。然而,当他开始项目开发时,就会发现还有许多实际的问题需要考虑和解决,软件开发并不像编程教材上的代码样例和习题那样专注于某个算法或思想。不妨考虑一个典型的业务系统,它是一个图形用户界面的程序,因而需要采用某种GUI框架开发界面; 用户在界面上的操作通过事件机制调用相应的处理程序; 用户界面、事件处理程序和体现需求的业务逻辑必须组成某种合理的结构,否则系统会随着功能的增加迅速变得难以理解和维护; 系统越大,组件越多,越需要适当地保持它们之间的依赖关系,合理地应用接口是关键; 这个业务系统显然比所有数据都来自即时输入的计算器复杂,许多信息要往返于数据库; 后,这是一个多用户使用的系统,必须适应不同用户的权限需求。编程语言和范式的理论知识没有触及这些实际的问题,开发框架虽然涉及实践,却又局限在具体的方案中,不易让人获得对知识的一般理解。软件开发实践中遇到的各个方面的问题往往缺乏系统的理论,程序员凭着各自的理解动手,或者知其然而不知其所以然,或者每个人的所以然有出入甚至矛盾。例如,针对接口编程就是尽量多用接口吗?事件驱动编程的本质是什么?怎么样算是应用了MVC架构?极简主义就是越简单越好吗?文档型数据库和关系型数据库的优劣各体现在什么地方?基于角色的存取控制系统是如何理解权限的?在主流的软件开发理念之外能否另辟蹊径?客户端和浏览器之间的竞争究竟意味着什么?对这类实践中涉及的概念和遇到的问题,如果追根溯源,多思考一些是什么、为什么和怎么做,达到融会贯通的理解,既对实际开发有帮助,又有益于在纷繁多变的技术浪潮中看清技术的本质、把握解决问题的方向。本书从以上思路出发,逐个讨论软件开发实践中的重要主题。第1章辨析对象间的依赖和针对接口编程。第2章讨论事件驱动编程的方方面面。第3章分析MVC架构的思想和实现。第4章比较图形用户界面的一些相关或对立的思想和技术,并介绍极简主义潮流。第5章分析热门的文档型数据库,并和关系型数据库做对比。第6章讨论存取控制的各个环节,分析基于角色的和基于属性的存取控制的优缺点。第7章介绍快速的Lotus Notes程序开发。第8章探讨软件的兴衰和客户端的潮流。顺序上靠前的章节内容具有一般性,不会依赖其后的部分,靠后的章节有可能应用前文的知识。编写风格上每章力图从主题的源头和本质入手,遵循逻辑层层展开,尽量全面地遍历主题涉及的方方面面。书中代码为正在讨论的理念和问题服务,只是示意性地勾勒出核心的部分,无关和繁冗的部分被省略。野人献曝,未免贻笑大方; 愚者千虑,或有一得可鉴。是为序。
作者2018年5月
表5.1一个含有函数依赖字段的表
abcd
0000100200111102很显然被依赖字段集合本身及其任何子集都依赖于该集合。假如一个表的所有字段组成的集合依赖于其某个子集,该子集就能被用于确定(Identify)表中任何一条记录(当然前提是,表中的记录组成一个集合,不存在两条完全相同的记录),即任意两条记录在该字段子集上的值都不完全相同。这样的字段子集依照其作用就被称为表的超键(Superkey,或译为超码)。之所以称之为超键,是沿用集合论的术语,集合a是集合b的子集,则称集合b是集合a的超集,显然任何包括一个超键的更大的字段子集也能确定表中任何一条记录。一个超键,如果再也不能减少字段,就被称为表的候选键(Candidate key)。候选键中被表的设计者选来区分记录的键被称为主键(Primary key)。一个表中包含的其他表的主键称为外键(Foreign key),主要用来维持表之间的关系。以后示例表中主键所在的列名称用大写字母标示。假如一个字段集合只决定所属表的另一个字段子集,而不是所有字段,也就是说,不是表的超键,那些依赖字段集合的数据就形成冗余,因为两条记录的被依赖字段值如果相同,依赖字段的值就必然重复。如表5.2所示,a字段是表的主键,c、d字段依赖于b,但b不是表的超键,c、d字段的数据就有冗余。
表5.2一个由函数依赖导致数据冗余的表
Abcd
10橘子黄色21苹果红色30橘子黄色41苹果红色为了消除这种冗余,关系型数据库有第二范式(BoyceCodd范式),它要求对于表中的所有函数依赖,要么被依赖字段是表的主键,要么依赖字段是被依赖字段的子集。容易看出,类似上面例子中的那种函数依赖不符合这两点要求,因而要被排除掉。排除的方法就是把表的字段拆为两部分: 一部分是被依赖字段和依赖字段的并集,另一部分则是表的字段全集除去依赖字段对被依赖字段的差集。这种方法对具体的例子很直观,上面的表就会被拆成表5.3和表5.4。
表5.3由表5.2的字段全集除去依赖字段对被依赖字段的差集得到的表
Ab
10213041
表5.4表5.2被依赖字段和依赖字段的并集组成的表
Bcd
0橘子黄色1苹果红色现在再回头看关系型数据库模式的范式。实体的某个属性所能取的值的集合称为该属性的域(Domain),如果该域中的元素都是不可分的,我们就说这个域是原子的【注: Atomic原子这个术语作为不可分的代名词,自从古希腊哲学家德谟克里特提出原子概念后,不仅在现代物理中被用来指称如今我们都知道的组成物质的粒子,而且在其他诸多科学领域中都被使用,例如,这里的原子域以及后面将讨论的数据库交易的原子性。】范式要求表所有字段的域都是原子的。属性值之不可分,用其反面的可分能更好地界定。一个属性值可分,有两种情况: 种是属性值由多个值组成,这种属性称为多值属性,与其相对的称为单值属性; 第二种是属性值像实体一样由若干成分属性组成,这种属性称为复合属性,每个成分属性本身又可能是复合属性,从而形成多层次的结构; 与其相对的则是简单属性。将实体映射为符合范式的表时,简单的单值属性直接对应一个字段; 复合属性被拆分直至简单属性,这些成分属性如果是单值的就用一个字段表示; 多值属性被映射为一个新的表,每个值转化为一条记录,字段除了容纳值本身所需的以外,还包括一个或多个字段容纳该属性所属实体对应的记录的主键。我们分别来看这两种额外工作的意义。5.1.2范式与复合、多值属性复合属性由成分属性组成,从这一点看,它和实体没有区别,所以用一张单独的表来存储它自然也是一个选项。另一方面,复合属性由哪些属性组成并没有限制,在原本由简单属性组成的实体中,可以将若干属性组合成一个复合属性,很显然没有理由因为这样任意的选择就将它们剔除出实体、存放于一张新的表。将复合属性作为实体的一部分,和实体存放在一张表中,这样做是否恰当由实体与复合属性之间的映射基数来决定。1. 一对一当实体与复合属性之间一一对应时,复合属性独立于实体之外存在就没有意义,实体内复合属性与其他属性之间的边界变得模糊,若把复合属性看成一个小结构,它就已经融入所属实体的大结构。下面例子中的表5.5容纳一系列国家的记录,名称字段是一个复合属性,包含中文名、英文名和域名三个部分,因为与所属的国家记录一一对应,名称复合属性应该和记录保存在一起。
表5.5一个实体与其中的复合属性一一对应的表
IDnamecapital
1{chinese: 中国, english: China }北京2{chinese: 法国, english: France }巴黎3{chinese: 德国, english: Germany }柏林2. 多对一此时复合属性的数据有冗余,可以为其创建单独的表。选择复合属性的某个成分属性集合作为复合属性的超键,复合属性的整体就依赖于该超键,而由于多对一的关系,该成分属性集合又并不是实体的超键,所以依据第二范式可以将复合属性划分出去、创建新实体,实体内原复合属性变为容纳新实体的主键、以保持两者间的关系。不用函数依赖的判别方法,复合属性的值相同的多条记录,在该属性上的数据有重复,这一点也是很明显的。我们的国家表增加了一个所在洲的复合属性,如表5.6所示,可以看出法国和德国因为同属欧洲,该字段的数据有冗余,因而被拆分成表5.7和表5.8。
表5.6一个实体与其中的复合属性多对一的表
IDnamecapitalcontinent
1{chinese: 中国, english: China }北京{chinese: 亚洲, english: Asia }2{chinese: 法国, english: France }巴黎{chinese: 欧洲, english: Europe }3{chinese: 德国, english: Germany }柏林{chinese: 欧洲, english: Europe }
表5.7表5.6将复合属性划分出去后形成的表
IDnamecapitalcontinent
1{chinese: 中国, english: China }北京Asia2{chinese: 法国, english: France }巴黎Europe3{chinese: 德国, english: Germany }柏林Europe
表5.8表5.6的复合属性独立成实体后形成的表
NAMEname_in_chinese
Asia亚洲Europe欧洲3. 一对多此时复合属性同时又是多值属性,但数据并没有冗余,可以任其保留在实体中。实体与复合属性的一对多关系意味着没有两个实体拥有公共的复合属性。因此实体的主键就能确定其复合属性多值组成的集合。或者反过来看,对复合属性的某个值,我们仍然可以选择其某个成分属性集合作为超键,该复合属性值就依赖于此键,但是此键也能确定整个实体,实际上复合属性多值中的任何一个都是实体的超键,所以一对多关系中的复合属性没有违反第二范式。在关系型数据库中,传统上却是依据范式将该属性剥离出来,存放进单独的表。这样做以后,如何保持原实体和新的属性实体之间的关系,又有两个选项。若仍然遵循属性的原子性要求,属性实体将开辟字段容纳原实体的主键; 若允许多值属性,则原实体将在某个字段内保留对应的多个属性实体的主键。这三种方案都做到了数据完备和没有冗余。我们把国家表的洲属性改为城市属性,表示这种一对多关系的三种方案分别由表5.9、表5.10与表5.11、表5.12与表5.13所示。
表5.9一个实体与其中的复合属性一对多的表
IDnamecapitalcities1{chinese: 中国, english: China }北京[{chinese: 上海, english: Shanghai}, {chinese: 香港, english: Hongkong}]2{chinese: 法国, english: France }巴黎[{chinese: 尼斯, english: Nice}, {chinese: 马赛, english: Marseille}]3{chinese: 德国, english: Germany }柏林[{chinese: 莱比锡, english: Leipzig}, {chinese: 汉堡, english: Hamburg }]
表5.10表5.9将复合属性划分出去后形成的表,不包含外键
IDnamecapital
1{chinese: 中国, english: China }北京2{chinese: 法国, english: France }巴黎3{chinese: 德国, english: Germany }柏林
表5.11表5.9的复合属性独立成实体后形成的表,包含外键
NAMEname_in_chinesecountry
Shanghai上海1Hongkong香港1Nice尼斯2Marseille马赛2Leipzig莱比锡3Hamburg汉堡3
表5.12表5.9将复合属性划分出去后形成的表,包含外键
IDnamecapitalcities
1{chinese: 中国, english: China }北京Shanghai, Hongkong2{chinese: 法国, english: France }巴黎Nice, Marseille3{chinese: 德国, english: Germany }柏林Leipzig, Hamburg
表5.13表5.9的复合属性独立成实体后形成的表,不包含外键
NAMEname_in_chineseNAMEname_in_chinese
Shanghai上海Marseille马赛Hongkong香港Leipzig莱比锡Nice尼斯Hamburg汉堡由于两种实体间的一对多和多对一关系仅仅是因为视角的不同,所以前面讨论的实体与复合属性多对一的情况也可以反过来用本节的方法处理。其中一种突出的反差就是实体和复合属性保存在一个表中,不过“多”的实体变成了复合属性,“一”的复合属性被视为实体,这样反客为主的结果是表5.14。
表5.14表5.6实体和复合属性地位对调后形成的表
NAMEname_in_chinesecountries
Asia亚洲{chinese: 中国, english: China }Europe欧洲[{chinese: 法国, english: France }, {chinese: 德国, english: Germany}]这种方法的局限性是多值属性的“多”数量上不能太大,否则在使用时就很不方便。例如一个银行的账户、一条主帖的回复,就不适宜把一对多的双方放在一个表里。4. 多对多此时和多对一类似,将复合属性保留在实体内会导致数据冗余。这种情况下,复合属性仍然是多值属性,但是多个实体的复合属性多值构成的集合有交集,复合属性内部的函数依赖不能扩展到整个实体,因此违背了第二范式。为消除冗余,复合属性将被剥离出来,形成独立实体。原实体和新的属性实体之间的关系如何保持,有三个选项。关系型数据库的传统做法是创建一个单独的表来记录两者的关系,字段包括两者的主键。假如允许多值属性,因为原实体和属性实体在关系上的对称性,另一个选项是选择其一开辟字段记录对应的另一方的多个主键。我们用一个国家内拥有的交通种类来演示多对多的各种表现形式,分别由表5.15~表5.20表示。
表5.15一个实体与其中复合属性多对多的表
IDnamecapitaltransportation
1{chinese: 中国, english: China }北京[{name: 铁路, vehicle: 火车}, {name: 公路, vehicle: 汽车}, {name: 航空, vehicle: 飞机}]2{chinese: 法国, english: France }巴黎[{name: 铁路, vehicle: 火车}, {name: 公路, vehicle: 汽车},{name: 航空, vehicle: 飞机}]
表5.16表5.15的实体将复合属性划分出去后形成的表
IDnamecapital
1{chinese: 中国, english: China }北京2{chinese: 法国, english: France }巴黎
表5.17表5.15的复合属性独立出来形成的表
NAMEvehicle
铁路火车公路汽车航空飞机
表5.18记录表5.16和表5.17之间关系的表,字段值都是原子的
COUNTRY_IDTRANSPORTATION
1铁路1公路1航空2铁路2公路2航空
表5.19表5.16通过多值外键记录其与表5.17的关系
IDnamecapitaltransportation
1{chinese: 中国, english: China }北京铁路、公路、航空2{chinese: 法国, english: France }巴黎铁路、公路、航空
表5.20表5.17通过多值外键记录其与表5.16的关系
NAMEvehiclecountries
铁路火车中国,法国公路汽车中国,法国航空飞机中国,法国使用简单的多值属性是否恰当之分析,实际已经包含在上面的一对多和多对多情况的讨论中。实体和属性值存在一对多关系时,多值属性包含在实体内不会导致冗余。相反为属性建立单独的表,只包括属性值和原实体的主键两个字段,增加了数据库设计和读写时的复杂性。实体和属性值的关系为多对多关系时,虽然表面上看构成冗余,但重复的仅仅是简单值,只要该值没有重命名的可能,就和两个实体多对多时一方包含另一方的主键一样。相反为属性和实体属性间的关系分别建立一个表,数据库设计和读写都变得不直观,却没有带来特别的好处。5.1.3关系型数据库中的多值和复杂数据类型从以上情况的讨论可以看出,多值属性和复合属性并不必定会导致冗余。更进一步地说,一个属性是否多值、复合,与其数据是否会冗余,是无关的。以上出现冗余的场合,只要遵循第二范式就能消除。多值属性对于实体来说很常见,特别是值都是简单的情况,像电话号码、关键字、用户名、测量值,将这些多值与实体保存在一起是十分自然的。类比于编程语言,除了原始数据类型,各种语言首先具备的就是某种容纳多值的类型: 数组、列表、集合等。复合属性能为相关属性提供逻辑上的组织,方便数据用户的读写。例如,一个由省、市、区、街道、楼宇、门牌号多级信息组成的复合属性,程序在读取和写入时都只需将所有数据作为一个整体地址对象,使用时按需访问该对象的属性; 而假如由多个分立的简单属性表示,读写时麻烦,可能还要和程序中使用时的整体地址做转换。很多时候如果刻意避免多值或复合属性,只会为数据引入多余的复杂性。关系型数据库模式范式要求的属性值原子性,既苛刻也是不必要的。结果就是SQL 1999及其之后的标准引入了复杂数据类型,允许关系型数据库在设计模式时定义复合(TYPE)和多值(ARRAY和MULTISET)字段。在这些标准发布前,有些数据库的开发商就已经在其产品中加入对复杂数据类型的支持。然而遗憾的是,这么多年过去后,关系型数据库对复杂数据类型的支持仍然不是普遍的,例如,至2017年1月的5.7版本为止,MySQL仍然不支持上述标准中的复杂数据类型【注: 虽然MySQL具备一些偏离和变通的方法,如SET、JSON类型和手动序列化反序列化ARRAY】。另一方面,即使在支持的产品中,所用的术语和语法也常常不同于标准,如Oracle数据库的ARRAY类型名称就是VARRAY。缺乏统一可用的语法、范式的影响以及长久以来的传统,都使得在关系型数据库中多值和复合属性的应用远远不如文档型数据库那样充分和有效。5.2数据库模式数据库的模式(Schema)指的是数据库内各种对象的逻辑设计,一个很好的类比是面向对象编程语言中的类型定义。关系型数据库有严格精确的模式,如用SQL对表做的定义,而文档型数据库通常被称为是无模式的(Schemaless)。本节就来看看这种对比的含义。关系型数据库在对要存储的实体建模时,认为可以将它们按类别划分,每一类实体具有相同的结构——相同的字段序列,每个字段又有特定的数据类型和取值上的约束。这些共同的结构被事先清晰地定义成表,以容纳同一类型的数据——记录。一个表一旦定义好,其中记录的结构就固定了,如要变动必须使用SQL语句修改表结构。实体的属性取值往往有某种要求,例如,一个用户的姓名不能为空、长度不超过某个数值,年龄必须为数字,身份证号,民族的值位于一个集合内,电子邮箱地址符合一定规则等。对这些要求的校验,既可以发生在写入数据库之前的应用程序部分,也可以包含在数据库里。关系型数据库的模式就能显式地包含这些要求。字段数据类型的划分十分精细,以机器为取向,在SQL标准规定的类型之外,一个数据库产品往往还有自己的扩展。例如,在MySQL中,数字类型被细分为TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT、DECIMAL、FLOAT、DOUBLE、BIT等,字符串类型被细分为CHAR、VARCHAR、BINARY、VARBINARY、TINYBLOB、BLOB、MEDIUMBLOB、LONGBLOB、TINYTEXT、TEXT、MEDIUMTEXT、LONGTEXT、ENUM、SET等,日期时间类型被细分为DATE、TIME、DATETIME、TIMESTAMP、YEAR等。除了字段的数据类型和字符串的长度,属性取值的种种要求被统称为数据的完整性约束(Integrity constraints),它们又可以被分为仅限于一个实体的属性值的实体完整性,和涉及多个实体的参照完整性。常用的完整性约束包括非空(Not null)、(Unique)、取值限定于另一个表的某列值等。理论上任何能用SQL语句表达的要求(谓词Predicate)都能以check从句的形式实施。关系型数据库的模式精确地规定了记录的结构和字段取值的要求。换用文档来表示同样的实体时,当然也可以用某种模式来规定文档的结构和属性取值的要求。XML模式(XML Schema)就被广泛用来校验XML文档。虽然不那么常用,也可以为JSON文档创建模式,并且和XML模式一样,JSON作为通用的数据格式,其模式本身也采用JSON。我们先用JSON来表达5.1节一对多关系中的一条记录:
{
“id”: 1,
“name”: {
“chinese”: “中国”,
“english”: “China”
},
“capital”: “北京”,
“cities”: [
{
“chinese”: “上海”,
“english”: “Shanghai”
},
{
“chinese”: “香港”,
“english”: “Hongkong”
}
]
}
它的模式是:
{
“$schema”: “http://json-schema.org/draft-04/schema#”,
“type”: “object”,
“properties”: {
“id”: {
“type”: “integer”
},
“name”: {
“type”: “object”,
“properties”: {
“chinese”: {
“type”: “string”
},
“english”: {
“type”: “string”
}
}
},
“capital”: {
“type”: “string”
},
“cities”: {
“type”: “array”,
“items”: {
“type”: “object”,
“properties”: {
“chinese”: {
“type”: “string”
},
“english”: {
“type”: “string”
}
}
}
}
},
“required”: [
“id”,
“name”
]
}
如果采用以上模式,对JSON文档也能施加一些关系型数据库的完整性约束。JSON模式能够表达单个文档范围内的对属性值的约束,如数据类型、非空、取值范围,还可以用正则表达式设定属性值的规则(XML模式也类似); 但是对涉及多个文档的约束就无能为力,如某个属性在同一种多个文档中取值,或者涉及多种文档的参照完整性。但是,文档型数据库从理念上选择不通过模式对其中的文档施加一致的严格的规定。通常所谓的文档型数据库的无模式就是指数据库理念和设计上的这种选择,而不是说文档本身没有模式。无模式的设计会带来两个结果。首先是数据库里的任何两个文档都不必有同样的结构,不再有表这样的严格的容器。其次是一个文档的结构也不是固定的,随时可以修改。例如程序读取一个文档,添加一个属性,删除一个属性,再保存回数据库。在属性的数据类型上,文档型数据库也不像关系型数据库那样有严格的区分,而是更多地以人为取向。例如,Lotus Notes数据库数字类型不区分INT、DECIMAL、FLOAT、DOUBLE等,只有一个NUMBERS; 字符串类型不区分CHAR、VARCHAR、BINARY、TEXT等,只有一个TEXT; 日期时间类型不区分DATE、TIME、DATETIME等,只有一个DATETIMES。以JSON作为文档格式的CouchDB支持的JSON在数据类型也同样简单。MongoDB采用的BSON格式基于效率的考虑,数据类型略多,稍微接近一些关系型数据库。属性值的数据类型只反映当前的情况,并不像表格的字段那样固定不变,例如一个文档的某个文本属性,可以随时写入数字或者日期值。无模式的设计意味数据库对写入的文档不会施加任何校验。值得注意的是,这只是默认的状况。具体的文档型数据库产品仍然可以设计各自的校验机制。宽松的如Notes在数据库层面没有校验机制,完全依靠应用程序负责。MongoDB在3.2版本中新增了文档校验的功能,覆盖范围与文档模式类似,涉及单个文档范围内的属性取值约束。CouchDB在文档校验上符合其一贯的简单灵活的理念,让用户直接用JavaScript写校验函数,相当于把应用程序的做法移植进数据库。关系型数据库与文档型数据库对世界的看法之差异在于: 前者认为每个实体都有一个模板,遵循同一模板的成千上万个实体只是模板的实例,模板先于实体,所以数据库的模式是必需的,对记录的结构和字段取值的要求是先天的; 后者认为每个实体都是独特的,不应该对实体有任何约束,如果一组实体确实遵循共同的模式,也可以对它们施加校验。类比于编程语言的类型体系,关系型数据库相当于静态强类型,变量值是一条记录,每个变量在被使用前都必须声明为某种类型; 文档型数据库相当于动态弱类型,变量值是一个文档,变量在使用前无须定义为特定的类型,在使用过程中变量值(对象)的结构也可以发生变化。CouchDB的图标曾经长期是一个人歪歪斜斜地躺坐在沙发上(现在被改成了一张空空的沙发),它的创始人Damien Katz博客网站上的标语Just Relax. Nothing is Under Control.(放松,一切尽在控制外)恰可以作为文档型数据库理念的一个比喻。宽松的好处是能快速适应实体的变化,这在开发中由于需求增加、设计变化或者修补bug是不时会发生的。文档结构可以随时改变,关系型数据库则每次变动都需要手工修改表结构,而有些修改是很麻烦的,如ALTER TABLE ADD COLUMN之类的语句往往很耗时,尤其是当表里的记录数较多时,开发人员不得不采用创建新表删除旧表,或是先将表里的数据导出再修改之类的替代手段。因而采用关系型数据库时,应尽量预先确定程序涉及的实体的需求细节,避免表结构的频繁改动。文档型数据库则适用于那些项目开始时实体的结构很难确定或随时间容易变化的情况。当然,灵活性的另一面是缺乏强制检查而可能犯的错误。没有特意设置校验时,文档对任何修改都来者不拒,增加了开发人员犯错和失误酿成不良后果的可能性。5.3数据建模无论使用哪种类型的数据库,数据建模(Data modeling)都是重要的必不可少的。数据建模可以在抽象的和针对某个具体数据库的两个层次上进行,下面分别进行讨论。5.3.1抽象的数据建模抽象的数据建模使用某种与具体实现无关的模型来表达关于数据的需求。这里继续运用实体关系模型,于是建模就变成根据需求抽象出实体及其间的关系,核心问题就是如何界定实体,以及如何表达它们之间的关系。因为同样的需求可以设计多套不同的实体和关系方案,换个角度说,一组实体和关系可以用许多其他等价的实体和关系来表达。具体地说,任何实体,只要不是只有一个属性的简单实体,都可以拆分成两个或两个以上的实体,每个新实体包含老实体的部分属性,新实体间保持一一对应的关系。包含多值复合属性的实体可以被拆分成由其他属性组成的父实体,和由每个复合属性值构成的子实体,父实体和子实体间保持一对多的关系。反过来,相互间有某种关系的两类实体也可以被合并成一类实体。具一对一关系的两类实体很自然地能被合并成一个更大的实体。具多对一和一对多关系的两类实体合并后,“多”的一方成为新实体的多值复合属性。具多对多关系的两类实体也能够合并,只不过必有一方的数据在新实体中会有冗余。综上所述,数据建模有两种对立的方向——分与合,我们把分的方式称为正规化(Normalize),合的方式称为去正规化(Denormalize)【注: 这两个术语的含义与单纯关系型数据库语境下的不完全相同,那里正规化的标准是遵循设计范式,这里强调的是分成更小的实体,主要体现在文档型数据库和关系型数据库建模的差别中。】当多类实体间存在二元关系时,在哪两者间建立直接的关系也有很大自由度。为了说明抽象数据建模中的这种任意性,我们来看下面的例子。A、B、C、D四类实体间的关系如图5.1所示。
图5.1A、B、C、D四类实体间的关系
可以去正规化,即合并某些实体,并调整二元关系的参与者,图5.2分别演示了众多可能性中的两个。
图5.2对图5.1表达的实体和关系去正规化的两种可能方案
当然,这些自由选择不应该走。我们不可能把所有实体都拆分到只包含单个属性,相反将所有实体合并成一个巨无霸也毫无意义。依据实体的意义和在需求中的自然联系做出的选择较为恰当。5.3.2针对具体数据库的建模针对具体数据库建模,就是将抽象建模得到的实体和关系转化成所用数据库具体的载体和方式。这时抽象的实体和关系的多种表示就不再是等价的,具体数据库的特点和限制使得只有部分设计方案能满足使用上的需求,并且根据特定的情况和需求这些方案在效率上存在显著的差异。所以针对具体数据库的建模要考虑的因素比抽象建模多,以求得到较好的设计。关系型数据库的载体是表和记录,文档型数据库的载体是文档。虽然有许多理念和细节上的差异,两种载体总体上功能是对等的,都可以用来表达实体及其间的关系。表达实体很直接,实体的属性分别映射到表的字段和文档的属性。表达关系时,总体上可分为两种方式: 一种是将相互间有关系的实体包含在单一的表或文档里; 另一种则是用外键【注: 文档型数据库通常不能保证一类文档用于引用另一类文档的属性,取值限定于被引用文档的属性值集合,只能靠数据库以外的程序去确保这种约束,所以严格地说不能称为是外键,但是在功能上是一样的。】来关联分开保存的实体。这又可以分为两种情况: 其一是外键位于某个实体内; 其二是实体本身不包含外键,由另一个单独的表来记录指向参与关系的实体的外键。因此理论上,可以将记录用结构相同的文档来表示,从而把关系型数据库中建立的模型移植到文档型数据库; 反过来,也可以把文档型数据库里建立的模型用关系型数据库来反映。然而实际上使用这两种数据库建模时,思路和结果都有相当的差异,我们接下来就要分析这些差异。从5.3.1节可见,数据建模的关键是选择何时正规化(或者说去正规化)。选择的主要标准是满足程序使用数据时的需要,同时也不可避免地会受到数据库特点和限制等影响。所有影响设计的因素性质上可分为两类: 一类是因为数据库的限制导致只能选择某种设计,另一类是在多种可行的设计中,存在效率上的优劣之分。1. 限制因素(1) 多值与复合属性。当所用的关系型数据库不支持多值或复合属性时,该属性值就只能被正规化成单独的表。(2) 事务的原子性要求。数据库中,一系列逻辑上相关的写操作组成一个事务。关系型数据库能保证事务具有原子性(Atomicity),意思是构成事务的一系列操作像一个整体一样,在任何情况下造成的结果都不可分,也就是说,或者都成功,一旦有一个操作失败,数据库就会回滚到所有操作之前的状态,像一切都没有发生一样。文档型数据库一般不具备事务的原子性,只能保证单个文档更新时的原子性,即文档或者更新成功,或者失败保持初状态,不会居于两者之间的状态,例如某些属性被修改、其他未被改变。这样当若干文档的更新有外在的原子性要求时,就只能将它们去正规化成单个文档。(3) 文档的大小限制。当多种实体用一个文档表达时,文档的体积容易膨胀。特别当用多值复合属性表示的子文档本身较大或数量较多时。文档的体积超出预先分配的空间时,数据库就要安排新的空间并移动文档。文档过大也会影响读写的性能。当这些情况可能发生时,文档就应该被去正规化。2. 效率因素效率是从使用数据库的应用程序的角度来评价的。完成某一业务逻辑对应的数据库操作需要多少时间、存储同样的实体和关系占据多少空间,这些都属于数据库的效率范畴。1) JOIN操作在关系型数据库中,可以通过JOIN操作将多个表中的信息组合在一起。也正是因为有这种能力,关系型数据库能够将一个实体正规化成多个表。JOIN操作功能强大,但成本高昂,往往是SQL语句中的性能瓶颈。文档型数据库一般缺乏JOIN文档的能力,因此查询相关文档时不能一次性完成,而需要对不同类别的文档分别进行查询,再由程序手动组合其信息。文档型数据库对此问题的解决方法是将相关文档合并成一个,这样对它们的读写就能一次性完成,并且无须费用高昂的JOIN操作。当然这样的去正规化,好是发生在参与文档原本就适宜合并的情况下,如一对一和一对多,在多对多的场合,嵌套文档就会有数据重复的代价。例如,考虑学生和课程两种实体之间的关系,一个学生可以选多门课程,一门课程通常有一名以上的学生。如果想要根据学生的姓名或者课程名称一次查询就获得相关的学生和课程的信息,就必须既将课程文档嵌套在学生文档里,又将学生文档嵌套在课程文档里,这样不仅会增大数据量,还难以保证两套数据的一致性。所以文档型数据库也努力引入JOIN的能力,如Couchbase的N1QL语言的JOIN从句和MongoDB的$lookup操作符。下面用MongoDB来演示。
//学生和课程的文档
Student1: { “_id”: 1, name: “Tom”, age: 20, courses: [”Physics”, “Chemistry”, “Math”]}
Student2: {“_id”: 2, name: “Mary”, age: 20, courses: [”Physics”, “Chemistry”, “Biology”]}
Student3: {“_id”: 3, name: “Jerry”, age: 21, courses: [”Physics”, “Math”, “English”]}
Course1: {“_id”: 4, name: “Math”, teacher: “Alex”, credit: 3}
Course2: {“_id”: 5, name: “Physics”, teacher: “Florence”, credit: 2.5}
Course3: {“_id”: 6, name: “Chemistry”, teacher: “Gates”, credit: 2.5}
//从学生的文档根据课名连接课程信息
db.students.aggregate([
{
$lookup:
{
from: “courses”,
localField: “courses”,
foreignField: “name”,
as: “courses_info”
}
}
])
//该操作的结果会像下面这样
Student1: {
“_id”: 1, name: “Tom”, age: 20, courses_info: [
{“_id”: 5, name: “Physics”, teacher: “Florence”, credit: 2.5},
{“_id”: 6, name: “Chemistry”, teacher: “Gates”, credit: 2.5},
{“_id”: 4, name: “Math”, teacher: “Alex”, credit: 3}
]
}2) 冗余如前所述,数据冗余有两个不利后果: 耗费更多的存储空间,以及很多时候更有害的数据不一致的风险。为了保证重复的数据处于一致的状态,系统至少要消耗更多的资源更新数据的所有副本。但有时候,冗余也能带来效率上的好处。设想大学的课程要求学生已经上过某些预备课程,例如,电动力学需要普通物理和微积分作为基础知识。在显示这些预备课程的时候,好能同时列出它们的基本信息以方便学生了解。在关系型数据库中,我们可以用一张表记录课程的信息,另一张表记录课程与预备课程之间的关系(如两列分别对应的课程id)。应用程序在展示这些信息时,对于某门课程,先要查询出它的预备课程,再到课程表读取它们的基本信息。这样的正规化设计完全没有冗余,但多次查询影响了程序的性能,一种可能的改善是扩展预备课程表,在其中保留每门预备课程的基本信息。这样应用程序只需一次查询,就能获得所需的全部数据。代价则是课程的信息每次变动时,除了课程表本身,还要更新预备课程表。因为课程信息较稳定,比起用户经常使用的阅读预备课程信息的性能提升,这个代价是可以接受的。一般地说,冗余有利于相关数据读的性能,不利于写的性能。一个应用场景,如果读的次数远多于写,则可以考虑接受一定程度的冗余; 反之,写的次数较高时,冗余就得不偿失了。5.4视图视图并不是关系型数据库必不可少的模式,但在某些文档型数据库中,视图就是查询数据得以进行的途径,并且还发挥创建索引、维护文档关系和显示数据等核心功能。比较视图在两种数据库中的不同角色,就是探索和展现两种设计理念的绝好机会。5.4.1索引数据库的用处是方便快速地管理大量数据,“管理”从技术上说就是增添、查询、修改和删除四种操作。数据量的庞大令操作的效率尤为重要。无论是查询还是更新,根据条件迅速找到目标数据都是性能的关键。以关系型数据库的术语为例来说明,查询条件由记录的一个或多个字段值的表达式构成,如
SELECT title, price FROM books WHERE price > 80
语句中价格的表达式。用作查询条件的字段称为搜索键(Search key)。一般情况下,books表中记录的排列顺序与价格字段的值无关,要找到符合条件的记录,就必须逐条检查记录搜索键的值。这样做的速度显然是无法接受的。好一点的情况下,记录按照价格排序存储于表中,这时对价格条件的查找就有更高效的方法。首先,逐条检查找到条价格大于80的记录,按升序的方向排在其后的就是所有符合条件的记录。其次,我们可以采取二分搜索算法,更快速地定位到价格开始高于80的记录。利用排序列,虽然可以进行二分搜索,但仍然要在存储记录的硬盘内反复定位搜索涉及的中间记录并读取其搜索键的值。为了进一步地优化,我们可以提取所有记录的价格字段值,建立一个索引。索引的想法很简单,就是从搜索键的值指向它们所在记录位置的映射。我们每个人查字典时所用的拼音和部首检字法,就是分别利用字典前面的拼音和部首索引。根据其原理,索引可以分为两类。类有序索引,简单的形式就是将记录中搜索键的值提取出来排序,一个值和它所在记录的位置,构成一个索引条目。这样的索引有些像按搜索键顺序排列的表本身,区别在于索引条目只包括一个或少数几个字段值,而表的每行记录则包含所有的字段,因此前者的读取和检索速度比后者快。为了便于进行二分搜索,数据库经常使用更精致的B树或B 树索引,搜索键的值被组织成B或B 树数据结构。凭借它,查询记录所需的时间稳定在记录数的对数级别。第二类哈希(Hash)索引。哈希函数的定义域是搜索键所有可能取值的集合,值域则是一个元素数量较小的集合,因此多个搜索键的值对应一个哈希值。前者到后者的映射,具有平均和随机的特性。也就是对于任意一个搜索键值的序列,得到每个哈希值的次数大致相等。哈希索引的原理是为被索引的字段找到一个合适的哈希函数,每个哈希值分配一个桶(Bucket),桶内保存具有同样哈希值的记录的索引条目。哈希函数取值的平均性和随机性,确保每个桶内索引条目的数量大致相等。查找具有某个字段值的记录,分为几步: 先用哈希函数计算出记录对应的索引条目所在的桶,再在桶内找到该索引条目,后由条目包含的位置定位到记录。因为哈希函数求值所需的时间为常数,每个桶内存储的索引条目数量不会超过一个设定值,所以凭借哈希索引查询记录所需的时间不会超过一个值。当搜索键超过一个时,仅使用单个字段值的索引就不敷应用了。例如,考虑“SELECT title, price FROM books WHERE category = “arts” AND price >80”查询。在类别和价格字段上都分别建立了索引。应对复合查询时有两种思路: 根据某个索引找出满足单个条件的记录,再逐条检查是否符合另一个条件; 抑或两个索引都用上,找出分别满足两个条件的记录,再求出它们的交集。两者都存在索引无法发挥效力的步骤,速度缓慢。解决的方法是创建对应搜索键的包含多个字段的复合索引,索引值为被包含的字段值组成的有序元组。如在本例中,复合索引由类别和价格两个字段组成,(“arts”, 80) < (“arts”, 100)< (“history”, 60)就是升序排列的几个索引值。有了复合索引,多个查询条件就能像单个查询条件一样被高效处理。有时候,索引条目除了搜索键的值,还包含记录的其他字段值。这样当某个查询选择的字段全部位于搜索键所用的索引内时,数据库就能在索引里找到被查询的条目后,直接读取其内容返回,不必再去读取条目指向的记录,性能得以大大提高。如此的索引称为覆盖索引(Covering indices)。继续看“SELECT title, price FROM books WHERE category = “arts” AND price>80”的例子,假如索引在类别和价格搜索键外还包括标题字段,执行这条查询就只需读取索引。一个表上可以建立多个索引。如果表中记录的存储依据某组字段排序,在该组字段上又建立了索引,我们就称该索引为表的聚集索引(Clustering index)或主索引(Primary index),其他索引则称为非聚集索引(Nonclustering index)或次索引(Secondary index)。索引带来的好处不是没有代价的。索引文件本身要占据空间。记录更新(新增、修改和删除)时,索引自然也要随着更新。新增记录时,更新索引单纯是性能上的开销。修改和删除时,除非是对所有记录,否则都有选择条件,此时搜索键上的索引正起到与查询时同样的作用,而修改和删除后更新索引则是开销。总的说来,在数据库查询和更新中经常用到的搜索键上建立索引,是有明显的性能收益的。对其他字段上的索引就要谨慎建立,以避免存储和更新索引的开销过大。以上关于索引的讨论对任何类型的数据库都是适用的,只需把关系型数据库的术语和技术转换成我们关心的那种数据库对应的术语和技术。例如,Lotus Notes和MongoDB两种文档型数据库尽管差别很大,但都建立了B树结构的索引,以提高查询文档的性能。5.4.2关系型数据库中的视图在关系型数据库中,定义了表的模式,就可以对其容纳的记录进行各种操作。在进行查询时,出于安全或者方便的原因,有时会希望能将某个查询结果当成一个“虚拟表”重复使用,例如下面的语句查询出books表中教材类型的书籍。
SELECT title, authors, publisher, price FROM books WHERE category = ‘textbook’
程序中若要经常查询教材记录,每次都重写这段语句就显得麻烦,解决办法是将上面这个查询作为对源表的“视图”(View)保存在数据库中。
CREATE VIEW textbooks AS
SELECT title, authors, publisher, price FROM books WHERE category = ‘textbook’
这样以后就可以直接用视图名称来代替对应的查询。
SELECT title, price FROM textbooks WHERE price > 80
为了保持数据,视图没有保存查询的结果,而是像函数调用一样,在被引用时临时执行查询。上面的查询相当于:
SELECT title, price FROM (SELECT title, authors, publisher, price FROM books WHERE category = ‘textbook’)
WHERE price > 80
视图的定义中可以包含任何查询语句,包括对多个表中记录的连接。但也因此,对视图的更新就变得十分棘手。例如,对一个多表查询组成的视图,插入一条记录时,记录中的字段必须被拆分保存到各个源表,而这些零碎的字段值很可能不满足表的完整性约束。所以关系型数据库通常不允许,或者仅仅当视图的定义满足一系列严格的条件时才允许对视图进行更新操作。于是,视图的功能实际上就像工作、购物等网站上用户可以自定义和保存的查询条件。视图不能定义索引,对其的查询只能依赖源表的索引。假如一个视图被频繁使用,它对应的查询结果又很少变化,将结果保存起来就能够节省每次执行查询的开销,这样的视图称为物化视图(Materialized view)。回过头看,视图这个名称可谓再贴切不过,它很好地反映了它所指称的模式的本质和功能。视图不代表独立的数据,而是对现有数据的一种View。它的只读限制也可被看作隐含于视图的含义。5.4.3文档型数据库中的视图关系型数据库中的视图是建立在表的基础上的,文档型数据库没有表的概念,不过却有功能上与前者部分一致的视图。不同的文档型数据库衍生出视图是分别出于两种迥然相异的逻辑,背后对应的则是两种看待文档的理念。如前所述,文档没有先天的表模式的限制,数据库中的任何两个文档都可以有差异。不过在实际应用中,数据库中的文档不可能每个都是独一无二的,还是会根据它们所代表的实体形成类别,例如人员、车辆、报销单。程序在查询文档时,首要的条件也是它们的类别。所以数据库必须提供某种机制,依照类别来组织文档。为此,MongoDB在文档与数据库之间引入一个集合(Collection)的层次,一个集合内的文档都表示某个类型的实体,具有相同的语义和基本一致的结构。集合充当了关系型数据库中表的角色。在此之上定义的视图也就与关系型数据库中的视图具有几乎一样的性质: 视图可以从一个或多个集合中选择文档、提取属性、计算聚合值; 视图是只读的; 视图是即时计算的; 视图不能定义索引,只能利用集合的索引。与MongoDB对立的另一个阵营,彻底遵循文档没有模式的理念,不给文档添加任何容器的约束。所有文档不区分类别地存储于数据库中。数据库就像一个大杂烩的容器,又像一个没有分门别类的书架,其中的文档没有任何外在的类别标记和存储上的区隔。Lotus Notes是这种数据库的代表,后来由它曾经的开发人员之一Damien Katz创造的CouchDB继承了这方面的设计,这个影响又被带入到与CouchDB有关的一系列数据库,如Couchbase中。在这些数据库中,视图不再是辅助性的模式,而是必不可少的对象,读取文档、创建索引、建立文档之间的关系等关键的任务都要依赖视图。接下来就以Lotus Notes为例,讨论这种独特的理念导致的逻辑发展。1. 选择文档子集虽然文档在数据库中的存储没有任何区隔,但是数据库仍然必须为它的用户提供某种按类别组织文档的机制。根据Lotus Notes(以后简称Notes)数据库的理念,类别不是文档的先天属性,只是利用普通的字段值做人为的区分,例如我们可以在文档内添加Type、Category或是名称上毫不相干的Level、Source、Form字段【注: Lotus Notes以Form字段保存打开文档所用的表单,于是它也经常被用来区分文档的类别】。接下来使用该字段的值,选择出数据库中文档的一个子集,如SELECT Form = “Person”选择出Form字段的值为Person的所有文档。这种机制具有很大的灵活性,用途不限于依据文档所代表的实体给它们分类。指定选择条件的语句使用的是Notes的公式语言,可以基于文档的属性和字段值写出复杂的条件,类似SQL查询中的WHERE从句。我们不妨把它称为选择公式。选择公式和SQL的SELECT语句有很大的差异。明显的是前者仅仅选择了文档,没有指定字段。更微妙的地方与索引有关。我们已经看到,为了提高查询文档的效率,可以为搜索键创建索引。但这样做隐含的前提是建立索引的文档集合属于同一类别,都包含搜索键。而Notes数据库中的文档是没有先天类别地存储的,如果在数据库层级上为所有文档建立索引,那必然有很多目标类别以外的文档不包含搜索键,将这些无关文档的空值纳入索引浪费空间、影响使用索引的性能、完全没有意义。简单说就是,选择文档子集需要索引的帮助,建立索引必须针对某一类别的文档,类别通过选择文档子集建立。从这个死循环可见,在Notes数据库用选择公式查找文档时,无法利用索引,剩下的办法只有逐个文档应用公式中的条件,结果为真的就被选中。这种原始的做法注定选择文档子集很难满足应用程序按需动态查询文档的需求。无法指定字段和利用索引提高查询效率,Notes数据库设计带来的两个问题,用它的另一项设计——视图——解决了。视图依然用选择公式找出满足条件的文档集合,不同的是它将这个结果保存下来,以后数据库有文档新增、修改和删除发生时,视图只需将这些更新反映到文档集合上。视图不只是文档集合,它还定义一组列,列的值可以来自文档的某个字段,也可以由公式计算得出,或者取多个文档字段的聚合值,如总和、平均数。这样,集合中的每一个文档都对应一行由若干列组成的视图条目,整个集合在视图中看起来就像关系型数据库的一个表。准确地说,视图保存的是条目的集合。依据选择公式包含的文档有变动时,数据库就会更新对应的视图条目。对于选择文档子集无法利用索引的问题,视图通过将选中的文档集合以条目的形式保存下来、以后只进行增量更新解决了。选择公式没有指定字段的问题,视图通过定义列解决了。从这个角度看,Notes视图类似于关系型数据库中的物化视图。视图对于选择的文档和字段都是采取静态的方式,即预先定义和保存结果。视图通常用设计器(Domino Designer,Notes应用程序的开发环境)创建,程序中查询文档时只会利用现有的视图,而不会临时创建,因为通过代码来设定视图的选择公式和列是一个烦琐的过程,并且视图初次建立时计算所有的条目很耗时,而且一旦建立就会保存在数据库中,如果以后不需要就必须在这次用完后删除,所以只有在极特殊的情况下——如通过代码来修改应用程序或者视图的设计在开发阶段无法预知——才会用代码动态创建视图。2. 创建索引由于其静态之本性,视图不能也不是用来在程序中对文档进行动态查询的,而是为查询提供了基础。我们用一个配置文档的视图的例子来分析。Notes应用程序常用类似图5.3所示的文档来保存关键字值形式的通用设置。关键字和值分别保存在Keyword和Values字段中,Status用于标记该设置的状态,Category和Remark提供额外的类别和说明。文档用一个名为Keyword的表单打开。
图5.3一个保存关键字值形式的通用设置的文档,用名为Keyword的表单打开
在设计器中创建一个选择这类配置文档的视图。从图5.4可以看到,配置文档所用的表单名为Keyword。
图5.4在设计器中创建选择关键字配置文档的视图
视图的列则采用文档对应的字段值,如图5.5所示。
图5.5视图的列采用文档对应的字段值
保存视图后,数据库根据选择公式和列值的计算公式得到的视图条目,图5.6展示的是视图在客户端中的样貌。
图5.6视图在客户端中打开的样貌
程序访问视图中的文档,有两种情况。种是遍历视图中所有文档的集合,例如,定时程序批量处理库存文档。更普遍的是第二种,程序只需访问视图中的某个或某些特定文档。这就要求能够根据这些文档的特征从视图中快速查找到它们。我们回想起索引的用途正在于此。视图建立起某个类别的文档集合,前面提到的在Notes数据库层次创建索引的障碍在视图内已消除。视图的索引不是直接针对其中文档的某个字段建立的,而是在设计器中通过指定列的属性建立的。视图在Notes客户端中显示为文档列表,或者更准确地说是视图条目的列表。在某列上建立了索引后,视图就可以按该列排序或分类显示。因此,在设计器中为某列建立索引时,Notes没有使用索引的字眼,而是以设定该列为排序或分类的形式。可以设定多个列排序或分类,视图显示时就按照它们的顺序,先在列上排序(分类),再在第二列上排序(分类),以此类推,这就相当于建立了多个列组成的复合索引。除了这些自动按其排序显示的列,还可以设定某个列为“点击列标题排序”,这样建立的索引就是相互独立的,从而为一个视图建立多个索引。设计器中设置列排序的界面如图5.7所示。
图5.7为视图设置排序列
有了排序列,就可以用它们对应的字段作搜索键,向视图查询文档。本例中用代码View.GetAllDocumentsByKey(“Brand”)就可以获得配置文档视图中所有个排序列(对应Keyword字段)的值为Brand的文档。视图除了包括对应搜索键的排序列,通常数量上更多的是提取和计算普通字段值得出的列(如本例中的Values、Status和Category列)。这些普通列与排序列一起,可以看作组成了覆盖索引,前面提到的视图条目就相当于覆盖索引的条目。与查询文档一样,从视图也能遍历所有条目或根据搜索键查找条目,读取视图条目中保存的文档字段值因为省去了打开文档的开销,速度更快。排序列和普通列的值都保存在视图的索引中。与关系型数据库不同的是,覆盖索引中的普通字段值是由该索引独占的,假如多个覆盖索引包含同样的普通字段,它们的值就会重复于每一个索引中。Notes视图的普通列则是由排序列共享的,利用“点击列标题排序”设置建立多组排序列时,普通列无须任何改动,之后视图依据任何一组排序列排序显示或者查找条目时,普通列保存的数据都能被同样读取。在通过排序列创建的索引之外,视图还隐含其他一些索引。首先,在没有定义任何排序列的情况下,视图中的文档会按照它们的Notes ID排序,这背后就是视图的默认索引。Notes为一对多的关系提供了一种现成的实现,一个主文档可以对应多个响应文档,响应文档在特殊的$REF字段中保存主文档的UNID。在视图中,主文档和响应文档可以显示为层次结构,视图也会为它们之间的关系建立索引。3. 建立文档之间的关系如同关系型数据库用主键和外键建立表之间的关系,Notes数据库借助文档的UNID和视图索引建立文档之间的关系。Notes和其他文档型数据库一样倾向于在建模时去正规化,用单个文档包含尽可能大的实体,所以一对一的实体关系一般不会用两个文档表达。但是因为Notes不支持在文档中嵌入子文档,面对一对多的实体关系时,要么将“多”方的文档以字段的形式拆分保存在“一”方的文档里,用字段名称中的数字后缀区分所属的文档,如Name_1、Status_1、Body_1属于个文档,Name_2、Status_2、Body_2属于第二个文档,这样读写这些文档都需要额外的程序,既不自然也不方便; 要么就是用两类文档分别代表两种实体,并在其中一类文档中保存另一类文档的UNID。例如一个A1文档对应多个文档B1、B2、B3,就可以在这些B文档里用一个字段AID记录A1的UNID。这样从B自然可以很方便地获取到A。下面用类似JSON的格式列举了一些这样的文档。
A1: {UNID: “007”, field1: “value1”, field2: “value2”}
B1: {UNID: “002”, aid: “007”, field1: “value1”, field2: “value2”}
B2: {UNID: “003”, aid: “007”, field1: “value1”, field2: “value2”}
B3: {UNID: “004”, aid: “007”, field1: “value1”, field2: “value2”}
B4: {UNID: “005”, aid: “005”, field1: “value1”, field2: “value2”}
B5: {UNID: “006”, aid: “009”, field1: “value1”, field2: “value2”}
要反过来从A获得对应的B只需要在包含B文档的视图的AID列建立索引,就可以快速地根据某个A文档的UNID查找到存储了该值的B文档,如表5.21所示。
表5.21包含B文档的视图的示意列表
文档建立了索引的列一列二列三
后面各列的列值aidfield1field2B4005value1value2B1007value1value2B2007value1value2B3007value1value2B5009value1value2使用View.GetDocumentsByKey(“007”)语句就可以获得包括B1、B2、 B3三个文档的集合。换一种方式,在A文档中以一个字段保存B文档的UNID列表,然后在包括A文档的视图上该字段对应的列建立索引,也能在两类文档间建立同样的关系。多对多的关系可以借助多值字段和视图索引来维护。例如A1、A2文档分别都对应B1、B2、B3文档,可以任意选一类文档保存另一类文档的UNID,不妨设B文档里有一个字段AID记录了多个A文档的UNID。从B文档获取对应的A文档仍然很直接。
A1: {UNID: “007”, field1: “value1”, field2: “value2”}
A2: {UNID: “008”, field1: “value1”, field2: “value2”}
B1: {UNID: “002”, aid: [”007″, “008”], field1: “value1”, field2: “value2”}
B2: {UNID: “003”, aid: [”007″, “008”], field1: “value1”, field2: “value2”}
B3: {UNID: “004”, aid: [”007″, “008”], field1: “value1”, field2: “value2”}
B4: {UNID: “005”, aid: “005”, field1: “value1”, field2: “value2”}
B5: {UNID: “006”, aid: “009”, field1: “value1”, field2: “value2”}
同样地,在包含B文档的视图的AID列建立索引,这时的多值列会被当成具有多个单值的AID的B文档那样处理,如表5.22所示。用某个A文档的UNID来查询时就可以获得所有包含该值的B文档,如:
View.GetDocumentsByKey(“007”) -> B1, B2, B3,View.GetDocumentsByKey(“008”) -> B1, B2, B3
表5.22新的包含B文档的视图的示意列表
文档建立了索引的列一列二列三
后面各列的列值aidfield1field2B4005value1value2B1007value1value2B2007value1value2B3007value1value2B1008value1value2B2008value1value2B3008value1value2B5009value1value24. 列表展现文档前面都是从使用数据库的程序的角度分析视图的功能: 选择文档子集、创建索引以帮助查询、建立文档之间的关系。Notes视图的重要性不仅体现在程序开发中,它还直接作为应用程序界面必不可少的组成部分暴露给用户。在各种应用程序、特别是以数据为中心的应用程序中,普遍有展现数据实体的列表界面。在一般的程序开发中,列表的图形界面和为它展现的数据是分离的。本机GUI应用程序环境里,列表都是由GUI框架提供的控件; Web开发中,列表既可能是服务器端或浏览器端的控件,也可以由程序员用两端的开发语言加HTML临时自行拼接。列表展现的数据则由程序从数据库读取。Notes视图则集提供数据和展现它们两种功能于一身。视图的选择公式定义了它包括的文档集合,列值的定义规定了每条文档对应的视图条目由哪些数据项目组成,这两者合起来确定的索引外观上就像一个表格形式的数据集合,这一点上它像物化视图。在用户界面方面,视图的设置包括了列和列标题的字体、颜色、宽度,视图条目的背景色、网格线等显示上的各种细节,这一点上它又像列表控件。作为一种设计元件,视图这两方面的设置都以文档的形式保存在Notes数据库中,客户端打开某个视图时,读取它对应的索引,应用它包含的显示设置,呈现在用户眼前的就是一个展现文档的列表,双击列表的每一行都会打开它对应的文档(那些仅仅展现分类的行则会在折叠和展开状态间切换)。以Web的方式访问视图,过程也类似。除了以列表的形式展现文档,视图的排序、分类、计算列以及基于分类定义的总计、平均等统计列,还承担了部分SQL查询从句的功能,是在Notes应用程序中自动进行简单报表的途径(否则就只能写代码,逐条读取、计算数据并终展现)。5. CouchDB数据库的视图和MapReduce前面提到CouchDB的创建者Damien Katz曾经是Notes的开发人员。CouchDB显然是他不满意Notes而开发的,这在CouchDB的设计理念和技术选择的多方面都可以看到。不过有趣的是,Notes开发人员如果学习CouchDB又会发现很多地方十分熟悉。CouchDB仍然从Notes数据库继承了一些核心思想。CouchDB里的文档也和Notes文档一样,直接存放在数据库里,没有任何类似表的概念。视图同样是数据库的重要功能组件——甚至更加必不可少,因为它是从CouchDB数据库查询文档的渠道。CouchDB官方文档对视图作用的如下描述可以一点不差地移到Notes数据库上。 过滤数据库的文档以满足特定应用的需要。 从文档中提取数据,并排序展现。 建立索引以便查找文档。 利用索引建立文档之间的关系。 根据文档中的数据计算聚合值,如总和。然而,到了视图的建立和通过它的搜索,两者就大为不同了。一个CouchDB视图从“外观”上或者说查询它所返回的结果是一行行的数据,内部则是以B树的数据结构保存。每一行由键和值两部分组成,键可以是单值也可以是数组,视图的数据就根据来它们排序,相当于Notes视图里的排序列; 值同样也可以是单个或者数组,相当于Notes视图里的其他数据列。区别在于,视图文档的选择和列的定义不是分别由选择公式和列的值公式设置,而是由两个自定义的函数Map和Reduce生成。Map函数的参数就是文档,函数体内可以编写过滤逻辑,被选中的文档则被用于生成键和值,调用emit函数返回。例如,对存储在数据库中的博客文档,下面这个Map函数检查文档是否具备标题和创建日期,然后对合格的文档,以日期为键,标题为值生成视图数据。这个视图实际上就是以日期排序的标题列表。
function(doc) {
if(doc.date && doc.title) {
emit(doc.date, doc.title);
}
}
我们也可以添加更多的排序列和数据列:
function(doc) {
if(doc.date && doc.title) {
emit([doc.author, doc.date], [doc.title, doc.tags.join(‘, ‘)]);
}
}
视图就先按作者排序,同一个作者的博客再按日期排序,另外还有两列分别是博客的标题和标签列表。上面的例子都没有用到Reduce函数,它确实不是必需的,在需要计算多行数据的聚合值时就要用到它。参数分别为Map函数计算所得的键和值列表,还有一个可选的rereduce标记是否进行额外的reduce。如下面这个例子,Map筛选出类型为post的文档,然后对它的每个标签生成一行只有一个常数值1的数据。Reduce函数就计算出每个标签对应的文档总数。
function(doc) {
if (doc.type === ‘post’ && doc.tags && Array.isArray(doc.tags)) {
doc.tags.forEach(function(tag) {
emit(tag.toLowerCase(), 1);
});
}
}
function(keys, values) {
return sum(values);
}
和Notes视图一样,初次建立时要在数据库的所有文档上应用Map和Reduce函数,以后就只需对更新的文档运行。有了视图的排序列之后,CouchDB的查询就可以高效进行。CouchDB是为Internet应用而开发的,所以它不仅采纳了Web应用流行的RESTful模式作为接口,而且激进地以此为数据接口。所以对其的查询也不是通过传统的API,而是指定一个URL地址。对我们之前所建的按创建日期排序的博客视图,下面这个URL对应的GET请求就可以获得指定日期间隔内的所有博客标题。
http://server/database/_design/designdocname/_view/viewname?startkey=”2009/01/30″&endkey=”2010/02/01″
从这个简单例子已经可以看出,CouchDB在数据查询上要比Notes灵活和强大一些,如可以对某个字段进行范围查询,返回的结果只包含视图定义的列。6. 视图索引的更新展现文档集合、使用户能够快速查找文档,这些用途都要求索引的数据是的,也就是与它所基于的文档的数据是一致的。理论上更新索引在时间上有三种方案: 一是文档变动(新增、修改和删除)后立即进行,二是延迟至索引被读取时才检查是否需要更新,三是定时更新。方案一保证了索引与文档始终是一致的,但是加大了文档变动时数据库的负担。方案二也能保证每次读取的索引是的。与方案一相比优点是,在索引被读取前,相关的文档可能已经过多次修改。不同用户修改不同文档,单个用户多次修改某个文档,等等,各种情况都会发生。采用种方案时每次修改索引都需要更新,而方案二使得对索引两次被读取之间的所有修改(如果有),只需进行一次批量更新,从而减少开销。缺点是如果读取时发现索引需要更新,就增加了等待时间。第三种方案也是批量更新,并且更新的次数得到控制,因而所需的开销小,代价则是被读取时索引的数据有可能是过时的。方案三可以作为方案二的补充,在保证索引数据的前提下,减少更新的次数和用户读取时的等待时间。关系型数据库对表索引的更新一般采用方案一,对物化视图的索引则选择三种方案的都有。文档型数据库也是选择各异。MongoDB采用方案一,CouchDB采用方案二,Notes则混合使用方案二和三。下面就来看看Notes数据库为了兼顾资源消耗和用户体验的索引更新过程。1) 定时更新定时更新由服务器运行的Update和Updall两个任务来完成。两者的运行时间都可以在notes.ini里设置,默认情况下Update持续运行,Updall则在每天凌晨两点运行一次。Update维护两个队列(queue): 一个是即时队列(immediate queue),另一个是延迟队列(deferred queue)。队列里保存的是一条条某个数据库索引需更新的请求。请求的来源有几类: 复制器(replicator)复制后如果某个副本的文档有变动,就会发送一条请求到队列。邮件路由器(router)将新消息派送到某个邮箱后也会发送一条请求。频繁的则是一个用户在某个数据库里创建、修改或删除了文档,当他退出该数据库时,会发送一条请求到队列。复制和用户修改产生的请求都被发送到延迟队列,特殊的例如触发数据库全文索引及时更新的操作会发送一条请求到即时队列。Update每隔5秒(可以通过notes.ini的UPDATE_IDLE_TIME和UPDATE_IDLE_TIME_MS设置修改此默认值,下同)检查两个队列,对即时队列里的请求马上处理,对延迟队列里的则会比较同一个数据库的请求,先到达的请求之后的15分钟(Update_Suppression_Time)内所有的请求都会被忽略,这样做还是为了减少资源的消耗,15分钟以内对某个数据库的修改只会触发Update更新其索引一次。请求只记录数据库的路径,Update在处理时先要判断对那些视图的索引进行更新。此时Notes再次显示了尽量节省资源的特性。首先过去7天(UPDATE_ACCESS_FREQUENCY)内未被打开过的视图被忽略。接着根据索引上次更新时间和视图的选择条件搜索近发生更改过的并且包含在该视图中的文档,如果数量少于20(UPDATE_NOTE_MINIMUM),也不做更新。Updall任务因为是在凌晨进行,Notes显得大方一些。它不管队列,而是检查所有数据库的所有视图,需要更新的更新,需要重建的重建。2) 打开时更新定时更新不能保证索引被访问时数据是的,只能尽限度地减少那时更新的负担。用户通过客户端界面或程序访问视图时,会调用Notes API的NIFOpenCollection()函数打开索引(NIF意为Notes Indexing Facility,就是Notes索引部分的组件),如果索引数据不是的,NIFOpenCollection()就会调用NIFUpdateCollection()更新索引。我们每次在客户端里打开一个视图时,都会发生这个过程(除非以下说明的特殊情况)。如果距离上次Update更新该视图索引,发生更改的文档不多,这个过程就很快能完成。而如果这期间有大量文档发生改动,甚至该视图的索引不存在,就需要等待很长时间。自版本7后,Notes会在单独的后台线程中进行索引更新,用户界面因而不会被卡住,用户还能进行其他操作。为了提高视图打开的速度,或者说限度地减少索引更新的次数,Notes视图里还有控制打开时是否更新的选项,如图5.8所示。
图5.8视图更新的选项
可选的视图更新选项有: 1.自动,初次使用后(Auto,after first use)。2.自动(Automatic)。3.手动(Manual)。4.自动,多每n小时更新一次(Auto,at most every nhours)。手动意为打开视图时索引不自动更新,需按F9键更新。三种自动的区别在于,设为1或4时,如果一个视图未被打开过没有索引,Update和Updall运行时不会创建索引; 设为2时则会; 设为4时,如果索引在设置的近n小时内被更新过,表现得就和设为3时一样。索引在数据库里占据的空间不可小觑,数据库里如果视图多且复杂(列多,分类和排序多),索引占据的空间甚至会超过文档的空间。所以视图里又有丢弃索引的设置,可以设为数据库关闭后或者未被访问超过一定天数后丢弃。如果符合设置的条件,Updall任务负责删除索引。5.5可伸缩性一个计算机系统应对增长的工作量的能力,或者被扩充以应对增长的工作量的潜力称为可伸缩性(Scalability)。面对增长的负荷,可伸缩性良好的系统处理时间变化不大,或者能够通过某种扩充的方式有效提高吞吐量。对于任何可能面临工作量大幅增长的系统,可伸缩性都是一项重要的评判指标。数据库就是这样一种典型的系统,它面对的企业数据量和事务执行频率有巨大的弹性。因此良好的可伸缩性对数据库来说至关重要。一般地说,按照系统被扩充的方式,可伸缩性分为两类。 垂直可伸缩性(Vertical scalability)。通过给单个节点添加更多的资源(如CPU、内存),提高系统的性能,称为垂直伸张(Scale up),反之是垂直收缩(Scale down)。 水平可伸缩性(Horizontal scalability)。通过增加并行的系统数量(如运行某程序的计算机),来分担负荷,提高系统的吞吐量,称为水平伸张(Scale out),反之是水平收缩(Scale in)。垂直伸张很直接,简单说就是硬件升级带来的性能提升,电脑、手机用户对这项技能都不陌生。尽管硬件性能升级和成本下降的速度都很快,但是归功于分布式系统和虚拟化技术的进展,通过增加计算机的数量来扩展系统,变得相对来说成本更低、更方便,从而越来越流行和重要。实际上CPU本身硬件性能的提升,也从传统的提高内核的运算速度——垂直伸张,转变为增加内核的数量——水平伸张。两种伸张方式都可以被应用到数据库系统上。垂直伸张受硬件性能提升的限制,水平伸张凭借的数量扩展潜力更大,其中主要的途径是数据库划分。划分(Partition)将数据库分割成若干部分,各部分的数据互不相同,组合起来构成完整的数据集合。划分又有两种方向。 垂直划分(Vertical partitioning)将数据库完整存储的实体分割成较小的部分,每个部分保存于一个划分中。把数据库中的实体想象成平放在地上的一捆柴火,垂直划分就像用铡刀把它分成几截。垂直划分有些类似数据的正规化,但后者拆分实体是为了符合一系列范式,前者则是为了提升数据库的处理能力。以关系型数据库为例,在表已经正规化的状况下,垂直划分继续将表拆分。这样做有两方面的好处。当一个表有很多列,记录数量庞大时,表和它的索引占据的空间很大。将该表拆分成若干各包含其部分字段的较小的表,分散保存到位于不同计算机的划分里,使得数据库能容纳更大数量的记录。就查询的执行速度而言,将表中经常读取和很少用到的、频繁更新和基本不变的、短小的和很宽的列划分到不同的表,有助于提高查询的效率。当需要多个划分中的数据时,可以用JOIN查询或预先建立一个跨表的视图。总的来说,垂直划分适用于其字段使用有差异的表,还需要数据库有将拆分的表联合起来的能力。这就使它的可用性不如水平划分。 水平划分(Horizontal partitioning)将数据库中同一类实体的集合分成几个部分,每个部分保存于一个划分中。沿用上面的比喻,水平划分就像把柴火分成几小捆。水平划分的想法较之垂直划分更直观。一个人背不动一大捆柴,就分成几捆由几个人来背。一个Excel文件行数太多、速度太慢,就以某种标准拆分成几个文件。对数据库也是如此,水平划分可以收到容量和速度两方面的好处。而且因为每个划分中的记录都保持完整,就没有垂直划分面临的限制。以文档型数据库为例,某种类型的文档数量庞大时,文档和索引占用的空间都很大,读写的性能也受到影响。根据某个属性的取值,将文档划分成几个较小的集合,由该属性值可以决定容纳它的文档位于哪个划分。例如,根据车牌号的个字将全国所有汽车信息划分到省、直辖市、自治区。数据库划分被置于不同计算机。当一个查询可以被事先判断仅关系到某个划分中的数据时,查询只在容纳该划分的计算机上执行。如要查询合肥市某辆车的文档,就只需在安徽省的划分内进行。当一个查询涉及多个划分中的数据或者不确定哪个划分时,查询被广播到相关的计算机上,各自执行完成后,再将结果汇总。例如根据目击者的回忆,要查找车牌号尾号为168的某辆车,查询就被发送到各个省的数据库划分,每个划分得到的结果后再被汇总。通过增加划分,数据库的负荷能有效地被更多的计算机分担。关系型数据库因为正规化的设计,实体分散在多个相关的表中,其间有参照完整性约束。这就意味着水平划分时,每个划分要保持同样的一套模式,相关的记录要保存在同一个划分里以便读写,对被划分的表还要能维持与其他表之间的参照完整性。而文档型数据库中,文档倾向于容纳更完整更大的实体,不同类文档间也没有参照完整性约束,所以水平划分时只需简单将某类文档集合拆分,存放在分离的计算机上。实际上,水平划分或者叫分片(Sharding)是许多文档型数据库的基本功能,由此带来的高可伸缩性也是它相对于关系型数据库主要的优势。5.6可得性与BASE划分是分布式数据库的一种形式。分布式数据库的数据和程序分散在通过网络相连的多个节点上,每个节点上的数据库既有一定的独立性、能够独自承担某些工作,需要时也能够合作。分布式数据库的另一种形式是复制。互为副本的数据库中数据相同,部署在网络的不同节点上,通过复制来确保彼此数据的一致性。复制的首要意义是提高数据库的可得性(Availability)。某个节点宕机时,用户能够访问其他工作的节点。这对那些对服务的连续性和稳定性有较高要求的系统,是必不可少的。在对数据库的访问主要是读的场景里,每个副本都能独立提供服务,多个副本自然提高了总的吞吐量,相当于数据库的水平伸张。但是这样的性能提升有相应的代价,当数据被写入时,它必须被传播到所有副本,并保证更新的结果一致,这就提高了数据库系统的复杂性、增加了写入数据的成本、带来了副本间数据不一致的风险。为了平衡可得性和一致性,人们为分布式数据库设计了许多机制。简单的是主从(Masterslave)模式,多个副本中只有一个主(Master)副本,能接受数据写入,其他从(Slave)副本的数据只能被读取。主副本中的更新被即时或定时传播到各从副本,以保持数据的一致。主从模式提高了数据库被读时的可得性和吞吐量,但对于写则和集中式数据库没有区别、一旦主副本宕机就不能提供服务。与主从模式相对的是更复杂的多主(Multimaster)模式,每个副本都能处理读写请求,因此全面提高了数据库的可得性。多主模式处理并发写请求的策略有两类。类同步复制将某个副本收到的写请求即时更新到所有副本,这就要求数据库采取锁定、时间戳等并发机制,防止冲突产生。这类策略不仅实现起来复杂,而且要求副本之间时刻保持连通,一旦网络割裂,就可能发生用户虽然能连接到某个副本、但无法写入的状况。牺牲了可得性,成就了一致性。第二类异步复制允许副本先本地写入,再将更新延迟传播到其他副本。这样当多个副本中的同一条数据各自被修改时,就会产生冲突,因此异步复制必须提供解决冲突的机制,如依据修改的时间较新的版本替代较旧的版本、区分实体不同属性的更新以合并冲突、保留多个版本。没有一种方法能完美地解决所有的冲突,必要时数据库只能通知用户产生了复制冲突,由他们根据具体的应用场景来人工解决冲突。牺牲了一致性,成就了可得性。由以上讨论可见,分布式数据库的可得性和一致性存在内在的矛盾。实际上,依照CAP定理,分布式数据库多只能拥有以下三个性质中的两个: 一致性(Consistency)。 可得性(Availability)。 分隔容忍性(Partitiontolerance)。这里的一致性包含两层含义。首先是所有副本的数据保持相同。其次是对于数据库各副本的一系列写入,某个副本的状态始终和这些操作全部按照同样的时间顺序发生在本地一样。这两点合起来的效果就是,数据库的分布式对用户是透明的,用户无论访问哪个副本,都像访问的集中式数据库一样。可得性就是指数据库能够被使用。分隔容忍性指的是分布式数据库所跨越的网络出现割裂,也就是某些副本之间无法通信或者有副本所在的计算机发生宕机时,副本仍然能为各自连通的用户服务的特性。对大规模的分布式系统,网络始终没有分隔是无法做到的。因此在保证分隔容忍性的前提下,就不得不在一致性和可得性之间做选择。传统上,关系型数据库视一致性为优先,因为它的使用者多是对一致性有很高要求的财务、金融、生产系统等。随互联网兴起的许多针对普通用户的网站(如社交应用、内容网站)对数据的一致性没有那么严格的要求,相反为了给全球各地的用户提供时刻可用的服务,可得性更为重要。在这些领域,关系型数据库的ACID特性被新的BASE要求取代。ACID是关系型数据库处理事务的一组特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。原子性指构成数据库事务的一系列操作的结果不可分,也就是或者都成功完成,或者就像都没有发生一样。一致性指数据库在任何状态下都符合预设的完整性约束,如非空、、外键和各种自定义的约束(注意和前面一致性含义的差别)。隔离性指多个事务并发执行的结果和它们串行执行一样,而不会由于交叉执行导致结果的不确定。持久性指事务一旦执行完成,结果就永久保存,而不会因为断电等故障而丢失。文档型数据库不能保证事务的原子性,而只能确保单个文档单次更新的原子性,也就是说,对某个文档的一次更新或者成功,或者像没有发生一样,文档的状态不会居于两者之间。因为不存在关系型数据库那样的完整性约束,涉及文档的事务自然也就没有一致性。当数据只有一份时(不存在多个副本),文档型数据库也能做到事务并发执行的结果; 但存在多个副本时,因为对可得性的优先,就可能不满足隔离性。持久性作为保存数据的基本要求,无论什么类型的数据库都应该满足,文档型数据库也不例外。可见,用关系型数据库的ACID属性作为标准,文档型数据库天然就是不合格的,所以它要建立新的招牌BASE。BASE意为如下一组特性: 基本可使用(Basically available)、软状态(Soft state)和终一致(Eventually consistent)。基本可使用强调的是可得性,即使在副本之间的网络出现分隔时,数据库也要能完成读写操作。软状态指的是网络分隔时的写操作造成的副本间数据的不一致。终一致则要求当分隔消除时,副本通过复制终达成数据一致,包括可能需要人工介入的消除冲突。与关系型数据库不同,文档型数据库通常在可得性和一致性间偏向前者,因此BASE就成为文档型数据库的典型特性。5.7编程接口设计模式、读写数据,对数据库的使用通常是透过某种编程接口进行的。以关系代数为理论基础的关系型数据库有一套标准、通用的SQL语言,文档型数据库则因为模式的松散灵活、具体数据库内部实现和功能上的巨大差异,没有公共的编程接口,而是提供各自独特的查询语言。总的来说,这些语言在表达能力和简洁程度上都不如SQL。以下分别是MongoDB、CouchDB和Couchbase查询一段时间内的博客标题的代码样例。
//MongoDB在查询本身包含在一个JSON样式的文档里
db.blogs.find(
{ $and: [
{ created: { $gte: new Date(“2009-01-30”) } } ,
{ created: { $lte: new Date(“2010-02-1”) } }
] },
{ title: 1}
)
//CouchDB在查询采取HTTP RESTful的方式
http://server/database/_design/designdocname/_view/blogs?startkey=”2009/01/30″&endkey=”2010/02/01″
//Couchbase既可以采用和CouchDB同样的方式,也可以使用一种类似SQL的N1QL
//声明式语言
SELECT title FROM blogs WHERE created >= MILLIS(“2009/01/30”) AND created <= MILLIS(“2010/02/01”)
使用面向对象的编程语言搭配关系型数据库开发时的一大麻烦是,对象和记录不能互相匹配。对象内的字段可以容纳多值、对其他对象的引用,这样的层次结构在关系型数据库中被分散到多个依靠主键和外键关联的表。当程序将对象存储到数据库,和从数据库读取对象时,就必须在数据的两种表达模式之间进行不轻松的转换,即所谓的对象关系映射(Object Relational Mapping,ORM)。而使用文档型数据库建模时,能够容纳层次数据的文档,与对象在结构上更接近,因此在开发时两者之间的转换也更容易。5.8总结文档型数据库和关系型数据库各有所长,各自有适宜的应用场景。两者间某一特性和能力上的优劣对比,并非设计上高下有别,而是不同理念的必然结果。就像我们不能说大货车比小汽车耗油是它的缺陷,小汽车比大货车载货量少是它的不足。一种设计在某些方面追求了,在其他方面就可能不如对手。关系型数据库中的记录遵循严格的模式; 倾向于通过正规化保存简单的实体并尽量避免冗余; 事务具有ACID特性; 更多地采用垂直伸张; 副本之间一般更强调一致性; 拥有统一的强大的查询语言SQL; 关系模型与编程语言中的对象需要相互转换。文档型数据库中的文档无须服从统一的模式; 倾向于用一个文档表达相关的实体,必要时用冗余来换取读的速度; 易于采用水平伸张提升性能; 多个副本时更强调可得性; 满足BASE要求; 查询语言不统一; 编程语言中的对象从数据库读写时更容易。两者的这些特点使得关系型数据库适宜数据结构化、实体多且关系复杂、在ACID方面要求严苛的场合,例如传统的金融、商务、生产等领域; 文档型数据库则适宜数据半结构化、实体较少关系简单、强调可得性和可伸缩性的场景,例如搜索引擎、社交应用、内容管理等。后,关系型数据库和文档型数据库不是水油不溶的,彼此都在相互借鉴,界限也变得模糊。例如,许多关系型数据库也支持XML或JSON等形式的半结构化数据,加强水平划分的能力; 而文档型数据库则一直在操作的原子性、写入文档时的校验、JOIN功能、更强大和方便的查询语言等方面借鉴关系型数据库。






评论
还没有评论。