Redis - hash list (常用命令/内部编码/应用场景)
目录
1. 数据类型 - hash
1.1 hset - 存 field-value
1.2 hget - 取 value
1.2.1 hmget - 批量取 value
1.3 hexists - 判断 field 是否存在
1.4 hdel - 删除 field-value
1.5 hkeys - 获取所有 field
1.6 hvals - 获取所有 value
1.7 hgetall - 获取所有 field-value
1.8 hlen / hsetnx / hincrby / hincrbyfloat
1.9 hash 内部编码
1.10 hash 应用场景
1.10.1 键(key/field)命名规范
1.10.2 应用场景 - 缓存
2. 数据类型 - list
2.1 lpush - 头插
2.1.1 lpushx
2.2 rpush - 尾插
2.2.1 rpushx
2.3 lrange
2.4 lpop / rpop - 头删/尾删
2.5 lindex - 获取指定下标的元素
2.6 linsert - 插入元素
2.7 llen - 获取列表长度
2.8 lrem - 删除指定个数的元素
2.9 ltrim - 删除范围外的元素
2.10 lset - 修改指定下标的值
2.11 blpop / brpop
2.12 list 内部编码
2.13 list 应用场景
2.13.1 作为数组
2.13.2 作为消息队列
2.13.3 构建和存储 Timeline
2.13.4 用作栈和队列
1. 数据类型 - hash
引言: hash 非常非常重要, 是最重要的数据结构(没有之一), 是面试中出场率最高的数据结构, 因此在一定要掌握这个类型.
在 Redis 中, key 永远是字符串, 这里说的数据类型是 hash 指的是 value 的类型.
Redis 自身就是一个哈希表, 因此当 value 也是 hash 时, 就很像 "套娃", hash 中套了一个 hash.
为了区分 Redis 最外层的 key-value 键值对, 因此把内部的键值对称为 field-value, 并且 field 和 value 都必须是 string 类型.

接下来介绍 hash 的常用命令. (使用下文命令, 必须要求 key 对应的 value 是 hash 类型)
1.1 hset - 存 field-value
使用 hset 往 key 对应的 value 中存 field value. 前提: 这个 value 是 hash 类型.
如果 key 存在, 则往 hash 中新增 field value, 如果 key 不存在则创建 hash 并且存 field value.
语法: hset key field value [field value ...]
时间复杂度: O(N) , 这里的 N 是指 field value 的个数.
返回值: 新创建的 field 的个数. (hset 可以一次设置多组 field value)

1.2 hget - 取 value
使用 hget 查询 hash 中 field 对应的 value 指, 一次只能查询一个 field.
语法: hget key field
返回值: 返回 field 对应的 value 值, 如果 key / field 不存在, 则返回 nil.
时间复杂度: O(1)

1.2.1 hmget - 批量取 value
hget 一次只能查询一次 field , 而 hmget 一次可以查询多个 field.

注: 查询到的 value 的顺序和 field 的顺序是匹配的.
1.3 hexists - 判断 field 是否存在
判断 hash 中是否存在指定的 field.
语法: hexists key field
时间复杂度: O(1)
返回值: 如果 field 存在, 返回 1. 如果 field(或者 key) 不存在, 返回 0.

1.4 hdel - 删除 field-value
从 hash 中删除指定的 field 字段.
语法: hdel key field [field ...]
返回值: 删除的字段的个数. (hdel 一次可以删除多个 field)
时间复杂度: O(N) , N 是指删除的 field 的个数.

注意:
hdel 删除的是内层 hash 的 field, 而 del 删除的是 key(删除整个 hash).
1.5 hkeys - 获取所有 field
查询 hash 中所有的 field.
语法: hkeys key
时间复杂度: O(N), 这里的 N 是指 hash 中 field value 键值对的个数.
因此, hkeys 就是查整个 hash, 当 hash 中存的键值对太多时, hkeys 也会阻塞 Redis. 因此要谨慎使用.

1.6 hvals - 获取所有 value
获取 hash 中所有的 value. (和 hkeys 相对)
时间复杂度也是 O(N). N 是 hash 中键值对的个数.

1.7 hgetall - 获取所有 field-value
获取 hash 中所有的 field value. (相当于 hkeys 和 hvals 的结合)
语法: hgetall key
时间复杂度也是 O(N). N 是 hash 中键值对的个数.

