一次使用 Redis 优化查询性能的实践

 

应用背景

有一个应用需要上传一组ID到服务器来查询这些ID所对应的数据,数据库中存储的数据量是7千万,每次上传的ID数量一般都是几百至上千数量级别。

以前的解决方案

  1. 数据存储在Oracle中,为ID建立了索引;
  2. 查询时,先将这些上传的ID数据存储到临时表中,然后用表关联的方法来查询。

这样做的优点是减少了查询次数(不用每个ID都查询一次),减少了解析SQL的时间(只需要执行1次查询SQL,但是多了插入数据的SQL处理时间)。

但是这样的设计仍然存在巨大的提升空间,当并发查询的数量增加时,数据库的响应就会很久。虽然建立了索引,但是每个ID查询的时间复杂度仍是O(logn)级别的,那么总的查询时间复杂度就应该是m*O(logn)。不知道Oracle对表关联查询有做了哪些优化,但应该也是改变不了时间复杂度的级别。

解决方法

一遇到读数据库存在瓶颈的问题,首先想到的就是要用内存数据库,用缓存来解决。首选 Redis,因为Redis是一种提供了丰富数据结构的key-value数据库,value可以存储STRING(字符串)、HASH(哈希),LIST(列表),ZSET(有序集)。

首先需要将数据的存储改成 key-value 架构。简单的做法就是一个ID对应一个字符串的 Value。但是一个 ID 可以对应多条数据,而且一条数据内又可以包含多个字段。这时候就需要将这些数据重新组合一下,拼在一起,或者是采用列表、哈希或集合来存储 Value。

Redis内部采用 HashTable(哈希表)来实现key-value的数据结构,是一种空间占用较高的数据结构。而我的应用场景又是ID有几千万规模的,如果按上述方法,使用每个ID作为key,那么内存的消耗将是巨大的。每个key-vaulue结构,Redis本身的维护开销就要80几字节,即便value存储的是纯数字(会使用long类型,占用4个字节),也依然很大,1000万的数据,就要占用快1G内存。

使用两级Hash优化内存

依据官方文档的内存优化方法,以及这篇文章 节约内存:Instagram的Redis实践,建议对ID分段作为key,并使用 hash 来存储第一级 key 的 value,第二级存储较少的数据量(推荐1000),因此第二级的key使用ID的后3位。

为了节约内存,Redis默认使用ziplist(压缩列表)来存储HASH(哈希),LIST(列表),ZSET(有序集)这些数据结构。当某些条件被满足时,自动转换成 hash table(哈希表),linkedlist(双端列表),skiplist(跳表)。

ziplist是用一个数组来实现的双向链表结构,顾名思义,使用ziplist可以减少双向链表的存储空间,主要是节省了链表指针的存储,如果存储指向上一个链表结点和指向下一个链表结点的指针需要8个字节,而转化成存储上一个结点长度和当前结点长度在大多数情况下可以节省很多空间(最好的情况下只需2个字节)。但是每次向链表增加元素都需要重新分配内存。—— 引用自这里的描述

ziplist的详细信息请看 Redis book ziplist 章节

查看 Redis 的 .conf 文件,可以查看到转换条件的设置信息。

# Hashes are encoded using a memory efficient data structure when they have a
# small number of entries, and the biggest entry does not exceed a given
# threshold. These thresholds can be configured using the following directives.
hash-max-ziplist-entries 512
hash-max-ziplist-value 64 # Similarly to hashes, small lists are also encoded in a special way in order
# to save a lot of space. The special representation is only used when
# you are under the following limits:
list-max-ziplist-entries 512
list-max-ziplist-value 64 # Similarly to hashes and lists, sorted sets are also specially encoded in
# order to save a lot of space. This encoding is only used when the length and
# elements of a sorted set are below the following limits:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

ziplist 查找的时间复杂度是 O(N),但是数据量较少,第二级Hash的查询速度依然在O(1)级别。

对第二级Hash存储的数据再编码

在我的应用场景中每个ID对应的数据可以有很多个字段,这些字段有很多实际上是类型数据,存储的也是ID。为了进一步节约内存,对这些使用数字作为ID的字段,采用base62编码(0-9,A-Z,a-z),这样可以使这些ID的字符长度变短,进一步减少在Redis中第二级hash需要存储的数据量,从而减少Redis占用的内存。

