Hash 是什么?

Redis Hash 本质是一个字段-值(field-value)集合,适合存储对象。

例如:

HSET user:1001 name "mianshiya" age 18 city "Tokyo"
HGET user:1001 name
HINCRBY user:1001 age 1

Redis 不会一次性把整个哈希表搬完,而是采用:

渐进式 Rehash

原因:

Redis 单线程,一次性迁移大量数据会阻塞其他请求。

流程:

ht[0]:旧表
ht[1]:新表

        ↓ 扩容

每次处理请求
        ↓
迁移一部分桶
        ↓
rehashidx++
        ↓
继续迁移
        ↓
全部迁移完成
        ↓
ht[1] 成为新 ht[0]

Rehash 期间:

  • 新数据直接写 ht[1]
  • 查询两个表都查
  • 增删改查过程中顺便迁移数据
  • rehashidx = -1 表示没有进行 Rehash

负载因子=已存储的哈希表节点数量/哈希表总容量

rehash 触发条件

触发条件也非常简单:

  • 负载因子 >= 1 && Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令
  • 负载因子 >= 5,直接触发

然后还有一个点,负载因子 < 0.1 会触发缩容操作。

提问:Hash 和 String 存 JSON 相比,各有什么优缺点?

回答:Hash 的优势是能单独改某个字段,不用整个对象读出来改完再写回去,省网络带宽也省 CPU。缺点是不支持嵌套结构,只能存一层键值对。String 存 JSON 能存复杂嵌套结构,但改单个字段得整体覆盖。一般对象属性平铺的用 Hash,结构复杂的用 String 存 JSON。

提问:渐进式 rehash 期间,读写操作是怎么处理的?

回答:写操作直接写到新表 ht[1],保证新数据不会被搬两次。读操作先查 ht[0],没找到再查 ht[1]。删除和更新也是两个表都要检查。每次操作完顺便搬一个桶,所以 rehash 期间每个请求会稍微慢一点,但不会出现长时间阻塞。

提问:为什么扩容是 2 倍而不是 1.5 倍?

回答:因为哈希表大小必须是 2 的幂次,这样计算桶位置可以用位运算 hash & (size-1) 代替取模,效率高很多。2 倍扩容刚好还是 2 的幂次。另外 2 倍扩容时,原来的元素要么在原位置,要么在原位置 + 原大小的位置,只用看 hash 值新增的那一位是 0 还是 1,迁移逻辑也简单。

提问:Hash 的大 key 问题怎么处理?

回答:一个 Hash 存几十万个字段就是大 key,删除的时候会阻塞很久。解决办法是拆分,比如按字段名哈希取模拆成多个小 Hash。或者用 HSCAN 分批删除,每次删一部分。Redis 4.0 之后有 UNLINK 命令可以异步删除,不会阻塞主线程。