Redis - hash & list (常用命令/内部编码/应用场景)

目录

[1. 数据类型 - hash](#1. 数据类型 - hash)

[1.1 hset - 存 field-value](#1.1 hset - 存 field-value)

[1.2 hget - 取 value](#1.2 hget - 取 value)

[1.2.1 hmget - 批量取 value](#1.2.1 hmget - 批量取 value)

[1.3 hexists - 判断 field 是否存在](#1.3 hexists - 判断 field 是否存在)

[1.4 hdel - 删除 field-value](#1.4 hdel - 删除 field-value)

[1.5 hkeys - 获取所有 field](#1.5 hkeys - 获取所有 field)

[1.6 hvals - 获取所有 value](#1.6 hvals - 获取所有 value)

[1.7 hgetall - 获取所有 field-value](#1.7 hgetall - 获取所有 field-value)

[1.8 hlen / hsetnx / hincrby / hincrbyfloat](#1.8 hlen / hsetnx / hincrby / hincrbyfloat)

[1.9 hash 内部编码](#1.9 hash 内部编码)

[1.10 hash 应用场景](#1.10 hash 应用场景)

[1.10.1 键(key/field)命名规范](#1.10.1 键(key/field)命名规范)

[1.10.2 应用场景 - 缓存](#1.10.2 应用场景 - 缓存)

[2. 数据类型 - list](#2. 数据类型 - list)

[2.1 lpush - 头插](#2.1 lpush - 头插)

[2.1.1 lpushx](#2.1.1 lpushx)

[2.2 rpush - 尾插](#2.2 rpush - 尾插)

[2.2.1 rpushx](#2.2.1 rpushx)

[2.3 lrange](#2.3 lrange)

[2.4 lpop / rpop - 头删/尾删](#2.4 lpop / rpop - 头删/尾删)

[2.5 lindex - 获取指定下标的元素](#2.5 lindex - 获取指定下标的元素)

[2.6 linsert - 插入元素](#2.6 linsert - 插入元素)

[2.7 llen - 获取列表长度](#2.7 llen - 获取列表长度)

[2.8 lrem - 删除指定个数的元素](#2.8 lrem - 删除指定个数的元素)

[2.9 ltrim - 删除范围外的元素](#2.9 ltrim - 删除范围外的元素)

[2.10 lset - 修改指定下标的值](#2.10 lset - 修改指定下标的值)

[2.11 blpop / brpop](#2.11 blpop / brpop)

[2.12 list 内部编码](#2.12 list 内部编码)

[2.13 list 应用场景](#2.13 list 应用场景)

[2.13.1 作为数组](#2.13.1 作为数组)

[2.13.2 作为消息队列](#2.13.2 作为消息队列)

[2.13.3 构建和存储 Timeline](#2.13.3 构建和存储 Timeline)

[2.13.4 用作栈和队列](#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

  1. hlen key : 获取 key 对应 hash 中 field-value 的个数.
    1. 时间复杂度: O(1). 返回值: hash 中 field-value 的个数.
  2. hsetnx key field value : 往 hash 中添加 field-value, 只有 field 不存在的时候, 设置成功, 否则设置失败.
    1. 和 hset 不同, hsetnx 一次只能设置一个 field-value.
    2. 时间复杂度: O(1). 返回值: 1 设置成功; 0 设置失败.
  3. hincrby key field num : 对 field 的 value 进行加减运算(前提: value 是整数).
    1. 加运算, num 就是正数; 减运算, num 就是负数.
    2. 时间复杂度: O(1). 返回值: 运算后的值.
  4. hincrbyfloat key field num : 对 field 的 value 进行加减运算(前提: value 是浮点数).
    1. 加运算, num 就是正数; 减运算, num 就是负数.
    2. 时间复杂度: O(1). 返回值: 运算后的值.

1.9 hash 内部编码

hash 内部使用了两种编码方式:

  1. ziplist: 压缩列表, 节省内存空间. 但读写元素时, 效率低下.
  2. hashtable : 真正的哈希表, 增删查改效率高. 由于 hash 是一个数组, 因此当数据稀疏时会存在一定的内存浪费.

当同时满足以下条件时, 才会使用 ziplist:

  1. hash 中的键值对(field-value)较少. (如果元素过多时, 会转换为 hashtable)
  2. 每个 value 的长度都比较短. (如果某个 value 太长了, 也会转换为 hashtable)

Redis 会根据阈值(如字段数量和单个字段长度)在两种结构间自动切换. 这个阈值是在 Redis 的配置文件(redis.conf)中定义的:

  1. hash-max-ziplist-entries (默认 512 字节) : 当 hash 中的元素小于 512 时, 使用 ziplist.
  2. 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] 范围内的元素.

  1. start: 起始位置的下标.
  2. 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

  1. <before | after> : 在目标元素的前面还是后面插入.
  2. pivot : 基准元素的值. (注意, 是值, 而非下标)
  3. element : 要插入的新元素.

返回值: 插入后, list 的长度. 时间复杂度: O(N), N 是指 list 的长度.

插入时, 是从左向右遍历列表, 找到基准元素后, 再向其 前/后 插入元素.

因此, 若基准元素在 list 中存在多个, 那从左向右的第一个就是要插入的位置.

2.7 llen - 获取列表长度

语法: llen key

2.8 lrem - 删除指定个数的元素

语法: LREM key count element

其中, count 指要删除的个数(删除多少个). element 指要删除的元素的值(根据值去删除).

并且, count 的值不同时, 删除的规则也不同:

  1. count > 0: 从左向右找值为 element 的元素, 删除 count 个.
  2. count < 0: 从右向左找值为 element 的元素, 删除 count 个.
  3. 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 有两种实现方式:

  1. ziplist : 压缩列表. 数据按照更加紧凑的形式进行表示, 节省空间. 但插入删除效率低(需要移动元素.)
    1. 当元素较少时, 插入删除开销也低. 可以使用 ziplist.
    2. 当元素多时, 插入删除开销就大了起来, 并且根据压缩规则进行解压缩, 当元素过多时, 效率也比较低.
  2. 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(博客列表).

java 复制代码
// 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-value
    hgetall key
}

2.13.4 用作栈和队列

  • 同侧存取(lpush + lpop / rpush + rpop)时, 为栈.
  • 异侧存取(lpush + rpop / rpush + lpop)时, 为队列.

end

相关推荐
一个儒雅随和的男子3 小时前
Redis连接超时排查与优化指南
redis·bootstrap·php
半路_出家ren3 小时前
图书销售系统数据库设计方案
数据库·mysql·子查询·ddl·dml·数据库设计·分组查询
自由会客室4 小时前
Ubuntu24.04
数据库·postgresql
ayaya_mana4 小时前
MySQL忘记Root密码,详细找回密码步骤
数据库·mysql·adb
顾三殇4 小时前
【MySQL】win 10 / win11:mysql 5.7 下载、安装与配置
数据库·mysql
言之。5 小时前
Django `select_related` 查询优化
数据库·django·sqlite
潜心编码5 小时前
基于Django的医疗电子仪器系统
前端·数据库·1024程序员节
芙蓉王真的好15 小时前
Django 新手常见错误:模板找不到、数据库连接失败的解决办法
数据库·django·sqlite
言之。5 小时前
django model Manager
数据库·django·sqlite