Redis的内存优化方式

文章目录

  • 1.压缩值
    • 1.1如何压缩字符串
    • 1.2优势
    • 1.3权衡
    • 1.4何时避免压缩
  • 2.使用较小的键
    • 2.1如何转换为较小的键
    • 2.2优势
    • 2.3权衡
  • 3.切换到32位
    • 3.1优势
    • 3.2权衡
    • 3.3何时避免切换到32位
  • 4.升级Redis版本
    • 4.1权衡
  • 5.使用更好的序列化器
    • 5.1使用哪些序列化器
      • 5.1.1MessagePack
      • 5.1.2Protocol Buffers
  • 6.将较小的字符串组合为哈希
    • 6.1如何将字符串转换为哈希
    • 6.2优势
    • 6.3权衡
    • 6.4何时避免将字符串组合为哈希
  • 7.从Set切换为Intset
    • 7.1权衡
  • 8.切换到bloom filter或hyperloglog
    • 8.1权衡
  • 9.大哈希分片成小哈希
    • 9.1分片如何发生
    • 9.2注意事项
    • 9.3权衡
  • 10.将哈希的Hashtable转换为Ziplist
    • 10.1为什么Ziplist使用更少的内存
    • 10.2权衡
  • 11.转换为List而不是Hash
    • 11.1NamedTuple如何工作
    • 11.2权衡
    • 11.3何时避免将Hash转换为List
  • 12.压缩字段名
    • 12.1压缩字段名是什么意思
  • 13.避免动态Lua脚本
    • 13.1使用动态Lua脚本的注意事项
  • 14.启用List压缩
    • 14.1什么是压缩深度
    • 14.2权衡
  • 15.更快地回收过期的键内存
    • 15.1如何检测过期后是否未回收内存
    • 15.2如何更快地回收过期的键内存
    • 15.3权衡

redis内存优化

1.压缩值

Redis和客户端通常受IO约束,并且相对于请求/应答序列的其余部分,IO成本通常至少为2个数量级。默认情况下,Redis不会压缩存储在其中的任何值,因此在存储到Redis中之前压缩数据变得非常重要。这有助于减少有效负载,从而为你带来更高的吞吐量、更低的延迟和更高的成本节省。

1.1如何压缩字符串

有多种压缩算法可供选择,每种算法都有其自身的权衡。

  1. Google的Snappy旨在实现极高的速度和合理的压缩。
  2. LZO压缩速度快和解压缩速度快。
  3. 其它的诸如Gzip应用更为广泛。

GZIP压缩比Snappy或LZO使用更多的CPU资源,但提供了更高的压缩比。对应不经常访问的冷数据,GZip通常是一个不错选择。对于经常访问的热数据,Snappy或LZO是更好的选择。

压缩字符串需要更改代码。有些库可以透明地压缩对象,你只需要配置库即可。在其它情况下,你可能必须手动压缩数据。

1.2优势

压缩字符串可以节省30-50%的内存。通过压缩字符串,还可以减少应用程序和Redis数据库之间的网络带宽。

1.3权衡

压缩/解压缩需要你的应用程序执行额外的工作。这种权衡通常是值得的。如果你担心额外的CPU负载,请切换到一个更快的算法,例如snappy或LZO。

1.4何时避免压缩

压缩不应盲目地遵循,有时压缩不能帮助你减少内存,反而会提高CPU利用率。在少数情况下应避免压缩:

  1. 对于较短的字符串,可能会浪费时间。短字符串通常不会压缩太多,因此增益太小。
  2. 当数据结构不合理时,则应避免压缩。JSON和XML擅长压缩,因为它们有重复的字符和标签。

2.使用较小的键

Redis键在增加Redis实例的内存消耗方面起到了魔鬼般的作用。通常,你应该始终偏爱描述性键,但是如果你拥有一个包含数百万个键的大型数据集,那么这些大键可能会花费你的大量金钱。

2.1如何转换为较小的键

在编写良好的应用程序中,切换到较短的键通常涉及更新应用程序代码中的几个常量字符串。

你必须识别Redis实例中的所有大键,并通过删除其中的额外字符来缩短它。你可以通过两种方式实现:

  1. 你可以使用RedisInsight识别Redis实例中的大键。这为你提供了有关所有键的详细信息,以及一种根据键的长度对数据进行排序的方法。
  2. 或者,你可以运行命令redis-cli --bigkeys

