redis优化建议

读完了Redis实战,感觉收获还是蛮多的。像往常那样,读完就想将书束之高阁。这几天总感觉差点什么,于是又翻了一下这本书,打算记录书上和自己知道的关于Redis优化的小知识点。


数据持久化

  • 选择恰当的持久化方式。Redis提供RDBAOF两种持久化方式。用户需要根据实际场景对两种持久化方式进行考量和选择。 RDB会在一定时间间隔内一次性将内存中的所有数据刷到磁盘上,非增量。它的一个主要缺点是如果Redis宕机了,那么可能会造成部分数据丢失,丢失的数据两和RDB持久化的时间间隔有关。此外,如果数据集非常大,Redis在创建子进程时可能会消耗相对较长的时间,这期间客户端请求都会被阻塞。这种情况下我们可以关闭自动保存,通过手动发送SAVE或者BGSAVE命令来控制停顿出现的时间。由于SAVE命令不需要创建子进程,从而避免了与子进程的资源竞争,因此它比BGSAVE速度更快。手动生成快照可以选择在线用户很少的情况下执行,比如使用脚本在凌晨三点执行SAVE命令。 从上述内容可以看出,RDB适用于即使丢失部分数据也不会造成问题的场景。同时我们需要注意快照是否生成得过于频繁或者稀少。 AOF持久化会将被执行的命令追加到AOF文件末尾。在redis.conf中该功能默认是关闭的,设置appendonly yes以开启该功能。这种方式会对磁盘进行大量写入,因此Redis处理命令的速度会受到硬盘性能的限制。并且AOF文件通常比RDB文件更大,数据恢复速度比RDB慢,性能消耗也比RDB高。由于它记录的是实际执行的命令,所以也易读。为了兼顾写入性能和数据安全,可以在配置文件设置appendfsysnc everysec。并不推荐appendfsync no选项,因为这种方式是由操作系统决定何时对AOF文件进行写入。在缓冲区被待写入硬盘的数据填满时,可能造成Redis的写入操作被阻塞,严重影响性能。

  • 重写AOF文件。如果用户开启了AOF功能,Redis运行时间越长,`AOF文件也会越来越大。用户可以发送BGREWRITEAOF重写AOF文件,它会移除AOF文件中的冗余命令以此来减小AOF文件的体积。由于AOF文件重写会用到子进程,因此也存在BGSAVE`命令持久化快照时因为创建子进程而导致的性能问题和内存占用问题。除了使用命令重写AOF文件,也可以在配置文件中配置,以让Redis自动执行重写命令。

      #当aof文件体积大于64mb且比上次重写之后的体积增大了至少一倍
      auto-aof-rewrite-percentage 100 
      auto-aof-rewrite-min-size 64mb 
      

内存优化

  • 设置maxmemory。设置Redis使用的最大物理内存,即Redis在占用maxmemory大小的内存之后就开始拒绝后续的写入请求,该参数可以确保Redis因为使用了大量内存严重影响速度或者发生OOM。此外,可以使用info命令查看Redis占用的内存及其它信息。

  • 让键名保持简短。键的长度越长,Redis需要存储的数据也就越多

  • 使用短结构。这节主要谈谈Redis的listhashsetzset这四种数据结构的存储优化。 在Redis3.2之前,如果列表、散列或者有序集合的长度或者体积较小,Redis会选择一种名为ziplist的数据结构来存储它们。该结构是列表、散列和有序集合三种不同类型的对象的一种非结构化表示,与Redis在通常情况下使用双向链表来表示列表、使用散列表示散列、使用散列加跳跃表表示有序集合相比,它更加紧凑,避免了存储额外的指针和元数据(比如字符串值的剩余可用空间和结束符"\0")。但是压缩列表需要在存储的时候进行序列化,读取的时候进行反序列化。以散列为例,在redis.conf中,可以进行如下设置

    hash-max-ziplist-entries 512 hash-max-ziplist-value 64

entries选项说明允许被编码为ziplist的最大元素数量,value表示压缩列表每个节点的最大体积是多少个字节。如果任意一个条件不满足,则压缩列表会退化成相应的常规结构。这样做的原因是,当压缩列表的体积越来越大时,操作这些数据结构的速度也会越来越慢,特别是当需要扫描整个列表的时候,因为Redis需要解码很多单独的节点。那么上述值各取多少合适呢?合理的做法是将压缩列表长度限制在500~2000个元素之内,并且每个元素体积在128字节之内。Redis实战推荐的做法是将压缩列表长度限制在1024个元素之内,并且每个元素体积不超过64字节。这类参数可能还得由应用的实际场景来定。此外,我们可以使用DEBUG OBJECT命令来查看某个存储的数据使用了何种数据结构及其它一些重要信息。 在Redis3.2及以后,列表的内部实现变成了quicklist而非ziplist或者传统的双端链表。官方定义是A doubly linked list of ziplists,即由ziplist组成的双向链表。quicklist这样设计的原因大概是一个空间和时间的折中:(1)双向链表便于在表的两端进行push和pop操作,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。(2)ziplist由于是一整块连续内存,所以存储效率很高。但是,它不利于修改操作,每次数据变动都会引发一次内存的realloc。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝,进一步降低性能。那么到底一个quicklist节点包含多长的ziplist合适呢?我们从存储效率来分析:(1)每个quicklist节点上的ziplist越短,则内存碎片越多。内存碎片多了,有可能在内存中产生很多无法被利用的小碎片,从而降低存储效率。这种情况的极端是每个quicklist节点上的ziplist只包含一个数据项,这就退化成一个普通的双向链表了。(2)每个quicklist节点上的ziplist越长,则为ziplist分配大块连续内存空间的难度就越大。有可能出现内存里有很多小块的空闲空间(它们加起来很多),但却找不到一块足够大的空闲空间分配给ziplist的情况。这同样会降低存储效率。这种情况的极端是整个quicklist只有一个节点,所有的数据项都分配在这仅有的一个节点的ziplist里面。这其实退化成一个ziplist了。 redis.conf提供了以下参数来设置quicklist的相关属性

  list-max-ziplist-size -2
  list-compress-depth 0
  
