读写分离是数据库扩展最简单实用的玩法了,这种方法针对读多写少的业务场景还是很管用的,而且还可以有效地把业务做相应的隔离。
如下图所示,数据库只有一个写库,有两个读库,所有的服务都写一个数据库。对于读操作来说,服务 A 和服务 B 走从库 A,服务 D 和服务 E 走从库 B,服务 C 在从库 A 和从库 B 间做轮询。
这样的方法好处是:
这样的方法不好的地方是:
综上所述,一般来说,这样的玩法主要是为了减少读操作的压力。
当然,这样的读写分离看上去有点矬,那么,我们还是为之找一个更靠谱的设计——CQRS。关于 CQRS,我在这里只做一个简单的介绍,更多的细节你可以上网自行 Google。
CQRS 全称 Command and Query Responsibility Segregation,也就是命令与查询职责分离。其原理是,用户对于一个应用的操作可以分成两种,一种是 Command 也就是我们的写操作(增,删,改),另一种是 Query 操作(查),也就是读操作。Query 操作基本上是在做数据整合显现,而 Command 操作这边会有更重的业务逻辑。分离开这两种操作可以在语义上做好区分。
这样一来,可以带来一些好处。
如果把 Command 操作变成 Event Sourcing,那么只需要记录不可修改的事件,并通过回溯事件得到数据的状态。于是,我们可以把写操作给完全简化掉,也变成无状态的,这样可以大幅度降低整个系统的副作用,并可以得到更大的并发和性能。
文本中有 Event Sourcing 和 CQRS 的架构示意图。
图片来源 - CQRS and Event Sourcing Application with Cassandra
一般来说,影响数据库最大的性能问题有两个,一个是对数据库的操作,一个是数据库中数据的大小。
对于前者,我们需要从业务上来优化。一方面,简化业务,不要在数据库上做太多的关联查询,而对于一些更为复杂的用于做报表或是搜索的数据库操作,应该把其移到更适合的地方。比如,用 ElasticSearch 来做查询,用 Hadoop 或别的数据分析软件来做报表分析。
对于后者,如果数据库里的数据越来越多,那么也会影响我们的数据操作。而且,对于我们的分布式系统来说,后端服务都可以做成分布式的,而数据库最好也是可以拆开成分布式的。读写分离也因为数据库里的数据太多而变慢,于是,分库分表就成了我们必须用的手段。
上面的图片是一个分库的示例。其中有两个事,这里需要提一下,一个是关于分库的策略,一个是关于数据访问层的中间件。
关于分库的策略。我们把数据库按某种规则分成了三个库。比如,或是按地理位置,或是按日期,或是按某个范围分,或是按一种哈希散列算法。总之,我们把数据分到了三个库中。
关于数据访问层。为了不让我们前面的服务感知到数据库的变化,我们需要引入一个叫 “ 数据访问层 ” 的中间件,用来做数据路由。但是,老实说,这个数据访问层的中间件很不好写,其中要有解析 SQL 语句的能力,还要根据解析好的 SQL 语句来做路由。但即便是这样,也有很多麻烦事。
比如,我要做一个分页功能,需要读一组顺序的数据,或是需要做 Max/Min/Count 这样的操作。于是,你要到三个库中分别求值,然后在数据访问层这里再合计处理返回。但即使是这样,你也会遇到各种令人烦恼的事,比如一个跨库的事务,你需要走 XA 这样的两阶段提交的操作,这样会把数据库的性能降到最低的。
为了避免数据访问层的麻烦,分片策略一般如下。
上面是最常见的分片模式,但是你还应考虑应用程序的业务要求及其数据使用模式。这里请注意几个非常关键的事宜。
先说明一下,这里没有讲真正数据库引擎的水平扩展的方法,我们只是在业务层上谈了一下数据扩展的两种方法。关于数据库引擎的水平扩展,你可能看一下我之前发过的《分布式数据调度的相关论文》一文中的 AWS Aurora 和 Google Spanner 的相关论文中提到的那些方法。
接下来,我们说一下从业务层上把单体的数据库给拆解掉的相关重点。
首先,你需要把数据库和应用服务一同拆开。也就是说,一个服务一个库,这就是微服务的玩法,也是 Amazon 的服务化的玩法——服务之间只能通过服务接口通讯,不能通过访问对方的数据库。在 Amazon 内,每个服务都会有一个自己的数据库,比如地址库、银行卡库等。这样一来,你的数据库就会被 “ 天生地 ” 给拆成服务化的,而不是一个单体的库。
我们要知道,在一个单体的库上做读写分离或是做分片都是一件治标不治本的事,真正治本的方法就是要和服务一起拆解。
当数据库也服务化后,我们才会在这个小的服务数据库上进行读写分离或分片的方式来获得更多的性能和吞吐量。这是整个设计模式的原则——先做服务化拆分,再做分片。
对于分片来说,有两种分片模式,一种是水平分片,一种是垂直分片。水平分片就是我们之前说的那种分片。而垂直分片是把一张表中的一些字段放到一张表中,另一些字段放到另一张表中。垂直分片主要是把一些经常修改的数据和不经常修改的数据给分离开来,这样在修改某个字段的数据时,不会导致其它字段的数据被锁而影响性能。比如,对于电商系统来说,商品的描述信息不常改,但是商品的库存和价格经常改,所以,可以把描述信息和库存价格分成两张表,这样可以让商品的描述信息的查询更快。
我们所说的 sharding 更多的是说水平分片。水平分片需要有以下一些注意事项。
好了,我们来总结一下今天分享的主要内容。首先,我介绍了单主库多从库的读写分离,并进一步用 CQRS 把语义区分成命令和查询。命令的执行可以变成事件溯源方式,从而得到更大的并发和性能。随后我讲了分库分表的策略及其数据访问层所做的抽象。最后,我指出了数据库扩展的设计重点。下篇文章中,我们讲述秒杀。希望对你有帮助。
也欢迎你分享一下你的数据库做过哪些形式的扩展?设计中有哪些方面的考量?