使用RedisInsight的优势在于,它为你提供了整个数据集中的大键,而bigkeys命令运行在特定的记录集上,并从该记录集中返回大键,因此,使用bigkeys很难从整个数据集中识别大键。

2.2优势

举个例子:假设你有100,000,000个键名称,例如

my-descriptive-large-keyname (28个字符)

现在,如果你将键名称缩短为

my-des-lg-kn (12个字符)

通过缩短键来节省16个字符,即16个字节,这可以节省1,000,000,000*16 = 1.6GB的RAM内存!

2.3权衡

大键比短键更具描述性,因此,在读取数据库时,你可能会发现键的相关性较差,但与这种痛苦相比,节省内存和成本更高效。

3.切换到32位

Redis为你提供了有关64位计算机的以下统计信息。

  1. 一个空的实例使用~3MB的内存。
  2. 1百万个小的键->字符串值对使用~85MB的内存。
  3. 1百万个键->哈希值,表示一个有5个字段的对象,使用~160MB的内存。

与32位计算机相比,64位有更多可用内存。但是,如果你确定数据大小不超过3 GB,那么存储在32位是一个不错的选择。

64位系统比32位系统使用更多的内存来存储相同的键,尤其是在键和值较小的情况下。这是因为为小键分配了完整的64位,导致浪费了未使用的位。

3.1优势

从64位计算机切换到32位可以大大降低所用计算机的成本,并可以优化内存的使用。

3.2权衡

对于32位Redis变体,任何大于32位的键名都要求该键跨越多个字节,从而增加了内存使用量。

3.3何时避免切换到32位

如果你的数据大小预计会增加超过3 GB,则应避免切换。

4.升级Redis版本

Redis 4.0是已发包的最新版本。与以前的版本相比,它包含各种重大改进。

  1. 它支持RDB+AOF混合格式。
  2. 改善内存使用和性能。
  3. 引入了新的Memory命令。
  4. 活动内存碎片整理。
  5. 更快地创建Redis集群键。

4.1权衡

Redis 4.0仍然不是一个稳定的发行版,而是经过严格测试的发行版,因此Redis 3.2是关键应用程序的更好选择,直到Redis 4.0在接下来的几个月中更加成熟为止。

5.使用更好的序列化器

Redis没有任何特定的数据类型来存储序列化对象,它们作为字节数组存储在Redis中。如果我们使用常规的方法来序列化java、python和PHP对象,则它们的大小可能会更大,从而影响内存消耗和延迟。

5.1使用哪些序列化器

代替你的编程语言的默认序列化程序(java序列化对象、python pickle、PHP序列化等),切换到更好的库。有各种库,例如——协议缓冲区Protocol Buffers,消息包MessagePack等。

5.1.1MessagePack

MessagePack是一种有效的二进制序列化格式。它使你可以在多种语言(如JSON)之间交换数据。但是它更快,更小。小整数被编码为一个字节,典型的短字符串除字符串本身外仅需要一个额外的字节。

正如Redis的创建者Salvatore Sanfilippo所说:

Redis scripting has support for MessagePack because it is a fast and compact serialization format with a simple to implement specification. I liked it so much that I implemented a MessagePack C extension for Lua just to include it into Redis.
Redis脚本支持MessagePack,因为它是一种快速紧凑的序列化格式,具有易于实现的规范。我非常喜欢它,所以我为Lua实现了一个MessagePack C扩展,只是将其包含在Redis中。

5.1.2Protocol Buffers

Protocol buffers(通常称为Protobuf)是Google开发的一种协议,用于允许对结构化数据进行序列化和反序列化。Google开发它的目的是提供一种比XML更好的方式来使系统进行通信。因此,他们专注于使其比XML更简单、更小、更快和更可维护。

6.将较小的字符串组合为哈希

在64位计算机上,字符串数据类型的开销约为90字节。换句话说,调用set foo bar使用大约96个字节,其中90个字节是开销。只有在以下情况下才应使用String数据类型:

  1. 该值至少大于100个字节
  2. 将编码的数据存储在字符串中——JSON编码或Protocol buffer
  3. 使用字符串数据类型作为数组或位集