上述的 fkeys / fvals / fgetall 都是比较危险的操作, 都有阻塞 Redis 的风险.
而 hscan 命令, 采取 "渐进式遍历" 的方式查询数据, 每次仅扫描部分数据, 多次执行即可完成整体遍历(敲一次命令 遍历一小部分, 敲多次, 就遍历完了), 因此可以保证每次遍历的时间可控.
在 Java 中, ConcurrentHashMap 的扩容机制, 也是采取的这种 "化整为零" 的思想来保证扩容时 数据复制移动 的时间花销可控.
1.8 hlen / hsetnx / hincrby / hincrbyfloat
- hlen key : 获取 key 对应 hash 中 field-value 的个数.
- 时间复杂度: O(1). 返回值: hash 中 field-value 的个数.

- 时间复杂度: O(1). 返回值: hash 中 field-value 的个数.
- hsetnx key field value : 往 hash 中添加 field-value, 只有 field 不存在的时候, 设置成功, 否则设置失败.
- 和 hset 不同, hsetnx 一次只能设置一个 field-value.
- 时间复杂度: O(1). 返回值: 1 设置成功; 0 设置失败.

- hincrby key field num : 对 field 的 value 进行加减运算(前提: value 是整数).
- 加运算, num 就是正数; 减运算, num 就是负数.
- 时间复杂度: O(1). 返回值: 运算后的值.

- hincrbyfloat key field num : 对 field 的 value 进行加减运算(前提: value 是浮点数).
- 加运算, num 就是正数; 减运算, num 就是负数.
- 时间复杂度: O(1). 返回值: 运算后的值.

1.9 hash 内部编码
hash 内部使用了两种编码方式:
- ziplist : 压缩列表, 节省内存空间. 但读写元素时, 效率低下.
- hashtable : 真正的哈希表, 增删查改效率高. 由于 hash 是一个数组, 因此当数据稀疏时会存在一定的内存浪费.
当同时满足以下条件时, 才会使用 ziplist:
- hash 中的键值对(field-value)较少. (如果元素过多时, 会转换为 hashtable)
- 每个 value 的长度都比较短. (如果某个 value 太长了, 也会转换为 hashtable)

Redis 会根据阈值(如字段数量和单个字段长度)在两种结构间自动切换. 这个阈值是在 Redis 的配置文件(redis.conf)中定义的:
- hash-max-ziplist-entries (默认 512 字节) : 当 hash 中的元素小于 512 时, 使用 ziplist.
- hash-max-ziplist-value (默认 64 字节) : 当 value 长度小于 64 时, 使用 ziplist.
1.10 hash 应用场景
1.10.1 键(key/field)命名规范
由于 Redis 没有像 mysql 那些数据库的 表/字段 的定义, 为了防止键名冲突和增加可维护性, 命令时可以使用类似 "对象名:唯一标识:属性" 的格式作为键名.
比如: 往 Redis 中存入用户的不同用户的信息:
key : user:1 (表示 id 为 1 的用户)
field1 : name ; value1 : 张三
field2 : age ; value2 : 18
field3 : gender ; value3 : male

1.10.2 应用场景 - 缓存
hash 和 string 一样, 也可用于缓存. 并且, 如果存储的是 结构化数据, 那么 hash 更合适, 比如上面提到的存储用户信息.
如果要修改用户信息时, 如果存入的 hash, 那么只需通过 hset 修改 field 即可; (但是, hash 也需要消耗更多的内存空间, ziplist 和 hashtable 之间的转换也需要消耗时间)
而若存的是 string 类型的 json 格式的数据, 那么需要将整个 json 字符串全部取出来, 再修改其中的 field , 再将修改后的 json 存入 Redis 中.

2. 数据类型 - list
这里的 list 依旧是指 value 的类型, 且 list 中的每个元素必须是 string.
Redis 的 list 并不是普通的 列表/顺序表, 而是一个 dequeue(双端队列), 可以进行头插尾插, 头删尾删操作. 因此, 两头插入/删除元素的时间复杂度为 O(1), 非常高效.

此外, list 是一个 "有序" 的列表, 这里的 "有序" 不是指升序/降序, 而是指顺序很关键.
比如: 两个 list, 他们里面的元素一样, 但是元素的顺序不一样, 那么他们就是两个不同的 list..
2.1 lpush - 头插
语法: lpush key element [element ...]
lpush, l 指的是 left, 即往 list 中头插元素.
时间复杂度: O(1). 返回值: list 的长度.
如果 key 已存在, 且 key 对应的 value 不是 list, 就会报错.

