无论是数据库读/写分离(见2.2节)、本地缓存(见2.3节)还是分布式缓存(见2.4节),其本质上都是读/写分离,这也是在微服务架构中经常被提及的CQRS模式。
CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种将数据的读取操作与更新操作分离的模式。query指的是读取操作,而command是对会引起数据变化的操作的总称,新增、删除、修改这些操作都是命令。
2.5.1 CQRS的简要架构与实现为了避免引入微服务领域驱动设计的相关概念,图2-8给出了CQRS的简要架构。
当业务服务收到客户端发起的command请求(即写请求)时,会将此请求交给写数据存储来处理。
写数据存储完成数据变更后,将数据变更消息发送到消息队列。
读数据存储负责监听消息队列,当它收到数据变更消息后,将数据写入自身。
当业务服务收到客户端发起的query请求(即读请求)时,将此请求交给读数据存储来处理。
读数据存储将此请求希望访问的数据返回。
写数据存储、读数据存储、数据传输通道均是较为宽泛的代称,其中写数据存储和读数据存储在 ...
由于本地缓存把数据缓存在服务进程的内存中,不需要网络开销,故而性能非常高。但是把数据缓存到内存中也有较多限制,举例如下。
无法共享:多个服务进程之间无法共享本地缓存。
编程语言限制:本地缓存与程序绑定,用Golang语言开发的本地缓存组件不可以直接为用Java语言开发的服务器所使用。
可扩展性差:由于服务进程携带了数据,因此服务是有状态的。有状态的服务不具备较好的可扩展性。
内存易失性:服务进程重启,缓存数据全部丢失。
我们需要一种支持多进程共享、与编程语言无关、可扩展、数据可持久化的缓存,这种缓存就是分布式缓存。
2.4.1 分布式缓存选型主流的分布式缓存开源项目有Memcached和Redis,两者都是优秀的缓存产品,并且都具有缓存数据共享、与编程语言无关的能力。不过,相对于Memcached而言,Redis更为流行,主要体现如下。
数据类型丰富:Memcached仅支持字符串数据类型缓存,而Redis支持字符串、列表、集合、哈希、有序集合等数据类型缓存。
数据可持久化:Redis通过RDB机制和AOF机制支持数据持久化,而Memcached没有数据持久化能力。
高可用性: ...
在计算机世界中,缓存(Cache)无处不在,如CPU缓存、DNS缓存、浏览器缓存等。值得一提的是,Cache在我国台湾地区被译为“快取”,更直接地体现了它的用途:快速读取。缓存的本质是通过空间换时间的思路来保证数据的快速读取。
业务服务一般需要通过网络调用向其他服务或数据库发送读数据请求。为了提高数据的读取效率,业务服务进程可以将已经获取到的数据缓存到本地内存中,之后业务服务进程收到相同的数据请求时就可以直接从本地内存中获取数据返回,将网络请求转化为高效的内存存取逻辑。这就是本地缓存的主要用途。在本书后面的核心服务设计篇中会大量应用本地缓存,本节先重点介绍本地缓存的技术原理。
2.3.1 基本的缓存淘汰策略虽然缓存使用空间换时间可以提高数据的读取效率,但是内存资源的珍贵决定了本地缓存不可无限扩张,需要在占用空间和节约时间之间进行权衡。这就要求本地缓存能自动淘汰一些缓存的数据,淘汰策略应该尽量保证淘汰不再被使用的数据,保证有较高的缓存命中率。基本的缓存淘汰策略如下。
FIFO(First In First Out)策略:优先淘汰最早进入缓存的数据。这是最简单的淘汰策略,可以基于队列实现 ...
大部分互联网应用都是读多写少的,比如刷帖的请求永远比发帖的请求多,浏览商品的请求永远比下单购买商品的请求多。数据库承受的高并发请求压力,主要来自读请求。
我们可以把数据库按照读/写请求分成两类:
专门负责处理写请求的数据库(写库)
专门负责处理读请求的数据库(读库)
让所有的写请求都落到写库,写库将写请求处理后的最新数据同步到读库,所有的读请求都从读库中读取数据。这就是数据库读/写分离的思路。
数据库读/写分离使大量的读请求从数据库中分离出来,减少了数据库访问压力,缩短了请求响应时间。
2.2.1 读/写分离架构我们通常使用数据库主从复制技术实现读/写分离架构,将数据库主节点Master作为“写库”,将数据库从节点Slave作为“读库”,一个Master可以与多个Slave连接,如图2-2所示。
市面上各主流数据库都实现了主从复制技术,参见1.7.3节介绍的MySQL数据库的主从复制原理。
2.2.2 读/写请求路由方式在数据库读/写分离架构下,把写请求交给Master处理,而把读请求交给Slave处理。那么由 ...
高并发意味着系统要应对海量请求。从笔者多年的面试经验来看,很多面试者在面对“什么是高并发架构”的问题时,往往会粗略地认为一个系统的设计是否满足高并发架构,就是看这个系统是否可以应对海量请求。再细问具体的细节时,回答往往显得模棱两可,比如每秒多少个请求才是高并发请求、系统的性能表现如何、系统的可用性表现如何,等等。为了可以清晰地评判一个系统的设计是否满足高并发架构,在正式给出通用的高并发架构设计方案前,我们先要厘清以下三点:
形成高并发系统的必要条件
高并发系统的衡量指标
高并发场景分类
2.1.1 形成高并发系统的必要条件形成高并发系统主要有三大必要条件。
高性能:性能代表一个系统的并行处理能力,在同样的硬件设备条件下,性能越高,越能节约硬件资源;同时性能关乎用户体验,如果系统响应时间过长,用户就会产生抱怨。
高可用性:系统可以长期稳定、正常地对外提供服务,而不是经常出故障、宕机、崩溃。
可扩展性:系统可以通过水平扩容的方式,从容应对请求量的日渐递增乃至突发的请求量激增。
我们可以将形成高并发系统的必要条件类比为一个篮球运动员的各项属性:
高性能:相当于这个球员在赛场上的表 ...
大部分互联网应用使用“同城双活”架构就可以承担海量用户请求与保障后台高可用了,但是如果你的应用不是仅面向一个国家,而是面向全球(如Facebook、Instagram等), 那么“同城双活”架构就会带来一些问题。
用户访问延迟问题。比如我们在泰国曼谷建设了“同城双活”机房,泰国、日本、 韩国、马来西亚等附近国家用户的访问请求能被快速响应,而欧洲用户的访问请求只能“跨越山河大海”才能接入机房(因为欧洲距离曼谷物理位置太远),这就会造成访问延迟大大增加,用户会明显感觉到应用卡顿。
数据合规问题。很多国家非常注重互联网用户隐私数据安全,它们通常要求应用将本国用户的数据独立存储到本国机房。“同城双活”架构最多只能满足一个国家的数据合规要求。
灾难问题。如果部署机房的国家发生了战争、暴乱、自然灾害等,则可能导致机房被破坏,进而导致整个应用在全球范围内不可用。
全球级互联网应用后台一般采用多国部署机房的架构:在全球范围内筛选几个国家和城市部署机房并负责接入附近国家用户的访问请求,各个机房之间通过数据复制保证它们都有全球全量数据,这就是“异地多活”架构。
1.14.1 架构要点假设我们在全球 ...
既然处于空闲状态的备机房既浪费资源又不确定可用,那么让备机房也与主机房一样日常对外提供服务不就好了吗?这样一来,机房资源被利用起来,也有了承接用户流量的实战经验,这就是“同城双活”架构。
1.13.1 存储层改造“同城双活”架构与主备机房架构类似,只不过我们需要做一些改造:将两个机房的接入层IP地址都配置到DNS。这样做的效果是两个机房都能负责一部分用户请求,形成 了“双活”的局面。
如图1-58所示,A机房与B机房组成“双活”机房,并由DNS负责决定将用户请求分流到哪个机房。从对外提供服务的角度来说,两者是对等的。此外,两个机房的服务可以通过专线实现互相访问,即“跨机房调用”。
但是这里有一个核心问题还没有解决:在主备机房架构下,备机房(即B机房)存储层的数据库是A机房的从库,从库意味着只能读数据,不可写数据,也就是B机房无法处理写请求,这样的“双活”无法达到我们的预期。
我们接着对存储层的访问做一些改造:B机房的所有写数据请求在访问存储层的数据库时,直接跨机房访问A机房对应的主库——无论是将数据写入Redis、MySQL、MongoDB还是写入其他存储系统,如图1-59所示。
...
除了要考虑机房内的各个组件,也要考虑机房自身的高可用问题。使用单机房架构搭建互联网应用后台,虽然接入层、业务服务层、存储层均具备高可用架构,但由于机房是单点,所以还是避免不了机房故障会造成整个应用无法访问的问题。可能造成机房级别故障的情况有人为破坏、自然灾害等,比如断电、火灾、机房核心交换机故障、计算机病毒等。
种种不可控因素导致的机房故障,通常会造成整个应用后台不可用,这对于大部分公司来说都难以接受。当应用的用户量级已经较为可观时,解决机房单点问题便成为工程师迫在眉睫的工作。
解决机房单点问题最简单的方案是建设主备机房:
在主机房所在的城市再建设一个备机房,整个备机房的内部完全复制主机房架构,在正常情况下仅主机房工作。
在存储层,备机房数据库被部署为主机房数据库的从库,主机房与备机房通过专线做存储层数据复制。
专线是一种特殊网线,就是为某个机构拉一条独立的专用网线,也就是建立一个独立的局域网,让用户的数据传输变得可靠、可信。专线的优点是安全性好,网络通信质量高;不过,专线价格相对较高,而且需要专业人员管理。专线被广泛应用于军事、银行等场景。
专线是主备机房数据复制的核心通道。为了 ...
消息队列(Message Queue)是分布式系统中最重要的中间件之一,在服务架构设计中被广泛使用。
1.11.1 通信模式与用途消息中间件构建了这样的通信模式:
一条消息由生产者创建,并被投递到存放消息的队列中;
消费者从队列中读取这条消息,于是生产者与消费者完成了一次通信。
这种通信模式在现实生活中很常见,典型的例子是E-mail通信:
住在北京的张三想把一个重要但不紧急的消息告诉住在上海的李四,张三给李四打电话,但是李四正在忙其他的事情而未接电话,张三为了把消息传达给李四,只能不停地拨打电话直到李四接听,这无疑浪费了张三大量的时间。于是,张三选择将消息使用E-mail的方式发送,他只需要把邮件投递到李四的收件箱中就可以去忙其他的事情了,而不用去管李四是否繁忙,E-mail系统保证只要李四空闲下来查看收件箱,就必然会收到张三的消息。对于消息中间件而言,张三和李四分别是生产者和消费者,E-mail系统就是消息队列。
消息队列的通信模式为生产者和消费者带来了便捷性,如下所述。
生产者将消息投递到消息队列中就单方面完成了消息通信,比如张三只需要发送邮件,而不用等待李四阅读邮件。
消 ...
这里我们简单介绍一下其他常见的NoSQL数据库及其适用的场景,其中部分数据库会在后续服务设计章节中正式使用时再做详细介绍。
1.10.1 文档数据库文档数据库的典型代表是MongoDB和CouchDB。文档数据库普遍采用JSON格式来存储数据,而不是采用僵硬的行和列结构,其好处是可以解决关系型数据库表结构(Schema)扩展不方便的问题,以及可以存储和读/写任何格式的数据。文档数据库与键值存储系统很类似,只不过值存储的内容是文档信息。文档数据库具有很好的可扩展性。
文档数据库适用的场景如下。
数据量大,且数据增长很快的业务场景。
数据字段定义不明确,且字段在不断变化、无法统一的场景。比如商品参数信息存储,电子设备商品参数有内存大小、电池容量等,服装商品参数有尺码、面料等。
文档数据库不适用的场景如下。
需要支持事务,文档数据库无法保证在一个事务中修改多个文档的原子性。
需要支持复杂查询,例如join语句。
1.10.2 列式数据库列式数据库的典型代表是BigTable、HBase等。关系型数据库按照行来存储数据,所以它也被称为“行式数据库”;而列式数据库按照列来存储数 ...