如果你没有执行上述任何操作,请使用哈希

6.1如何将字符串转换为哈希

假设我们必须在用户的帖子上存储评论数,我们可以有一个键名,例如user:{userId}:post:{postId}:comments

这样,我们每个用户的每个帖子都有一个键。所以现在,如果我们需要查找整个应用程序的评论总数,我们可以做

Redis::mget("user:{
     $userId}:post:1", "user:{
     $userId}:post:2", ...);

要将其转换为哈希,你可以执行以下操作:

Redis::hmset("user:{
     $userId}:comments", "post:1", 20, "post:2", 50);

这将构建一个Redis哈希,其中包含两个字段post:1post:2,其值分别为20和50。

6.2优势

将小字符串组合为哈希可以减少使用的内存,从而节省成本。

哈希可以在一个非常小的内存空间中有效地编码,因此Redis制作者建议我们尽可能使用哈希,因为“几个键比一个包含几个字段的哈希的单个键使用更多的内存”,一个键表示一个Redis对象包含的信息比它的值要多的多,另一方面,一个哈希字段只包含指定的值,这就是为什么它更高效的原因。

6.3权衡

性能是有代价的。通过将字符串转换为哈希,我们节省了内存,因为它只保存字符串值,而没有额外的信息,例如:空闲时间idle time、过期时间expiration、对象引用计数object reference count以及与之相关的编码encoding。但是,如果我们希望键具有过期值,则不能将其与哈希结构相关联,因为过期不可用。

6.4何时避免将字符串组合为哈希

这个决定取决于字符串的数量,如果少于100万且内存消耗不高,则转换不会受到很大影响,并且没有必要增加代码的复杂性。

但是,如果字符串超过100万,并且内存消耗很高,那么肯定应该遵循这种方法。

7.从Set切换为Intset

仅包含整数的Set集合在内存方面非常节省。如果你的Set集合包含字符串,请尝试通过将字符串标识符映射到整数来使用整数。

你可以在编程语言中使用枚举,也可以使用redis哈希数据结构将值映射为整数。切换为整数后,Redis将在内部使用IntSet编码。

这种编码非常节省内存。默认情况下,set-max-intset-entries的值为512,但是你可以在redis.conf中设置此值。

7.1权衡

通过增加set-max-intset-entries的值,Set集合操作的延迟会增加,并且Redis服务器上的CPU利用率也会增加。你可以在进行此更改之前和之后运行此命令来进行检查。

运行 `info commandstats`

8.切换到bloom filter或hyperloglog

唯一项可能难以计数。通常,这意味着存储每个唯一项,然后以某种方式调用这些信息。使用Redis,可以通过使用set和单个命令来完成此操作,但是对于非常大的set,存储和时间复杂性都是令人望而却步的。HyperLogLog提供了一种概率替代方法。

如果你的set集合包含大量元素,并且只使用集合进行存在性检查或消除重复项,那么使用布隆过滤器bloom filter将使你受益。

Bloom过滤器本身不受支持,但是你可以在redis之上找到几种解决方案。如果你只使用该set集合来计算唯一元素的数量(例如,唯一IP地址、用户访问的唯一页面等),那么切换到hyperloglog可以节省大量内存。

8.1权衡

以下是使用HyperLogLog的权衡:

  1. 从HyperLogLog获得的结果不是100%准确的,它们的标准误差约为0.81%。
  2. Hyperloglog只告诉你唯一计数。它不能告诉你集合中的元素。

例如,如果你想维护今天有多少个唯一的ipaddress进行了一次API调用。HyperLogLog告诉你今天有46966个唯一IPs

但是,如果你想显示这些46966个IP地址——它就不能显示。为此,你需要在一个set集合中维护所有IP地址。

9.大哈希分片成小哈希

如果哈希中包含大量键值对,并且每个键值对都足够小,则将其分解为较小的哈希以节省内存。要切分HASH表,我们需要选择一种对数据进行分区的方法。

哈希本身有键,可用于将键划分为不同的分片。分片的数量取决于我们要存储的键总数和分片的大小。使用分片数量和哈希值,我们可以确定键所在的分片ID。

