背景

  最近被分配到的一个需求,数据量每周新增上千万,预计两个月就会破亿,这里记录一下对这个服务的性能优化的过程。


正文

需求介绍

  首先大致介绍一下这个需求的内容。这个需求是一个周报服务,每周日向用户推送他本周使用服务的时常,最晚使用时间等统计数据,这应该是很多应用都有实现的功能。而对于后台服务来说,只需要提供一个接口,它实现的功能就是去查询用户的周报数据。但是这个服务的用户量庞大,有数千万,而被筛选后,需要统计周报信息的用户,大致有1000w左右。这也就是说,每周将会新增1000w条数据,两个月左右,总数据就会破亿。


问题说明

  这个服务数据存储比较简单,就只有一张DB表,存储每一个用户的周报数据,但是因为数据量庞大,所以如果只是简单的接收请求后,直接去查询DB,那服务性能就太低了,而且也扛不住多少请求量,随着数据量的增大,这个问题也会越来越严重。所以这里我们需要对服务进行优化。


优化方式一:建立索引

  由于数据量巨大,查询DB的时候,如果需要全表扫描,那查询速度将非常缓慢,首先可以想到的就是建立索引。查询条件有两个,一个就是用户的id,一个就是周报的时间(那周周一的日期,表示需要查询用户第几周的周报),所以我们可以直接使用这两个字段建立一个联合索引。使用1000w左右的数据进行测试,未添加索引前,查询速度在10秒以上,而添加索引后,单次查询速度降低到了10毫秒以下。


优化方式二:分表

  由于这个服务每周会增加千万条数据,所以使用一张表进行存储,那只会使得查询的速度越来越低,单张表的索引也会越来越大,所以此时肯定是需要考虑分表的。那如果来进行分表呢,对于这个服务来说就简单了。这是一个周报服务,DB表每周只会新增一次数据,那就是在每周的周日,会将用户这周的周报数据导入,所以我们自然而然的就可以想到,按周进行分表。将每周的数据,单独存储在一张表中,表名加上这周的时间,查询的时候,找到对应时间的表进行查询即可。比如,2021年7月5号到7月11号这周的数据,我们可以放到t_weekly_info_20210705这张表中,表的后缀就是这周周一的日期。这样一来,我们就控制住了每张表的数据量,同时每次查询也只需要在一周的数据中进行查找,提升了查询的效率。用户在请求周报时,会带上一个时间参数,以此来表明需要哪一周的数据,而根据这个参数,我们即可拼出对应的表名。

  除此之外,使用了分表之后,我们也可以对索引进行优化,之前的索引,由用户id和周报时间组成,但是由于使用了分表,同一张表中所有的数据,周报时间都是相同的,所以索引可以不需要周报时间这个字段,只留一个用户id即可。


优化方式三:添加缓存

  这么大数据量的服务,缓存必不可少。这里我使用Redis实现缓存,在接收到用户的请求后,我们先去中Redis中查询用户的周报,如果查询成功,则直接返回;如何查询失败,则再去对应的DB表中查询,再更新到Redis中。除此之外,查询DB时,根据查询失败的原因不同,处理方式也有所区别,如果是因为DB中没有这个用户的数据,导致查询失败,那我们也需要将空数据缓存到Redis,因为即使他下次再查,也是不会有数据的;但是如果查询失败的原因是网络等原因导致查询异常,那此时我们就不需要缓存了,因为下一次查询,是有可能成功的。而且由于周报记录的是一周的数据,用户一般查询的也是本周的周报,所以我们的缓存时间可以长一些,比如一天。

  但是这个方式并不保险,很有可能出现缓存击穿的问题,当我们还没有更新某个用户的缓存,或者这个用户的缓存失效后,突然有大量的请求进来,请求这个用户的数据,由于是并发的请求,此时也没有缓存,所以都打到了DB上,給DB造成巨大的压力,从而查询效率降低,请求超时,用户的缓存将无法得到更新,最终甚至可能导致DB被打挂。而为了防止这个问题的发生,我们就需要用到另一种缓存方式了:数据预热。


优化方式四:数据预热

  什么是数据预热呢,其实就是“异步更新缓存”。我们提前将DB中所有的数据都加载到Redis中,然后有请求过来,直接去Redis中查,然后每隔一段时间,使用一个异步的线程去更新缓存,也就是让缓存永不过期。但是这里无法真正的做到缓存永不过期,因为数据量巨大,且每周都在增长,所以缓存所有的数据并不现实,而且也没有必要。对于周报,一般来说,用户只会查看上周的周报,而对于几周前的周报,会查看的频率就比较低了,所以我们缓存最近一到两周的数据,基本上就可以涵盖大部分的请求了。假设以两周的数据来算的话,大概需要多大的Redis空间内?一条数据大70B左右,以一周1000w条数据来算的话,缓存一周的数据,大概需要不到700MB,所以我们使用一个4G的Redis,也足够缓存两周的数据了。


总结

  以上使用了四种方式对服务的性能进行了优化,其实都是比较简单的技巧,但是却非常的有效。这个服务的主要瓶颈是在DB,所以优化的主要思路就是尽量少查询DB,已经查询DB时查询更少的数据。还有一些优化的方式上面没有提到,比如调整数据库连接池的连接数量,但是这里一般框架都封装的比较好了,再加上我对这里的调整标准也不是很了解,就不细说了。