当前位置:首页 > 问答 > 正文

Redis里Map怎么存的那些事儿,聊聊底层原理和应用场景

主要参考了《Redis设计与实现》一书、Redis官方文档以及一些技术社区如Stack Overflow、CSDN上的相关讨论)

咱们今天就来聊聊Redis里的Map,也就是我们常说的Hash类型,这个东西用起来很简单,就像是我们编程语言里的字典或者HashMap,一个key对应着一堆field和value,但它在Redis的肚子里是怎么存的,什么时候用最合适,这里面还是有些门道的。

底层怎么存的?两种面孔,看情况变脸

Redis里Map怎么存的那些事儿,聊聊底层原理和应用场景

Redis是个内存高手,它非常珍惜内存空间,对于Hash结构,它准备了两种底层编码方式来存储,就好像有两套不同的储物柜,东西少的时候用小而精致的储物柜,东西多了就自动换成大容量的储物柜,这两种编码方式是:ziplist(压缩列表)hashtable(哈希表),Redis会根据你存入的数据量大小,自动在这两种格式之间切换,这个转换的阈值可以在配置文件里设置。

  1. 当Hash还“小”的时候:用ziplist(压缩列表) 你可以把ziplist想象成一个长长的、紧凑的“包裹”或者“行李袋”,它不是像普通链表那样一个节点一个节点分开的,而是把所有数据(包括键和值)都紧密地排列在一块连续的内存空间里。

    • 怎么存的? 比如我们执行命令 HSET user:1 name "张三" age 30,在ziplist里,它会按顺序存放:name -> "张三" -> age -> 30,一个字段名紧跟着它的值,再跟着下一个字段名和值,像排队一样。
    • 优点: 因为内存是连续的,没有那些零零碎碎的内存开销(比如每个节点需要的指针),所以非常节省内存,对于字段少、值也不长的Hash,用这种方式是最高效的。
    • 什么时候用? 当同时满足两个条件时,Redis就会使用ziplist:
      • Hash对象保存的所有字段名和字段值的字符串长度都小于配置的值(默认是64字节)。
      • Hash对象保存的字段数量小于配置的值(默认是512个)。 只要有一条不满足,Redis就会觉得这个“包裹”太鼓了,不方便了,就会进行“升级”。
  2. 当Hash变“大”了以后:用hashtable(哈希表) 当字段数量或者值的大小超过上面说的阈值后,Redis就会自动把ziplist转换成真正的hashtable,这个hashtable就和很多编程语言里的HashMap实现思路很像了。

    Redis里Map怎么存的那些事儿,聊聊底层原理和应用场景

    • 怎么存的? 它底层是一个数组,数组的每个位置我们叫它“桶”(bucket),Redis会用一个哈希函数计算出每个字段名(field)应该放在哪个桶里,如果不同的字段名算出来在同一个桶(哈希冲突),就会用链表把它们串起来(在Redis新版本中,为了效率,这个链表在变长后会转换成跳表)。
    • 优点: 虽然比ziplist多占一点内存(因为要存储指针等额外信息),但它的读写速度非常快,而且稳定,无论这个Hash有多大,查询一个字段的平均时间复杂度都是O(1),几乎是瞬间完成。
    • 什么时候用? 就是当Hash不再“小巧”的时候,自动切换过来。

这种“双模式”的设计,体现了Redis在时间和空间之间做的精妙权衡:在数据量小的时候,优先省空间;数据量大的时候,优先保速度。

应用场景:什么时候该请出Hash这位大神?

了解了底层原理,我们就能更好地决定在什么场景下使用Hash了。

Redis里Map怎么存的那些事儿,聊聊底层原理和应用场景

  1. 缓存对象(最经典的用法) 这是Hash最天然、最合适的场景,比如我们要缓存一个用户信息、一件商品信息,相比于把整个用户对象序列化成JSON字符串存成一个String键值,使用Hash有巨大优势:

    • 部分读写: 我可以只用 HGET 命令获取用户的昵称,或者只用 HSET 更新用户的最后登录时间,而不用读取和写入整个庞大的用户对象,这在网络传输和数据处理上都更高效。
    • 存储结构清晰: 字段名(如name, age, email)本身就表达了含义,比解析JSON字符串再查找字段要直观和高效。
  2. 存储聚合数据 比如统计网站每天的不同页面的点击量,我们可以设计一个key叫 page:view:20231027,它的field就是各个页面的URL,value就是点击次数,这样,增加点击量用 HINCRBY 命令非常方便,要获取全天的数据直接 HGETALL 即可,这种需要频繁更新其中某个部分数据的场景,Hash非常适合。

  3. 配置信息存储 系统的各种配置项往往也是键值对的形式,而且可能经常需要单独修改某一项,用Hash来存,管理起来非常方便。

一些使用上的小提示

  • 别滥用HGETALL: 如果一个Hash非常大(比如有几万个字段),直接使用 HGETALL 命令可能会阻塞Redis一段时间,因为要一次性返回所有数据,在这种情况下,可以考虑使用 HSCAN 命令进行游标式的遍历,分批获取,避免服务抖动。
  • 思考“大Key”问题: 如果一个Hash存储了上百万个字段,它本身就会成为一个“大Key”,可能会在迁移、持久化时带来问题,所以设计时要有所考虑,比如是否可以按某种维度拆分成多个小Hash。
  • 与String类型的权衡: 如果需要整个对象一起设置、一起过期,或者使用自增等操作,有时String类型也可能是个选择,要根据具体的读写模式来决定。

Redis的Hash是一个灵活而强大的数据结构,它通过内部智能切换ziplist和hashtable两种存储方式,在内存和性能之间取得了很好的平衡,当你需要表示一个对象,或者存储一组需要单独操作的键值对时,它通常是你的首选。