文章目录
任何一个系统,从单机到分布式,从前端到后台,功能和逻辑各不相同,但干的只有两件事:读和写。而每个系统的业务特性可能都不一样,有的侧重读、有的侧重写,有的两者兼备,本节主要探讨在不同业务场景下存储读写的一些方法论。
1.读写分离
大多数业务都是读多写少,为了提高系统处理能力,可以采用读写分离的方式将主节点用于写,从节点用于读,如下图所示。
读写分离架构有以下几个特点:
(1)数据库服务为主从架构;
(2)主节点负责写操作,从节点负责读操作;
(3)主节点将数据复制到从节点;
基于读写分离思想,可以设计出多种主从架构,如主-主-从、主-从-从等。主从节点也可以是不同的存储,如MySQL+Redis。
读写分离的主从架构一般采用异步复制,会存在数据复制延迟的问题,适用于对数据一致性要求不高的业务。可采用以下几个方式尽量避免复制滞后带来的问题。
- 写后读一致
即读自己的写,适用于用户写操作后要求实时看到更新。典型的场景是,用户注册账号或者修改账户密码后,紧接着登录,此时如果读请求发送到从节点,由于数据可能还没同步完成,用户登录失败,这是不可接受的。针对这种情况,可以将自己的读请求发送到主节点上,查看其他用户信息的请求依然发送到从节点。
- 二次读取
优先读取从节点,如果读取失败或者跟踪的更新时间小于某个阀值,则再从主节点读取。
- 区分场景
关键业务读写主节点,非关键业务读写分离。
- 单调读
保证用户的读请求都发到同一个从节点,避免出现回滚的现象。如用户在 M 主节点更新信息后,数据很快同步到了从节点 S1,用户查询时请求发往 S1,看到了更新的信息。接着用户再一次查询,此时请求发到数据同步没有完成的从节点 S2,用户看到的现象是刚才的更新的信息又消失了,即以为数据回滚了。
2.分库分表
读写分离虽然可以明显的提示查询的效率,但是无法解决更高的并发写入请求的场景,这时候就需要进行分库分表,提高并发写入的能力。
通常,在以下情况下需要进行分库分表:
(1)单表的数据量达到了一定的量级(如 MySQL 一般为千万级),读写的性能会下降。这时索引也会很大,性能不佳,需要分解单表。
(2)数据库吞吐量达到瓶颈,需要增加更多数据库实例来分担数据读写压力。
分库分表按照特定的条件将数据分散到多个数据库和表中,分为垂直切分和水平切分两种模式。
- 垂直切分
按照一定规则,如业务或模块类型,将一个数据库中的多个表分布到不同的数据库上。以电商平台为例,将商品数据、订单数据、用户数据分别存储在不同的数据库上,如下图所示:
优点:
(1)切分规则清晰,业务划分明确;
(2)可以按照业务的类型、重要程度进行成本管理,扩展也方便;
(3)数据维护简单。
缺点:
(1)不同表分到了不同的库中,无法使用表连接Join。不过在实际的业务设计中,也基本不会用到 Join 操作,一般都会建立映射表通过两次查询或者写时构造好数据存到性能更高的存储系统中。
(2)事务处理复杂,原本在事务中操作同一个库的不同表不再支持。这时可以采用柔性事务或者其他分布式事物方案。
- 水平切分
按照一定规则,如哈希或取模,将同一个表中的数据拆分到多个数据库上。可以简单理解为按行拆分,拆分后的表结构是一样的。如用户信息记录,日积月累,表会越来越大,可以按照用户 ID 或者用户注册日期进行水平切分,存储到不同的数据库实例中。
优点:
(1)切分后表结构一样,业务代码不需要改动;
(2)能控制单表数据量,有利于性能提升。
缺点:
(1)Join、count、记录合并、排序、分页等问题需要跨节点处理;
(2)相对复杂,需要实现路由策略;
综上所述,垂直切分和水平切分各有优缺点,通常情况下这两种模式会一起使用。
3.动静分离
动静分离将经常更新的数据和更新频率低的数据进行分离。最常见于 CDN,一个网页通常分为静态资源(图片/JS/CSS等)和动态资源(JSP、PHP等),采取动静分离的方式将静态资源缓存在 CDN 边缘节点上,只需请求动态资源即可,减少网络传输和服务负载。
在数据库和 KV 存储上也可以采取动态分离的方式。动静分离更像是一种垂直切分,将动态和静态的字段分别存储在不同的库表中,减小数据库锁的粒度,同时可以分配不同的数据库资源来合理提升利用率。
4.冷热分离
冷热分离可以说是每个存储产品和海量业务的必备功能,MySQL、ElasticSearch 等都直接或间接支持冷热分离。将热数据放到性能更好的存储设备上,冷数据下沉到廉价的磁盘,从而节约成本。
5.重写轻读
基本思路就是写入数据时多写点(冗余写),降低读的压力。
社交平台中用户可以互相关注,查看关注用户的最新消息,形成 Feed 流。
用户查看 Feed 流时,系统需要查出此用户关注了哪些用户,再查询这些用户所发的消息,按时间排序。
为了满足高并发的查询请求,可以采用重写轻读,提前为每个用户准备一个收件箱。
每个用户都有一个收件箱和一个发件箱。比如一个用户有1000个粉丝,他发布一条消息时,写入自己的发件箱即可,后台异步的把这条消息放到那1000个粉丝的收件箱中。
这样,用户读取 Feed 流时就不需要实时查询聚合了,直接读自己的收件箱就行了。把计算逻辑从"读"移到了"写"一端,因为读的压力要远远大于写的压力,所以可以让"写"帮忙干点活儿,提升整体效率。
上面展示了一个重写轻度的一个例子,在实际应用中可能会遇到一些问题。如:
(1)写扩散:这是个写扩散的行为,如果一个大 V 的粉丝很多,这写扩散的代价也是很大的,而且可能有些人万年不看朋友圈甚至屏蔽了朋友。需要采取一些其他的策略,如粉丝数在某个范围内是才采取这种方式,数量太多采取推拉结合和分析一些活跃指标等。
(2)信箱容量:一般来说查看 Feed 流(如微信朋友圈)不会不断地往下翻页查看,这时候应该限制信箱存储条目数,超出的条目从其他存储查询。
6.数据异构
数据异构顾名思义就是存储不同结构的数据,有很多种含义:
- 数据格式的异构
数据的存储格式不同,可以是关系型(如 MySQL、SQL Server、DB2等),也可以是 KV 格式(如 Redis、Memcache等),还可以是文件行二维数据(如 txt、CSV、XLS等)。
- 数据存储地点的异构
据存储在分散的物理位置上,此类情况大多出现在大型机构中,如销售数据分别存储在北京、上海、日本、韩国等多个分支机构的本地销售系统中。
- 数据存储逻辑的异构
相同的数据按照不同的逻辑来存储,比如按照不同索引维度来存储同一份数据。
这里主要说的是按照不同的维度建立索引关系以加速查询。如京东、天猫等网上商城,一般按照订单号进行了分库分表。由于订单号不在同一个表中,要查询一个买家或者商家的订单列表,就需要查询所有分库然后进行数据聚合。可以采取构建异构索引,在生成订单的时同时创建买家和商家到订单的索引表,这个表可以按照用户 ID 进行分库分表。