分享

20190728 冗余设计

 文明世界拼图 2020-01-15

在设计数据库时,某一字段属于一个表,但它又同时出现在另一个或多个表,且表示的意义完全相同,那么这个字段就是一个冗余字段。

关系数据库中的数据冗余主要是指关系数据库中同一信息数据的重复存贮。数据冗余浪费了宝贵的资源,应尽量减少。但关系数据库中为实现一些功能有些数据冗余是必需的。必需的数据冗余主要用于以下用途:

(1)数据间建立联系,如两表间通过共同属性建立联系;

(2)数据恢复,如建立备份文件以备正式文件被破坏时恢复; 

(3)数据核查,如设立数据校验位可以检查数据在存贮、传输等过程中的改变; 

(4)数据使用的便利,如为了查看数据的直观,使用数据的方便、高效。 

(5)减少数据通讯开销,如分布式数据库在不同场地重复。

 

冗余字段的好处:提高查询效率,减少关联查询。单表查询比关联查询速度要快。

劣势:浪费存储空间;需要多处维护冗余字段的更新;违背了数据库设计范式理论。

数据库设计范式理论:要求数据库设计逻辑清晰、关系明确;连接查询;缺点是:如果一个表存在几十万条记录,也只能尽行关联检索。

目前要进行一个关系型数据库设计,我们有两种选择:

1、尽量遵循范式理论的规约,尽可能少的冗余字段,让数据库设计看起来精致、优雅、让人心醉。

2、合理的加入冗余字段这个润滑剂,减少join,让数据库执行性能更高更快。

在数据库设计领域,有一个被大家奉为圭臬的数据库设计范式,这个范式理论上要求数据库设计逻辑清晰、关系明确,比如,”用户昵称”字段”nickname”本来属于表”user”,那么,表示”用户昵称”的字段就唯一的只应该属于”user”表的”nickname”字段,这样,当用户要修改昵称的时候,程序就只需要修改 user.nickname这个字段就行了。不过问题也随之而来,我在其他数据表(如订单orders表)里只存储了用户的ID,我要通过这个ID值得到用户昵称该怎么办呢?一个普遍的解决方法是通过联接(join),在查询时,通过id这个唯一条件联接两个表,从而取到用户的昵称。

这样确实是没问题,我也一直觉得这样是最好的方案,扩展方便,当要更新用户信息时,程序中要修改的地方很少,但是随着数据库里数据不断增加,百万,千万,同时,用户表的数据肯定也在不断的增加的,它可能是十万,百万。这个时候,你会发现两个表通过联接来取数据就显得相当费力了,可能你只需要取一个nickname这个用户昵称属性,你就不得不去联一下那个已经几十万的用户表进行检索,其速度可想而知了。

这个时候,你可以尝试把nickname这个字段加到orders这个订单表中,这样做的好事是,当你要通过订单表呈现一个订单列表时,涉及用户的部分可能就不需要再进行联接查询了(变成了单表查询)。当然,有利就有弊,这样做的弊端就是,当你尝试更新用户信息时,你必须记得用户信息表里当前被更新的字段中,有哪些是冗余字段,分别属于哪些表,找到他们,然后加入到你的更新程序段中来。这个是程序中的开销,开销在开发人员的时间上了。至于这样做是否值得,就得看具体情况而定了。

选择哪一种呢?如果你是一个美学狂人,并且财大气粗,非要使用第一种方案,也没关系,这种方案的短板并非不可救药的。比如,你可以增加服务器,从数据库集群入手,进行读写分离,读的时候可以将压力分散到不同的数据库服务器上,这样也可以获得很好的性能,只是多付出了硬件成本和维护成本。或者你可以在数据库前端架设Memcached之类的缓存服务,减少读写数据库的次数,也可以达到同样的效果。

 

在程序设计中有一种常用的提升数据查询性能的手段以"空间换时间"这种理念。典型的场景就是使用“缓存”,在查询数据库之前加一层“全局共享缓存”(如:redis),更有甚者在应用实例内部在加一层“本地缓存”。以java应用+mysql数据库为例,数据查询逻辑如下:

1、本地缓存的查询速度是纳秒级;

2、Redis缓存的查询速度是毫秒级;

3、Mysql数据的查询速度是毫秒级-秒级;

有人会说既然本地缓存是最快的为什么不直接用本地缓存,还要使用redis缓存呢?

1、本地缓存说白就是jvm内存,空间毕竟是有限的;

2、再者分布式多实例部署的应用有多个jvm实例,本地缓存分散在各个实例内部不便于同步更新,使用上存在局限。具体怎么去权衡“本地缓存”和“全局共享缓存”不是本次讨论的重点,不再过多介绍。所以为了提高性能需要把同一份数据存放三份,这就是数据冗余,典型的“以空间换时间”的使用场景

可以发现在该场景中,最终瓶颈还是mysql数据库(在缓存失效时都会落到数据库),如果想要进一步的优化性能,一个重要的优化点还是在msyql数据库查询性能优化上(当然还有一个点就是优化程序本身)。