9.1分片如何发生

  • 数字键——对于数字键,将根据键的数字键值将键分配给分片ID(在同一分片中保持数字相似的键)。

  • 非数字键——对于非数字键,使用CRC32校验和。在这种情况下,使用CRC32是因为它返回一个简单的整数而无需进行额外工作,并且计算速度很快(比MD5或SHA1哈希算法快得多)。

9.2注意事项

在进行分片时,你应该对预期元素的总数分片大小保持一致,因为这两个信息是减低分片数量说必需的。理想情况下,你不应该更改这些值,因为这会更改分片的数量。

如果你要更改任何一个值,则应该有一个将数据从旧数据分片移动到新数据分片的过程(通常称为重新分片resharding)。

9.3权衡

将大哈希转换为小哈希的唯一权衡是,这会增加代码的复杂性。

10.将哈希的Hashtable转换为Ziplist

哈希有两种编码类型:HashTableZiplist。根据Redis提供的两种配置——hash-max-ziplist-entrieshash-max-ziplist-values,完成存储哪种数据结构的决定。

默认情况下,redis.conf具有以下设置:

  • hash-max-ziplist-entries = 512
  • hash-max-ziplist-values = 64

因此,如果一个键的任何值超过这两个配置,它将自动存储为Hashtable而不是Ziplist。可以观察到,与Ziplist相比,HashTable消耗几乎两倍的的内存,因此为了节省内存,你可以增加这两个配置并将哈希表转换为ziplist。

10.1为什么Ziplist使用更少的内存

Redis中的ziplist实现通过每个条目只存储三段数据来实现其较小的内存大小; 第一段是前一个条目的长度,第二段是当前条目的长度,第三段是存储的数据。因此,ziplist消耗更少的内存。

10.2权衡

简洁是有代价的,因为更改大小和检索条目需要更多时间。因此,redis服务器上的延迟增加了,CPU使用率也可能增加了。

注意: 同样,对于sorted set,也可以将其转换为ziplist,但是唯一的区别是zset-max-ziplist-entries是128,这比哈希的条目少。

11.转换为List而不是Hash

Redis哈希存储字段名和值。如果你有成千上万个具有相似字段名的小型哈希对象,则字段名使用的内存将累加起来。为避免这种情况,请考虑使用List而不是Hash。字段名成为List列表的索引。

虽然这样可以节省内存,但是仅当你具有成千上万个哈希并且每个哈希有相似的字段时,才应使用此方法。压缩字段名是减少字段名使用的内存的另一种方法。

举个例子。假设你要在Redis中设置用户详细信息。你像这样做:

hmset user:123 id 123 firstname Bob lastname Lee location CA twitter bob_lee

现在,Redis 2.6将其内部存储为Zip List;你可以通过运行调试对象user:123并查看encoding编码字段进行确认。在这种编码中,键值对是按顺序存储的,因此我们在上面创建的用户对象将大致如下所示:["firstname", "Bob", "lastname", "Lee", "location", "CA", "twitter", "bob_lee"]

现在,如果你创建第二个用户,则键将被复制。如果你有一百万个用户,那么,再次重复这些键是很大的浪费。为了解决这个问题,我们可以从Python——NamedTuples中借用一个概念。

11.1NamedTuple如何工作

NamedTuple只是一个只读列表,但是它有一些魔术使该列表看起来像字典。你的应用程序需要维护从字段名到索引的映射,例如"firstname" => 0, "lastname" => 1,依此类推。

然后,你只需创建一个List列表而不是一个Hash哈希,例如lpush user:123 Bob Lee CA bob_lee。在应用程序中使用正确的抽象,可以节省大量内存。

11.2权衡

唯一的权衡与代码复杂性有关。对于小哈希和小列表,Redis内部使用相同的编码(ziplist),因此切换到列表时不会对性能造成影响。 但是,如果你的哈希中的字段超过512个,则不建议使用此方法。

11.3何时避免将Hash转换为List

以下是应避免将哈希转换为列表的情况:

  1. 当对象少于50,000个时。
  2. 对象是不规则的,即一些用户拥有很多信息,而其他用户则很少。

12.压缩字段名

Redis哈希由字段及其值组成。与值一样,字段名也消耗内存,因此在分配字段名时需要牢记。如果你有大量具有相似字段名的哈希,则内存将显着增加。为了减少内存使用,可以使用较小的字段名称。

