前言
本章接着上一节继续介绍 Redis 的基础数据结构中的 Hash 字典.
基本介绍
Hash 也可以用来存储用户信息, 和 String 不同的是 Hash 可以对用户信息的每个字段单独存储,String 则需要序列化用户的所有字段后存储. 并且 String 需要以整个字符串的形式获取用户, 而 hash 可以只获取部分数据, 从而节约网络流量. 不过 hash 内存占用要大于 String, 这是 hash 的缺点.
> hset books java "Effective java"
(integer) 1
> hset books golang "concurrency in go"
(integer) 1
> hget books java
"Effective java"
> hset user age 17
(integer) 1
>hincrby user age 1 #单个 key 可以进行计数 和 incr 命令基本一致
(integer) 18
Redis 中的 Hash 和 Java 的 HashMap 更加相似, 都是数组 + 链表的结构. 当发生 hash 碰撞时将会把元素追加到链表上. 值得注意的是在 Redis 的 Hash 中 value 只能是字符串.
内部原理
看完基本介绍之后, 我们先来了解下 hash 的内部结构. 第一维是数组, 第二维是链表. 组成一个 hashtable.
部分源码:
struct dictht {
dictEntry **table; //entry 数组
long size; // 数组长度
long used // 数组中的元素个数
...
}
struct dictEntry{
void *key; //hash 的 key
void *val; //hash 的 value
dictEntry *next; // 下一个 dictEntry 链表结构
}
在 Java 中 HashMap 扩容是个很耗时的操作, 需要去申请新的数组, 为了追求高性能,Redis 采用了 渐进式 rehash
策略. 这也是 hash 中最重要的部分.
渐进式 rehash
在 hash 的内部包含了两个 hashtable, 一般情况下只是用一个. 如图所示:
在扩容的时候 rehash 策略会保留新旧两个 hashtable 结构, 查询时也会同时查询两个 hashtable.Redis 会将旧 hashtable 中的内容一点一点的迁移到新的 hashtable 中, 当迁移完成时, 就会用新的 hashtable 取代之前的. 当 hashtable 移除了最后一个元素之后, 这个数据结构将会被删除. 如图所示:
数据搬迁的操作放在 hash 的后续指令中, 也就是来自客户端对 hash 的指令操作. 一旦客户端后续没有指令操作这个 hash.Redis 就会使用定时任务对数据主动搬迁.
正常情况下, 当 hashtable 中元素的个数等于数组的长度时, 就会开始扩容, 扩容的新数组是原数组大小的 2 倍. 如果 Redis 正在做 bgsave(持久化) 时, 可能不会去扩容, 因为要减少内存页的过多分离(Copy On Write). 但是如果 hashtable 已经非常满了, 元素的个数达到了数组长度的 5 倍时,Redis 会强制扩容.
当 hashtable 中元素逐渐变少时,Redis 会进行缩容来减少空间占用, 并且缩容不会受 bgsave 的影响, 缩容条件是元素个数少于数组长度的 10%.