读写分离 CQRS

读写分离是数据库扩展最简单实用的玩法了,这种方法针对读多写少的业务场景还是很管用的,而且还可以有效地把业务做相应的隔离。

如下图所示,数据库只有一个写库,有两个读库,所有的服务都写一个数据库。对于读操作来说,服务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

分库分表Sharding

一般来说,影响数据库最大的性能问题有两个,一个是对数据库的操作,一个是数据库中数据的大小。

对于前者,我们需要从业务上来优化。一方面,简化业务,不要在数据库上做太多的关联查询,而对于一些更为复杂的用于做报表或是搜索的数据库操作,应该把其移到更适合的地方。比如,用ElasticSearch来做查询,用Hadoop或别的数据分析软件来做报表分析。

对于后者,如果数据库里的数据越来越多,那么也会影响我们的数据操作。而且,对于我们的分布式系统来说,后端服务都可以做成分布式的,而数据库最好也是可以拆开成分布式的。读写分离也因为数据库里的数据太多而变慢,于是,分库分表就成了我们必须用的手段。

上面的图片是一个分库的示例。其中有两个事,这里需要提一下,一个是关于分库的策略,一个是关于数据访问层的中间件。

关于分库的策略。我们把数据库按某种规则分成了三个库。比如,或是按地理位置,或是按日期,或是按某个范围分,或是按一种哈希散列算法。总之,我们把数据分到了三个库中。

关于数据访问层。为了不让我们前面的服务感知到数据库的变化,我们需要引入一个叫"数据访问层"的中间件,用来做数据路由。但是,老实说,这个数据访问层的中间件很不好写,其中要有解析SQL语句的能力,还要根据解析好的SQL语句来做路由。但即便是这样,也有很多麻烦事。

比如,我要做一个分页功能,需要读一组顺序的数据,或是需要做Max/Min/Count这样的操作。于是,你要到三个库中分别求值,然后在数据访问层这里再合计处理返回。但即使是这样,你也会遇到各种令人烦恼的事,比如一个跨库的事务,你需要走XA这样的两阶段提交的操作,这样会把数据库的性能降到最低的。

为了避免数据访问层的麻烦,分片策略一般如下。

上面是最常见的分片模式,但是你还应考虑应用程序的业务要求及其数据使用模式。这里请注意几个非常关键的事宜。

  1. 数据库分片必须考虑业务,从业务的角度入手,而不是从技术的角度入手,如果你不清楚业务,那么无法做出好的分片策略。

  2. 请只考虑业务分片。请不要走哈希散列的分片方式,除非有个人拿着刀把你逼到墙角,你马上就有生命危险,你才能走哈希散列的分片方式。

数据库扩展的设计重点

先说明一下,这里没有讲真正数据库引擎的水平扩展的方法,我们只是在业务层上谈了一下数据扩展的两种方法。关于数据库引擎的水平扩展,你可能看一下我之前发过的《分布式数据调度的相关论文》一文中的AWS Aurora和Google Spanner的相关论文中提到的那些方法。

接下来,我们说一下从业务层上把单体的数据库给拆解掉的相关重点。

首先,你需要把数据库和应用服务一同拆开。也就是说,一个服务一个库,这就是微服务的玩法,也是Amazon的服务化的玩法——服务之间只能通过服务接口通讯,不能通过访问对方的数据库。在Amazon内,每个服务都会有一个自己的数据库,比如地址库、银行卡库等。这样一来,你的数据库就会被"天生地"给拆成服务化的,而不是一个单体的库。

我们要知道,在一个单体的库上做读写分离或是做分片都是一件治标不治本的事,真正治本的方法就是要和服务一起拆解。

当数据库也服务化后,我们才会在这个小的服务数据库上进行读写分离或分片的方式来获得更多的性能和吞吐量。这是整个设计模式的原则——先做服务化拆分,再做分片。

对于分片来说,有两种分片模式,一种是水平分片,一种是垂直分片。水平分片就是我们之前说的那种分片。而垂直分片是把一张表中的一些字段放到一张表中,另一些字段放到另一张表中。垂直分片主要是把一些经常修改的数据和不经常修改的数据给分离开来,这样在修改某个字段的数据时,不会导致其它字段的数据被锁而影响性能。比如,对于电商系统来说,商品的描述信息不常改,但是商品的库存和价格经常改,所以,可以把描述信息和库存价格分成两张表,这样可以让商品的描述信息的查询更快。

我们所说的sharding更多的是说水平分片。水平分片需要有以下一些注意事项。

小结

好了,我们来总结一下今天分享的主要内容。首先,我介绍了单主库多从库的读写分离,并进一步用CQRS把语义区分成命令和查询。命令的执行可以变成事件溯源方式,从而得到更大的并发和性能。随后我讲了分库分表的策略及其数据访问层所做的抽象。最后,我指出了数据库扩展的设计重点。下篇文章中,我们将会聊聊秒杀这个特定的场景,希望对你有帮助。

也欢迎你在留言区分享一下你的数据库做过哪些形式的扩展?设计中有哪些方面的考量?

文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。