“以空间换时间”的方式同样适用运用到mysql数据库的性能优化中,主要体现在三个地方:表字段冗余、读写分离之镜像复制、读写分离之非对称复制。下面根据不同的场景,分别进行讲解。

表字段冗余

读书期间,在数据库表设计章节,相信大家都学习过“范式”。如果完全遵循“三范式”设计的数据库,对于数据存储来说可以极大的提高数据的存储量。但对于数据查询来说,会有很多联表查询,这会非常影响性能。对于高频查询的sql语句来说 “联表查询”绝对是个灾难。这时的常用手段就是“表字段冗余”,举个真实的案例:在一个项目初期,使用一张表名为“sale_info”的表存放活动信息。随着业务的发展,该表的字段越来越多,为了防止一张表的字段太多,最简单的做法就是新增一个扩展表“sale_info_ext”,后续新增的字段都放到这张扩展表里。这种简单的处理方式,解决了“宽表”问题。但同时又引入了新的问题,在查询活动信息时,由于业务数据分散在两张表里,经常需要做“联表查询”:select a.xx,b.xx from sale_info a left join sale_info_ext b on a.id=b.sale_id where “省略其他条件”。刚开始没有发现问题,但随着业务的增长,两张表的数据越来越多,应用程序经常出现“timeout”现象,通过分析慢查询日志有一条高频“联表查询”语句在中暴露出来。发现该问题后,初步做法是优化索引,也就是对“省略其他条件”字段加索引,但效果并不明显。最后的做法是:分析者两张表的所有“联表查询”sql语句,把主业务相关的字段迁移到sale_info表,把副业务相关的字段迁移到sale_info_ext表,对于主副业务都需要的常用字段 在两张表中做“冗余”存储。从而保证高频查询的sql语句,都是单表查询,最终解决该问题。对于一些“低频”查询的sql语句,仍然“联表查询”已经无所谓了(有时还应防止这些“低频”查询的sql语句转为“高频”)。

“表字段冗余”说起来原理简单,但实际操作有时会比较复杂。最重要的原则还是要根据业务划分找准“冗余点”才能做到“以空间换时间”,否则“空间”消耗了“时间”增加--这就不是我们想要的效果了。

 

读写分离之镜像复制

对于“读多写少”的业务(其实大部分业务都是这种场景),最常见的以“空间换时间”的做法就是“读写分离”,要做读写分离 首先要做“主备”。对应“写”业务直接写“主库”,对于延迟要求不高的“读”业务读“从库”。现在的问题就变为 主从同步问题,数据同步始终会有延迟(即便是采用数据库自带的,如mysql的Replication)。所以,对于不允许“延迟”的业务,只能读“主库”

所谓“镜像复制”就是所有备库的内容,跟主库内容完全一致。“镜像复制”,又存在两种情况“一主多备”、“多主多备”。对于,延迟要求不高的业务,可以采用“一主多备”;对于,延迟要求高的业务,可以采用“多主多备”,具体做法是:写入数据时,写入多个主库(或者说写库),对于不允许延迟的业务直接从主库中读取数据,对于延迟要求不高的业务到“从库”读取数据。“多主多备”的架构设计如下:

这时会出现在同一个应用中有多个数据源的情况,一般做法是:在spring配置文件中配置多个数据源,获取数据源时通过一个“工具类”获取:

最后这些数据源配置可以放到“配置管理”系统,可以实现在线切换“数据库”


读写分离之非对称复制
前一种“镜像”同步方式,是主库和备库的内容是完全相同的。在“分库分表”的系统中,做数据冗余还有另外一种数据冗余方式:非对称复制,主要作用就是减少查询时多张表的join操作。下面看一个真实的场景:

在笔者所在的一个“活动页cms系统”中,需要记录每个页面上的sku(商品编码),每天上线的活动页数量成千上万个,每个页面上对应的sku从几十个到几百个不等。可见数据量,是比较大,一般会进行“分表”存储。

应用中经常会 根据“活动页”id查询页面上的sku列表,为了减少表的join次数,我们用“活动页”id做hash(可以直接取模,或者使用一致性hash) 进行分表,保证每个“活动页”id对应的sku都存在同一张表中。假设分8张表存储,分表方式如下:

现在要查询某个“活动页id”下的所有sku,首先通过“活动页id”mod 8,计算出数据所在的表,然后通过一条简单的select语句查询该表就搞定。

但现在问题来了,“业务方”想要知道某个sku 今天在哪些页面上出现过,用来对比各个不同的页面推广效果。怎么办呢?如果按照上述分表,包含某个sku的的“活动页id”分散在8张表里,需要进行7次join操作或者查询8次进行合并 采用获取到所有的“活动页id”。

这种场景就可以使用“非对称复制”,在写入数据时,我们可以用另外的8张表存储上述相同的数据,唯一不同的地方就是分表规则改为对sku编号进行hash(取模或者一致性hash都可以),分表方式如下:

好了,现在要查询某天某个sku出现过的活动页面有哪些,也就同样简单了,首先通过sku编号 mod 8获取到所在表,再通过一个简单的selec语句查询该表就搞定。也就是说:如果查询条件是“活动页id”就使用第一种分表规则;反正就是使用第二种分表规则。都是单表唯一索引查询,查询速度也非常快。只是存储空间翻倍,这也是典型的“空间换时间”的场景。

这里讲的是单库操作,按照第二冗余方式所讲,如果需要做“读写分离”,这时有两种方案。方案一:两种分表方式的写入操作,都在同一个“主库”中进行,再通过数据库自带的同步工具同步到“读库”,查询时直接读“读库”。方案二:在写入数据时,两种分表方式分别写入不同的数据库,这时直接借助程序自己实现,也可以借助一些数据库中间件来完成。第二种方式虽然麻烦些,但如果数据量大 同时查询又很频繁,采用这种方式可以进一步实现不同的查询业务“分库”,单个数据库可以存储更多数据 并且降低单个数据库并发查询压力。

仅对sql语句层面进行优化始终有局限,作为项目里的架构师一定要从更高层次出发,根据不同的业务场景采用不同架构设计手段,以减轻数据库的压力。这里不是说优化sql语句不重要,优化sql是需要首先做。另外以“空间换时间”是架构设计中的常用手段,可以在各种不同的场景下使用,达到以多个廉价的pc机(空间),换取需要使用昂贵的“大中型”机采用达到的性能(时间)。

 

为什么要冗余数据?

互联网数据量很大的业务场景,往往数据库需要进行水平切分来降低单库数据量。水平切分会有一个patition key,通过patition key的查询能够直接定位到库,但是非patition key上的查询可能就需要扫描多个库了。此时常见的架构设计方案,是使用数据冗余这种反范式设计来满足分库后不同 维度的查询需求。

例如:订单业务,对用户和商家都有订单查询需求:

Order(oid, info_detail);

T(buyer_id, seller_id, oid);

如果用buyer_id来分库,seller_id的查询就需要扫描多库。

如果用seller_id来分库,buyer_id的查询就需要扫描多库。

此时可以使用数据冗余来分别满足buyer_id和seller_id上的查询需求:

T1(buyer_id, seller_id, oid)

T2(seller_id, buyer_id, oid)

 

同一个数据,冗余两份,一份以buyer_id来分库,满足买家的查询需求;一份以seller_id来分库,满足卖家的查询需求。如何实施数据的冗余?

服务同步双写

111

顾名思义,由服务层同步写冗余数据,如上图流程:业务方调用服务,新增数 据。 服务先插入T1数据。服务再插入T2数据。服务返回业务方新增数据成功。

优点:不复杂,服务层由单次写,变两次写。数据一致性相对较高(因为双写成功才返回)。

缺点:请求的处理时间增加(要插入两次,时间加倍)。数据仍可能不一致,例如第二步写入T1完成后服务重启,则数据不会写入T2。

 

服务异步双写

111

数据的双写并不再由服务来完成,服务层异步发出一个消息,通过消息总线发送给一个专门的数据复制服务来写入冗余数据,如上图流程:业务方调用服务,新增数据。服务先插入T1数据。服务向消息总线发送一个异步消息(发出即可,不用等返回,通常很快就能完成)。 服务返回业务方新增数据成功。消息总线将消息投递给数据同步中心。数据同步中心插入T2数据。

优点: 请求处理时间短(只插入1次)。

缺点:系统的复杂性增加了,多引入了一个组件(消息总线)和一个服务(专用的数据复制服务)。因为返回业务线数据插入成功时,数据还不一定插入到T2中,因此数据有一个不一致时间窗口(这个窗口很短,最终是一致的)。 在消息总线丢失消息时,冗余表数据会不一致。

不管是服务同步双写,还是服务异步双写,服务都需要关注“冗余数据”带来的复杂性。如果想解除“数据冗余”对系统的耦合,引出常用的第三种方案

 

线下异步双写

111

为了屏蔽“冗余数据”对服务带来的复杂性,数据的双写不再由服务层来完成,而是由线下的一个服务或者任务来完成,如上图流程:业务方调用服务,新增数据。服务先插入T1数据。服务返回业务方新增数据成功。 数据会被写入到数据库的log中。线下服务或者任务读取数据库的log。线下服务或者任务插入T2数据。

优点:数据双写与业务完全解耦。请求处理时间短(只插入1次)。

缺点:返回业务线数据插入成功时,数据还不一定插入到T2中,因此数据有一个不一致时间窗口(这个窗口很短,最终是一致的)。数据的一致性依赖于线下服务或者任务的可靠性。

 

互联网数据量大的业务场景,常常:

1)使用水平切分来降低单库数据量。

2)使用数据冗余的反范式设计来满足不同维度的查询需求

3)使用服务同步双写法能够很容易的实现数据冗余。

4)为了降低时延,可以优化为服务异步双写法。

5)为了屏蔽“冗余数据”对服务带来的复杂性,可以优化为线下异步双写法。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多