注意: 头插时, 是按照命令中元素的顺序, 依次进行头插的, 因此上图的命令执行完后, 5 排在最前面, 1 排在最后面:

2.1.1 lpushx
语法: lpushx key element [element ...]
lpushx 中的 x 是指 exists, 只有当 key 存在时, 才会往 list 中头插元素, 而上面的 lpush 是如果 key 不存在就直接创建一个新的 list.
返回值是 list 的长度.

2.2 rpush - 尾插
语法: rpush key element [element ...]
rpush, r 指的是 right, 即往 list 中尾插元素.
时间复杂度: O(1). 返回值: list 的长度.
如果 key 已存在, 且 key 对应的 value 不是 list, 就会报错.

尾插时, 依旧按照命令中元素的顺序进行尾插.
2.2.1 rpushx
语法: rpushx key element [element ...]
rpushx, 只有当 key 存在时, 才会尾插元素.


2.3 lrange
语法: lrange key start end
l 指的是 list, 表示查询 list 中 [start, end] 范围内的元素.
- start: 起始位置的下标.
- end: 结束位置的下标.
在 Redis 中, 下标可以是负数, 表示倒数第几个元素.

如果指定的下标, 超出了 list 的范围, Redis 不会想 Java 那样报个下标越界的错误, 而是会尽可能返回范围内的元素:

2.4 lpop / rpop - 头删/尾删
语法:
- lpop key : 从 list 中头删元素.
- rpop key : 从 list 中尾删元素.
返回值: 返回删除的元素的值. 若 list 为空(key 不存在), 则返回 nil.
时间复杂度: O(1)

2.5 lindex - 获取指定下标的元素
语法: lindex key index
获取 list 中指定下标的元素. 时间复杂度: O(N), N 是指 list 中元素的个数.
返回值: 指定下标元素的值. 若元素不存在, 则返回 nil.

2.6 linsert - 插入元素
在基准元素的前面/后面插入新元素.
语法: linsert key <before | after> pivot element
- <before | after> : 在目标元素的前面还是后面插入.
- pivot : 基准元素的值. (注意, 是值, 而非下标)
- element : 要插入的新元素.
返回值: 插入后, list 的长度. 时间复杂度: O(N), N 是指 list 的长度.

插入时, 是从左向右遍历列表, 找到基准元素后, 再向其 前/后 插入元素.
因此, 若基准元素在 list 中存在多个, 那从左向右的第一个就是要插入的位置.

2.7 llen - 获取列表长度
语法: llen key

2.8 lrem - 删除指定个数的元素
语法: LREM key count element
其中, count 指要删除的个数(删除多少个). element 指要删除的元素的值(根据值去删除).
并且, count 的值不同时, 删除的规则也不同:
- count > 0: 从左向右找值为 element 的元素, 删除 count 个.

- count < 0: 从右向左找值为 element 的元素, 删除 count 个.

- count = 0: 删除 list 中所有值为 element 的元素.

时间复杂度为 O(N+M), 其中 N 是指 list 的个数(遍历 list 为 O(N)), M 是指要删除元素的个数(删 M个元素为 O(M)).
返回值: 成功删除的个数.
2.9 ltrim - 删除范围外的元素
语法: LTRIM key start stop
删除 list 中 [start, stop] 范围外的元素, 保留 [start, stop] 范围内的元素.
时间复杂度: O(N), N 是指删除元素的个数. (并非 list 的长度, 因为只需 头删/尾删 N 次即可, 无需遍历 list)
2.10 lset - 修改指定下标的值
语法: LSET key index element
将 index 下标的值修改为 element.
时间复杂度: O(N)

若下标(index) 越界, 则会报错:

