An important means to improve database performance--redundancy

foreword

 

In program design, there is a common method to improve the performance of data query - space for time. A typical scenario is to use " cache " , add a layer of " global shared cache " (eg: redis ) before querying the database , and even add a layer of " local cache " inside the application instance . Taking java application + mysql database as an example, the architecture is designed as follows:



 

 

The data query logic is:

 



 

 

The query speed of the local cache is nanoseconds

The query speed of Redis cache is millisecond

The query speed of Msyql data is millisecond - second

 

Some people will say that since the local cache is the fastest, why not use the local cache directly, but also use the redis cache? To put it bluntly, the local cache is the JVM memory. After all, the space is limited. In addition, there are multiple JVM instances in the distributed multi-instance deployment application. The local cache is scattered in each instance, which is not convenient for synchronous update, and there are limitations in use. How to balance "local cache" and " global shared cache " is not the focus of this discussion and will not be introduced too much.

 

Therefore, in order to improve performance, it is necessary to store three copies of the same data, which is data redundancy, a typical usage scenario of " exchanging space for time " .

 

It can be found that in this scenario, the final bottleneck is the msyql database (which will fall to the database when the cache is invalid). If you want to further optimize the performance, an important optimization point is the query performance optimization of the msyql database (of course, there is another point is the optimizer itself).

 

The method of " exchanging space for time " is also applicable to the performance optimization of MySQL database, which is mainly reflected in three places: table field redundancy, mirror replication of read-write separation, and asymmetric replication of read-write separation. The following explanations are given according to different scenarios.

 

table field redundancy

 

During the reading period, in the database table design chapter, I believe that everyone has learned "paradigm". If the database is designed in full compliance with the "three paradigms", it can greatly reduce the amount of data storage for data storage. But for data query, there will be many join table queries, which will greatly affect performance. For high-frequency query SQL statements, "join table query" is definitely a disaster.

 

The common method at this time is "table field redundancy". For a real case: at the beginning of a project, a table named "sale_info" is used to store activity information. With the development of the business, there are more and more fields in the table. In order to prevent too many fields in a table, the easiest way is to add an extension table "sale_info_ext", and add subsequent fields to this extension. inside and outside the table.

 

这种简单的处理方式,解决了宽表问题。但同时又引入了新的问题,在查询活动信息时,由于业务数据分散在两张表里,经常需要做联表查询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

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326403569&siteId=291194637