carlosfu
作者carlosfu·2021-09-29 10:37
软件开发工程师·快手

Redis开发规范解析(一)--键名设计

字数 3619阅读 1093评论 0赞 0

本篇是第一篇:由键名设计想到的SDS内存优化

原文

1. key名设计

  • (1)【建议】: 可读性和可管理性

以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id

  1. ugc:video:1
  • (2)【建议】:简洁性

保证语义的前提下,控制key的长度,当key较多时,内存占用也不容忽视,例如:

  1. user:{uid}:friends:messages:{mid}简化为u:{uid}:fr:m:{mid}。
  • (3)【强制】:不要包含特殊字符

反例:包含空格、换行、单双引号以及其他转义字符

解析

上面这些内容本来没什么好说的,但是这个就和做菜“放盐少许”一样,key多长才算最佳呢?我们从之前遇到的一个问题讨论下。

一. 1个问题

之前公司有个同事找我,说他申请两个集群,双写两个集群,但是写满后容量是这样的:

遇到此类问题,我还是习惯把老图翻出来:

此类问题有很多种可能:

  • 自身内存:没多少,几百KB
  • Lua内存:没用
  • 缓冲内存:AOF和复制缓冲配置一致,客户端缓冲不存在,客户端已经停了。
  • 对象内存:
  • 键值个数:个数相同
  • 内部编码:都是字符串类型,而且ziplist,quicklist等配置一致

那么问题出现在哪里呢?比较好的是,我们这边Redis的集群ID有一定含义的,比如12xxx开头的是Redis 3.0.x,以13xxx开头的是Redis 3.2.x,以14xxxx开头的是Redis 4.0.x

这时候想到是了可能是SDS的问题,Redis 3.2开始SDS有个升级,https://raw.githubusercontent.com/antirez/redis/3.2/00-RELEASENOTES

    • [NEW] SDS improvements for speed and maximum string length.
  1. This makes Redis more memory efficient in different use cases.
  2. (Design and implementation by Oran Agra, some additional work
  3. by Salvatore Sanfilippo)

二、一个实验

我做了一个简单试验,分别在Redis 3.0.7和Redis 4.0.12,写入10亿个44字节的key和value,代码如下:

  1. //Redis 3.0.7
  2. int port = 6379;
  3. //Redis 4.0.12
  4. int port = 6380;
  5. int byteCount = 44;
  6. Jedis jedis = new Jedis("127.0.0.1", port);
  7. jedis.flushAll();
  8. List keyValueList = new ArrayList();
  9. for (int i=0;i<1000000000;i++) {
  10. //44个字节
  11. String str = (UUID.randomUUID().toString() + UUID.randomUUID().toString()).substring(0, byteCount);
  12. keyValueList.add(str);
  13. keyValueList.add(str);
  14. if (keyValueList.size() % 10000 == 0) {
  15. jedis.mset(keyValueList.toArray(new String[keyValueList.size()]));
  16. keyValueList.clear();
  17. }
  18. }

Redis 3.0.7的内存消耗:

  1. used_memory_human:177G

Redis 4.0.12的内存消耗:

  1. used_memory_human:147G

可以看到Redis 4.0.12的内存消耗比Redis 3.0.7小了30G,几乎可以顶上一台小内存机器了。

三. Redis的SDS

下面来看看为什么选的是44字节:

  1. 内部编码

Redis中的字符串类型,有三种内部编码:raw、embstr、int。当值小于44字节(Redis 3.2+),使用embstr,否则使用raw(这里不讨论int),例如

  1. 44个字节

  2. 127.0.0.1:6379> set key1 4096c7a2-1ae8-4bdc-ada1-a0c705de0f982fbe4c30
  3. OK
  4. 127.0.0.1:6379> strlen key1
  5. (integer) 44
  6. 127.0.0.1:6379> object encoding key1
  7. "embstr"
  8. 45个字节

  9. 127.0.0.1:6379> set key2 4096c7a2-1ae8-4bdc-ada1-a0c705de0f982fbe4c30a
  10. OK
  11. 127.0.0.1:6379> strlen key2
  12. (integer) 45
  13. 127.0.0.1:6379> object encoding key2
  14. "raw"

下图展示了两者的区别,可以看到embstr将redisObject和SDS保存在连续的64字节空间内,这样可以只需要一次jemalloc分配,而对于raw来说,SDS和redisObject分离,需要两次jemalloc,而且占用更多的内存空间。

回头来看内存不一致的问题,实际不存在embstr和raw的区别,因为他们是双写,键值内容应该是完全一致的。那肯定就是SDS的变化:

可以看到embstr在3.2+中使用了叫sdshdr8的结构,在该结构下,元数据只需要3个字节,而Redis需要8个字节,所以总共64个字节,减去redisObject(16字节),再减去SDS的原信息,最后的实际内容就变成了44字节和39字节。

现在回过头来屡一下两个问题:

  1. 字符串多短为好:其实就是要尽量使用embstr。
  2. Redis 3.0 和 Redis 3.2+的sds有很大不同,新版本的sds会根据字符串长度使用不同的原信息,下面来看一下

Redis 3.0

  1. struct sdshdr {
  2. unsigned int len;
  3. unsigned int free;
  4. char buf[];
  5. };

Redis 3.2+ (3.2 4.0 5.0):sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64、

  1. struct attribute ((__packed__)) sdshdr5 {
  2. unsigned char flags; / 3 lsb of type, and 5 msb of string length /
  3. char buf[];
  4. };
  5. struct attribute ((__packed__)) sdshdr8 {
  6. uint8_t len; / used /
  7. uint8_t alloc; / excluding the header and null terminator /
  8. unsigned char flags; / 3 lsb of type, 5 unused bits /
  9. char buf[];
  10. };
  11. struct attribute ((__packed__)) sdshdr16 {
  12. uint16_t len; / used /
  13. uint16_t alloc; / excluding the header and null terminator /
  14. unsigned char flags; / 3 lsb of type, 5 unused bits /
  15. char buf[];
  16. };
  17. struct attribute ((__packed__)) sdshdr32 {
  18. uint32_t len; / used /
  19. uint32_t alloc; / excluding the header and null terminator /
  20. unsigned char flags; / 3 lsb of type, 5 unused bits /
  21. char buf[];
  22. };
  23. struct attribute ((__packed__)) sdshdr64 {
  24. uint64_t len; / used /
  25. uint64_t alloc; / excluding the header and null terminator /
  26. unsigned char flags; / 3 lsb of type, 5 unused bits /
  27. char buf[];
  28. };

四、结论

  1. SDS在Redis 3.2+有可能节省更多的空间,但3.2更像一个过渡版本,Redis 4更加适合(异步删除、psync2、碎片率整理),我已经在线上大量使用,“赶紧”去用吧。
  2. embstr从Redis3 39字节->Redis3.2+ 44字节
  3. 做个环保的程序员,小优化大效果。

如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!

0

添加新评论0 条评论

Ctrl+Enter 发表

作者其他文章

相关文章

相关问题

相关资料

X社区推广