12.1压缩字段名是什么意思

参考前面的将哈希转换为列表的示例,我们有一个包含用户详细信息的哈希。

hmset user:123 id 123 firstname Bob lastname Lee location CA twitter bob_lee

在这种情况下,firstname, lastname, location, twitter是全字段名,可以缩写为:fn, ln, loc, twi。这样做可以节省字段名使用的一些内存。

13.避免动态Lua脚本

不要生成动态脚本,这会导致Lua缓存增长并失去控制。加载脚本会消耗内存。内存消耗是由于以下因素造成的。

  1. 存储原始文本的server.lua_scripts字典使用的内存
  2. Lua内部使用的内存,用于保存编译后的字节码。因此,如果你必须使用动态脚本,则只需使用简单的EVAL,因为没有必要先加载它们。

13.1使用动态Lua脚本的注意事项

  1. 记住要跟踪Lua内存消耗,并使用SCRIPT FLUSH定期刷新缓存。
  2. 不要在Lua脚本中硬编码和/或以编程方式生成键名,因为这会使键名在集群Redis设置中无用。

14.启用List压缩

List只是数组的链接列表,其中没有一个数组被压缩。默认情况下,redis不会压缩列表中的元素。但是,如果使用长列表,并且大多数情况下只从头和尾访问元素,则可以启用压缩。

我们有两种配置:list-max-ziplist-size:-2(8Kb大小,默认);list-compression-depth:0,1,2(默认为0)

redis.conf文件list-compression-depth=1中的配置更改可帮助你实现压缩。

14.1什么是压缩深度

压缩深度是在开始压缩内部节点之前,列表两端保持不变的列表节点的数量。

例:

  1. depth = 1表示压缩除列表的头和尾之外的每个列表节点。
  2. depth = 2表示永远不要压缩head或head->next或tail或tail->prev。
  3. depth = 3开始压缩head->next->next之后以及tail->prev->prev之前,等等。

14.2权衡

对于较小的值(例如,每个列表条目为40个字节),压缩对性能的影响很小。当使用最大ziplist大小为8k的40个字节值时,每个ziplist大约包含200个单独的元素。创建新的ziplist时,你只需支付额外的“压缩开销”费用(在这种情况下,每200个插入一次)。

对于较大的值(例如,每个列表条目为1024字节),压缩确实会对性能产生明显影响,但是对于ziplist大小的所有良好值(-2),Redis仍以每秒150,000次操作以上的速度运行。当使用1024字节值且最大ziplist大小为8k时,每个ziplist最多可以包含7个元素。在这种情况下,每7个插入一次,将支付额外的压缩开销。这就是为什么在1024字节元素的情况下性能略有下降的原因。

15.更快地回收过期的键内存

当你在一个键上设置了过期时间时,redis不会在该瞬间使它过期。相反,它使用随机算法来找出应该过期的键。由于此算法是随机的,因此键可能没有过期。这意味着redis消耗内存来保存已经过期的键。一旦访问键,就将其删除。

如果只有几个键已过期并且Redis还没有删除它们——它是好的。只有当大量键还没有过期时,它是一个问题。

15.1如何检测过期后是否未回收内存

  1. 运行INFO命令,找到所有数据库的total_memory_used以及所有键的总和。
  2. 然后获取一个Redis Dump(RDB)并找出总内存和键总数。

查看不同之处,你可以清楚地指出,已过期的键仍然没有回收大量内存。

15.2如何更快地回收过期的键内存

你可以按照以下三个步骤之一来回收内存:

  1. 重新启动redis服务器
  2. 在redis.conf中增加maxmemory-samples。(默认值为5,最大为10),以便更快地回收过期的键。
  3. 可以设置一个cron作业,该作业在一定间隔后运行scan命令,这有助于回收过期键的内存。
  4. 另外,增加键的过期时间也有帮助。

15.3权衡

如果我们增加maxmemory-samples配置,它将使键更快地过期,但是会花费更多的CPU周期,从而增加了命令的延迟。其次,增加键的过期时间有所帮助,但这需要对应用程序逻辑进行重大更改。

本文参考:
memory-optimizations

你可能感兴趣的:(Redis,压缩值,Hash优化,List压缩,Set优化,Redis内存优化)