提升数据库性能的重要手段--冗余

前言

 

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



 

 

数据查询逻辑为:

 



 

 

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

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

Msyql数据的查询速度是毫秒级-秒级

 

有人会说既然本地缓存是最快的为什么不直接用本地缓存,还要使用redis缓存呢?本地缓存说白就是jvm内存,空间毕竟是有限的,再者分布式多实例部署的应用有多个jvm实例,本地缓存分散在各个实例内部不便于同步更新,使用上存在局限。具体怎么去权衡“本地缓存”和全局共享缓存不是本次讨论的重点,不再过多介绍。

 

所以为了提高性能需要把同一份数据存放三份,这就是数据冗余,典型的以空间换时间的使用场景。

 

可以发现在该场景中,最终瓶颈还是msyql数据库(在缓存失效时都会落到数据库),如果想要进一步的优化性能,一个重要的优化点还是在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语句转为高频)。

 

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

 

读写分离之镜像复制

 

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

 

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



 

 

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

 

public class DataSourceUtil {
    private List<DataSource> readDatasources;//通过spring配置注入
    private List<DataSource> writeDatasources;//通过spring配置注入
    private Random random = new Random();
 
    public List<DataSource> getALLWrite(){//获取所有的写数据源
        return readDatasources;
    }
 
    public DataSource getOneWrite(){//随机获取一个写数据源 用于“非延时”读
        return readDatasources.get(random.nextInt(readDatasources.size()));
    }
 
    public DataSource getOneRead(){//随机获取一个写数据源 用于“可延时”读
        return readDatasources.get(random.nextInt(readDatasources.size()));
    }
}

 

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

 

读写分离之非对称复制

 

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

 

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

 

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



 

 

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

 

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

 

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



 

 

好了,现在要查询某天某个sku出现过的活动页面有哪些,也就同样简单了,首先通过sku编号 mod 8获取到所在表,再通过一个简单的selec语句查询该表就搞定。

 

也就是说:如果查询条件是“活动页id”就使用第一种分表规则;反正就是使用第二种分表规则。都是单表唯一索引查询,查询速度也非常快。只是存储空间翻倍,这也是典型的空间换时间的场景。

 

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

 

总结

 

仅对sql语句层面进行优化始终有局限,作为项目里的架构师一定要从更高层次出发,根据不同的业务场景采用不同架构设计手段,以减轻数据库的压力。这里不是说优化sql语句不重要,优化sql是需要首先做。

 

 

另外以空间换时间是架构设计中的常用手段,可以在各种不同的场景下使用,达到以多个廉价的pc机(空间),换取需要使用昂贵的大中型机采用达到的性能(时间)。

 

出处:

http://moon-walker.iteye.com/blog/2405545

 

猜你喜欢

转载自moon-walker.iteye.com/blog/2405545