size参数可取正值和负值,取正的时候表示按照数据项个数来限定每个quicklist节点上的ziplist长度。取负的时候表示按照数据项大小来限制每个quicklist上的ziplist长度。计算方式是2^(abs(n)+1),比如这里-2表示每个quicklist节点上的ziplist大小不能超过2^(2+1)即8kb。 当列表很长的时候,最容易被访问的很可能是两端的数据,中间的数据被访问的频率比较低(访问起来性能也很低)。如果应用场景符合这个特点,那么list还提供了一个选项,能够把中间的数据节点进行压缩,从而进一步节省内存空间。Redis的配置参数list-compress-depth就是用来完成这个设置的。它表示两端不被压缩的元素个数。这里节点个数指quicklist双向链表的节点个数,如果一个quicklist节点上的ziplist被压缩,就是整体被压缩。如果值为0,则表示两端数据都不被压缩,为n,则表示两端各n个数据不被压缩。 关于ziplistquicklist细节可以阅读参考链接中的相关文章。

集合(set)也有自己的紧凑表示形式。如果集合元素全是整数,而这些整数处于平台的有符号范围之内,并且它们的数量又在一定范围内,那么Redis会以有序整数数组的方式存储集合,这种方式被成为整数集合(intset)。redis.conf中可以通过

  set-max-intset-entries 512
  
设置该范围。当存储数据个数大于512的时候或者存储了其它类型的数据时,它会退化为hashtable。在数据量较大的时候,与ziplist由于编码解码数据(如果有对数据移动的操作也会有影响)主要造成性能瓶颈的原因不同,主要影响intset性能的原因是它在执行插入或者删除操作的时候都需要对数据进行移动。因此,需要根据实际情况设置intset最大的元素个数。

  • 对数据进行分片。比如当单个散列比较大的时候,可以按一定规则(key+id%shard_num)对数据进行分片,然后ziplist便更不容易退化为hashtable,且不会出现编码解码引起的性能问题。

扩展读写能力

  • 扩展读性能。在redis.conf中添加slaveof host port即可将其配置为另一台Redis服务器的从服务器。注意,在从服务器连接主服务器的时候,从服务器之前的数据会被清空。可以用这种方式建立从服务器树,扩展其读能力。但这种方式并未做故障转移,高可用Redis部署方案可以参考Redis Sentinel,Redis ClusterCodis

  • 扩展写性能。(1)使用集群分片技术,比如Redis Cluster;(2)单机上运行多个Redis实例。由于Redis是单线程设计,在涉及到cpu bound的操作的时候,可能速度会大大降低。如果服务器的cpu、io资源充足,可以在同一台机器上运行多个Redis服务器。

应用程序优化

应用程序优化部分主要是客户端和Redis交互的一些建议。主要思想是尽可能减少操作Redis往返的通信次数

  • 使用流水线操作。Redis支持流水线(pipeline)操作,其中包括了事务流水线和非事务流水线。Redis提供了WATCH命令与事务搭配使用,实现CAS乐观锁的机制。WATCH的机制是:在事务EXEC命令执行时,Redis会检查被WATCH的key,只有被WATCH的key从WATCH起始时至今没有发生过变更,EXEC才会被执行。如果WATCH的key在WATCH命令到EXEC命令之间发生过变化,则EXEC命令会返回失败。使用事务的一个好处是被MULTIEXEC包裹的命令在执行时不会被其它客户端打断。但是事务会消耗资源,随着负载不断增加,由WATCHMULTIEXEC组成的事务(CAS)可能会进行大量重试,严重影响程序性能。 如果用户需要向Redis发送多个命令,且一个命令的执行结果不会影响另一个命令的输入,那么我们可以使用非事务流水线来代替事务性流水线。非事务流水线主要作用是将待执行的命令一次性全部发送给Redis,减少来回通信的次数,以此来提升性能。

  • 使用mset、lpush、zadd等批量操作数据。它的原理同非事务性流行线操作。

  • 使用lua脚本。Lua脚本跟单个Redis命令及MULTI/EXEC组成的事务一样,都是原子操作。Redis采用单线程设计,每次只能执行一个命令,每个单独的命令都是原子的。Lua脚本有两个好处:(1)减少多个操作通信往返带来的开销(2)无需担心由于事务竞争导致的性能开销。

  • 尽可能使用时间复杂度为O(1)的操作,避免使用复杂度为O(N)的操作。避免使用这些O(N)命令主要有几个办法:(1)不要把List当做列表使用,仅当做队列来使用;(2)通过机制严格控制Hash、Set、Sorted Set的大小;(3)可能的话,将排序、并集、交集等操作放在客户端执行;(4)绝对禁止使用KEYS命令;(5)避免一次性遍历集合类型的所有成员,而应使用SCAN类的命令进行分批的,游标式的遍历

Redis提供了Slow Log功能,可以自动记录耗时较长的命令,redis.conf中的配置如下

  #执行时间慢于10000毫秒的命令计入Slow Log
  slowlog-log-slower-than 10000
#最大纪录多少条Slow Log slowlog-max-len 128
使用SLOWLOG GET n命令,可以输出最近n条慢查询日志。使用SLOWLOG RESET命令,可以重置Slow Log


参考

Redis实战

Redis内部数据结构详解(4)——ziplist

Redis内部数据结构详解(5)——quicklist