使用Lua脚本来处理批量操作

由于每次查询都上传几百上千个ID,如果对这些ID,都单独调用一次HGET命令,那么一次查询就需要上千次TCP通信,速度很慢。这个时候最好的方法就是一次性将所有的查询都发送到 Redis Server,然后在 Redis Server 处再依次执行HGET命令,这个时候就要用到 Redis 的Pipelining(管道)Lua 脚本(需要 Redis 2.6以上版本)。这两项功能可以用来处理批量操作。由于Lua脚本更简单好用,因此我就直接选用Lua脚本。

Redis Lua 脚本具有原子性,执行过程会锁住 Redis Server,因此 Redis Server 会全部执行完 Lua 脚本里面的所有命令,才会去处理其他命令请求,不用担心并发带来的共享资源读写需要加锁问题。实际上所有的 Redis 命令都是原子的,执行任何 Redis 命令,包括 info,都会锁住 Redis Server。

不过需要注意的是:

为了防止某个脚本执行时间过长导致Redis无法提供服务(比如陷入死循环),Redis提供了lua-time-limit参数限制脚本的最长运行时间,默认为5秒钟(见.conf配置文件)。当脚本运行时间超过这一限制后,Redis将开始接受其他命令但不会执行(以确保脚本的原子性,因为此时脚本并没有被终止),而是会返回"BUSY"错误——引用自这里的描述

遇到这种情况,就需要使用 SCRIPT KILL 命令来终止 Lua 脚本的执行。因此,千万要注意 Lua 脚本不能出现死循环,也不要用来执行费时的操作。

性能分析

实验基本设置:

  1. 将7000万数据按照上面描述的方法,使用两级Hash以及对数据再编码,存储到Redis中。
  2. 模拟数据请求(没有通过HTTP请求,直接函数调用),查询数据,生成响应的JSON数据。

(数据仅供参考,因为未真正结合Web服务器进行测试)

使用上述方法,对Redis的内存优化效果非常好。

实验设置:

  1. 模拟每次查询500个ID,分批次连续查询。用于模拟测试并发情况下的查询性能。

响应速度与查询的数据量,几乎是线性相关。30s 的时间就可以处理2000次请求,100W个ID的查询。由于Oracle速度实在太慢,就不做测试了。

实验设置:

  1. 连续查询1W个ID,每次500个,分20次。用于测试Redis中存储的数据量对查询性能的影响。

查询速度受存储数据量的影响较小。当存储的数据量较多时,第二级hash存储的数据量就会更多,因此查询时间会有略微的上升,但依然很快。

参考文献

  1. Redis book(Redis设计与实现)
    http://redisbook.readthedocs.org/en/latest/
  2. Redis 官网 http://redis.io/
 
分类: Database
标签: Redis