2.11 blpop / brpop
blpop / brpop 是 lpop / rpop 的阻塞版本, b 就是指 bloke(阻塞).
- 当 list 中存在元素, blpop / brpop 和 lpop / rpop 的作用完全一致.
- 当 list 为空, blpop / brpop 则会发生阻塞, 直到 list 不空或者到达超时时间为止.
类似于阻塞队列, Java 中的 BlockingQueue, 但是 Redis 的 blpop / brpop 只考虑队列空的情况, 不考虑队列满的情况.
语法: BLPOP key [key ...] timeout
- blpop / brpop 可以指定多个 key(也就是多个 list), 但是只会弹出一个元素.
- 若一个或者多个 list 不为空, 立即从左到右检查 list, 并将第一个非空 list 中弹出一个元素并返回
- 若所有 list 都为空, blpop 会进入阻塞, 直到被其他客户端 push 进元素(立刻弹出元素并返回)或者到达超时时间(主动退出).
- blpop / brpop 也可以指定阻塞的超时时间(单位为秒), 当到达超时时间后, list 仍然为空, 则不再等待.
- 如果多个客户端对同一个 list 执行了 blpop, 那么最先执行命令的客户端会 pop 出元素. (先来后到)
- A 和 B 两个客户端都在 blpop 一个空的 list, 但 A 先执行的命令. 后续当一个元素被推入进这个空列表中时, Redis 只会把这个元素交给先来的 A 去 pop, 而 B 仍然处于阻塞状态, 等待下一个元素进入.
返回值: 弹出元素所在的 key 和元素的值 或者 nil.
注意: blpop / brpop 进行阻塞时, 并不会阻塞 Redis 服务器, 因此其他客户端可以操作其他的命令. (虽然 Redis 是单线程的, 但是对 blpop 和 brpop 进行了特殊处理).
情况一: 针对一个非空 list 进行操作.

情况二: 针对一个空 list 进行操作, 等待其他客户端 push 元素.

情况三: 针对多个 list 进行操作.

blpop 和 brpop 这俩命令, 主要用于消息队列使用, 但是功能有限, 因此使用的频率也不高.
2.12 list 内部编码
在旧版 Redis 中, list 有两种实现方式:
- ziplist : 压缩列表. 数据按照更加紧凑的形式进行表示, 节省空间. 但插入删除效率低(需要移动元素.)
- 当元素较少时, 插入删除开销也低. 可以使用 ziplist.
- 当元素多时, 插入删除开销就大了起来, 并且根据压缩规则进行解压缩, 当元素过多时, 效率也比较低.
- linkedlist : 链表. 插入删除时效率高, 但占用空间大.
在 Redis3.0 开始, Redis 采用 quicklist 来实现 list, quicklist 结合了 ziplist 和 linkedlist.

quicklist 整体是一个链表, 但是链表上的元素是 ziplist(当 ziplist 体积达到一个阈值, 就会拆分成多个, 再以链式结构进行连接.). 因此, 可以保证所存储的每个元素是压缩过的, 节省了空间, 并且也保证了插入删除的效率.
上面提到的 ziplist 体积的阈值, 是可以在 Redis 配置文件中设置的.
2.13 list 应用场景
2.13.1 作为数组
使用 list 作为数组, 存储多个元素.

2.13.2 作为消息队列
list 并不是一个普通的列表, 而是一个双端队列, 再结合上 lpush / rpush 入队列和 blpop / brpop 出队列, 就能够在生产者消费者模型中当做一个消息队列使用.

此外, 也可以使用多个 list 来实现 分频道阻塞消息队列:
比如抖音, 就可以使用一个 list 单独传输短视频数据, 使用一个 list 单独传输点赞数据, 使用一个 list 单独传输评论数据 ....

2.13.3 构建和存储 Timeline
比如构建博客 timeline(博客列表).
// 1. 使用 hash 存储博客信息.
hset blog:1 titl1 xxx timestamp 12625376625 content xxx
hset blog:2 titl1 xxx timestamp 12625376625 content xxx
hset blog:3 titl1 xxx timestamp 12625376625 content xxx
.......
hset blog:n titl1 xxx timestamp 12625376625 content xxx// 2. 使用 list 存储用户有哪些博客
lpush user:1:blogs blog:1 blog:2 blog:3 ......
lpush user:2:blogs xxxxxx// 3. 查询 user1 的前 10 篇博客(分页查询)
keylist = lrange user:1:blogs 0 9
for(key : keylist) {// 根据 key 从 hash 中查询所有的 field-valuehgetall key
}
2.13.4 用作栈和队列
- 同侧存取(lpush + lpop / rpush + rpop)时, 为栈.
- 异侧存取(lpush + rpop / rpush + lpop)时, 为队列.
end