Redis 优化查询性能的更多相关文章

  1. 一次使用 Redis 优化查询性能的实践

    因为我的个人网站 restran.net 已经启用,博客园的内容已经不再更新.请访问我的个人网站获取这篇文章的最新内容,一次使用 Redis 优化查询性能的实践 应用背景 有一个应用需要上传一组ID到 ...

  2. 【分布式搜索引擎】Elasticsearch如何部署以及优化查询性能

    一.Elasticsearch生产集群如何部署 (1)es生产集群部署5台机器,若每台机器是6核64G的,那么集群总内存是320G (2)假如我们es集群的日增量数据大概是2000万条,每天日增量数据 ...

  3. 使用Spring Ehcache二级缓存优化查询性能

    最近在对系统进行优化的时候,发现有些查询查询效率比较慢,耗时比较长, 通过压测发现,主要耗费的性能 消耗在 查询数据库,查询redis 数据库:连接池有限,且单个查询不能消耗大量的连接池,占用大量IO ...

  4. 最新IP数据库 存储优化 查询性能优化 每秒解析上千万

    高性能IP数据库格式详解 每秒解析1000多万ip  qqzeng-ip-ultimate.dat 3.0版 编码:UTF8     字节序:Little-Endian 返回规范字段(如:亚洲|中国| ...

  5. Sql Server查询性能优化之不可小觑的书签查找

    小小程序猿SQL Server认知的成长 1.没毕业或工作没多久,只知道有数据库.SQL这么个东东,浑然分不清SQL和Sql Server Oracle.MySql的关系,通常认为SQL就是SQL S ...

  6. SQL Server-聚焦过滤索引提高查询性能(十)

    前言 这一节我们还是继续讲讲索引知识,前面我们讲了聚集索引.非聚集索引以及覆盖索引等,在这其中还有一个过滤索引,通过索引过滤我们也能提高查询性能,简短的内容,深入的理解,Always to revie ...

  7. SQL Server-聚焦过滤索引提高查询性能

    前言 这一节我们还是继续讲讲索引知识,前面我们讲了聚集索引.非聚集索引以及覆盖索引等,在这其中还有一个过滤索引,通过索引过滤我们也能提高查询性能,简短的内容,深入的理解,Always to revie ...

  8. 提升50%!Presto如何提升Hudi表查询性能?

    分享一篇关于使用Hudi Clustering来优化Presto查询性能的talk talk主要分为如下几个部分 演讲者背景介绍 Apache Hudi介绍 数据湖演进和用例说明 Hudi Clust ...

  9. SQL常见优化Sql查询性能的方法有哪些?

    常见优化Sql查询性能的方法有哪些? 1.查询条件减少使用函数,避免全表扫描 2.减少不必要的表连接 3.有些数据操作的业务逻辑可以放到应用层进行实现 4.可以使用with as 5.使用“临时表”暂 ...

随机推荐

  1. webstorm创建nodejs + express + jade 的web 项目

    webstorm创建nodejs + express + jade 的web 项目 前简单了解过nodejs,觉得用nodejs来做个网站也太麻烦了,要自己拼html的字符串返回,这能做网站嘛? 最近 ...

  2. VMware workstation 10.0的永久key

    MA491-6NL5Q-AZAM0-ZH0N2-AAJ5A5A6F6-88247-XZH59-HL0Q6-8CD2VHF6QX-20187-2Z391-522NH-9AELT5F29M-48312-8 ...

  3. Jvascript方法

    Jvascript实用方法   这篇我主要记录一些在工作中常用的.实用的方法. String trim 字符串方法中的trim主要用来去空格使用,很多时候,在后台做参数处理的时候,我们都会使用该方法, ...

  4. 自己写CPU第五级(4)——逻辑、实现移动和空指令

    我们会继续上传新书<自己写CPU>(未公布).今天是18片,我每星期试试4 5.5 改动OpenMIPS以实现逻辑.移位操作与空指令 为了实现逻辑.移位操作与空指令(当中nop.ssnop ...

  5. Codeforces Beta Round #3 A. Shortest path of the king

    标题效果: 鉴于国际棋盘两点,寻求同意的操作,是什么操作的最小数量,在操作过程中输出. 解题思路: 水题一个,见代码. 以下是代码: #include <set> #include < ...

  6. ruby 删除文件夹(包括文件夹中的文件夹和文件)

    def deleteDirectory(dirPath) if File.directory?(dirPath) puts "是文件夹"; Dir.foreach(dirPath) ...

  7. eclipse android ndk 提示Type 'JNIEnv' could not be resolved 等信息解决办法

    新配置完eclipse c++ android ndk 环境后,导入项目提示以下信息 是由于没有将jni.h导入的缘故,而这个文件在ndk的目录下面.所以,参照以下步骤:Project Propert ...

  8. android开源应用(主要是博客上带有分析的)收集 【持续更新】

    2014.5.24更新: (android高仿系列)今日头条    http://blog.csdn.net/vipzjyno1/article/details/26514543 CSDN Andro ...

  9. HDOJ 4248 A Famous Stone Collector DP

    DP: dp[i][j]前i堆放j序列长度有多少行法, dp[i][j]=dp[i-1][j] (不用第i堆), dp[i][j]+=dp[i-1][j-k]*C[j][k] (用第i堆的k个石头) ...

  10. JAVA学习第六十二课 — TCP协议练习

    通过练习掌握TCP在进行传输过程中的问题 练习1:创建一个英文大写转换server client输入字母数据,发送给服务端,服务端收到后显示到控制台,并将该数据转成大写返回client,知道clien ...