Redis知识整合(一):常用数据结构&指令集

前言:Redis各版本重大更新

截至2021年5月,Redis的稳定版本已经更新到了6.2,所以这里我想整理一下redis自发布以来每一个大版本的重要更新,当然我不会全部都展示出来,只挑那些具有重要革新的功能说。

redis2.6

发行于2012年,相比旧版,它新支持了以下功能:

  • redis-server支持Lua脚本
  • 键的过期时间可以精确到毫秒
  • slave节点提供只读功能
  • 新指令:bitcountbittop(操作位图用的)
  • 基于浮点数的自增功能:incrbyfloathincrbyfloat
  • redis-cli支持用--eval参数实现Lua脚本

redis2.8

发行于2013年11月,新功能如下:

  • 支持部分主从复制,降低了由于网络问题导致频繁全量复制RDB时对系统造成的压力
  • 支持IPv6
  • 支持config rewrite指令可以将config set持久化到redis的配置文件中
  • 新指令:pubsub(给订阅发布功能用的)
  • redis哨兵v2

redis3.0

发行于2015年4月,新功能如下:

  • 支持redis集群(Redis Cluster),这是redis官方的集群方案,在这之前,有两种方案也可以实现redis的集群:CodisTwemproxy
  • LRU算法大幅提升
  • migrate连接缓存,大幅提升键的迁移速度
  • incrbitcount命令性能提升

redis3.2

  • 新增GEO功能
  • 对redis底层字符串(SDS)在速度和节省空间上都做了相关优化
  • 支持用upstartsystemd管理redis进程
  • 新的list编码类型:quicklist
  • 新增hstrlen指令、增强debugspop指令(支持的参数更多了)、增强cluster nodes指令(提速)
  • 新RDB格式(兼容旧版),提升了RDB文件的加载速度
  • Jemalloc更新至4.0.3版本

redis4.0

  • 提供模块功能,方便第三方拓展功能
  • PSYNC2.0:优化了之前版本中,主从节点切换必然引起全量复制的问题
  • 支持LFU,并对已有缓存过期算法进行了优化
  • 提供非阻塞式的delflushall/flushdb功能,解决了删除大key时可能会造成redis阻塞的问题
  • 提供RDB-AOF混合持久化格式,充分结合了RDB和AOF的优点
  • 新增memory指令,使内存监控更加全面
  • Redis Cluster兼容NAT和docker

一、五种数据结构

redis有五种数据结构,在操作它们时会使用到不同的api,在实际开发中,这些api也是离我们开发最近的一块内容,所以熟练掌握常用的api对我们非常有用。

字符串(string)

最基本的k-v格式,value是一个字符串,它的内部编码如下:

图1-1

指令集:

指令 解释 时间复杂度
set [k] [v] 最常用的质量,写入一对k-v O(1)
mset [k1] [v1] [k2] [v2] … 批量写入k-v O(k) k=键数
set [k] [v] nx 只有当key不存在时才会写入成功(新增) O(1)
setnx [k] [v] 作用等同于上面一条 O(1)
set [k] [v] xx 只有当key存在时才会写入成功(修改) O(1)
setxx [k] [v] 作用等同于上面一条 O(1)
get [k] 取value值 O(1)
del [k1] [k2] … 删除掉k-v数据,支持传多个key O(k) k=键数
incr [k] 自增 O(1)
decr [k] 自减 O(1)
incrby [k] [num] 自增指定的数字 O(1)
decrby [k] [num] 自减指定的数字 O(1)
incrbyfloat [k] [num] 自增指定的数字(针对浮点数自增) O(1)
append [k] [v] 往原有字符串尾部追加内容 O(1)
strlen [k] 获取value的长度(单位:字节) O(1)
setrange [k] [offset] [v] 字符替换,offset为被替换字符的位置 O(1)
getrange [k] [start] [end] 获取部分字符串,截取区间:[start, end] O(n) n=字符串长度

适用范围:做基本的分布式缓存、分布式锁、计数/限流、保存集群下的登录状态等。

列表(list)

顾名思义,一个key对应了一个列表,列表内保存着多个value,其内部结构和编码如下:

图1-2

其中list-max-ziplist-entrieslist-max-ziplist-value都是redis的配置项。

指令集:

指令 解释 时间复杂度
rpush [k] [v1] [v2] … 从列表的右边插入元素 O(k) k=插入元素数
lpush [k] [v1] [v2] … 从列表的左边插入元素 同上
linsert [k] before/after [vx] [vn] 向vx的前或后面插入vn O(n) n=vx与表头/尾的距离
lrange [k] [start] [end] 输出指定范围的列表,范围:[start, end],start/end为下标值 O(s+n) s=start偏移量;n=start~end的范围
lindex [k] [index] 获取指定下标的value O(n) n=index偏移量
llen [k] 获取列表长度 O(1)
lpop [k] 弹出左边第一个元素 O(1)
rpop [k] 弹出右边第一个元素 O(1)
blpop [k] [timeout] 阻塞式弹出左边第一个元素,timout非必须,当列表中没有元素时阻塞 O(1)
brpop [k] [timeout] 阻塞式弹出右边第一个元素,timout非必须,当列表中没有元素时阻塞 O(1)
lrem [num] [v] 删除v包括其前后的元素,这取决于num:num>0,从左至右删除最多num个元素;num<0,从右至左删除最多|count|个元素;num=0,全部删除 O(n) n=列表长度
ltrim [k] [start] [end] 只保留[start, end]区间的数据 O(n) n=start~end的长度
lset [k] [index] [v] 修改指定下标中的元素 O(n) n=index偏移量

如你所见,我们可以利用lpush和brpop来实现一个消息队列:

图1-3

除此之外,我们可以利用它来保存一个信息id列表,然后用lrange指令实现列表的高效分页,拿到单页id集合后再去批量查询这些id对应的详细信息,最终聚合成完整信息输出给客户端,这样既节约资源,又便于我们设计和复用缓存结构。(ps:根据lrange的复杂度不难推断出在数据量庞大的情况下会影响其性能,这时可以尝试大key拆分成小key,比如可以利用CRC32(value) % key_num来做数据拆分,当然也可以利用redis3.2推出的quicklist内部编码实现,它对大key的性能一样很高)

集合(set)

跟list一样,一个key对应一堆value,但是set不允许有重复的value,同时它也是无序的,其内部结构和编码如下:

图1-4

其中set-max-intset-entrie可配。

指令集:

指令 解释 时间复杂度
sadd [k] [v1] [v2] … 添加元素 O(k) k=插入元素数
srem [k] [v1] [v2] … 删除元素 O(k) k=删除元素数
scard [k] 获取元素总数 O(1)
sismember [k] [v] v是否在集合k中 O(1)
srandmember [k] [num] 从集合k中随机返回num个元素 O(k) k=num
spop [k] [num] 从集合k中随机弹出num个元素 O(1)
smembers [k] 获取所有元素(结果无序) O(n) n=元素总数
sscan [k] 迭代出所有元素,类似下面scan的用法 同上
sinter [k1] [k2] … 求多个集合的交集 O(m*k) k=多个集合中最少元素数,m=集合数
sinterstore [dk] [k1] [k2] … 将算出的交集存入dk中 同上
suinon [k1] [k2] … 求多个集合的并集 O(k) k=多个集合的总元素数
suinonstore [dk] [k1] [k2] … 将算出的并集存入dk中 同上
sdiff [k1] [k2] … 求多个集合的差集 同上
sdiffstore [dk] [k1] [k2] … 将算出的差集存入dk中 同上

相比list,set并不能用来分页以及展示一个列表,因为它是无序的,但我们仍然可以为它找到合适的应用场景,比如可以用它来存储用户的喜好标签,便于利用取交集差集等功推荐同好;也可以存储某些权益信息,比如某大V粉丝集合,利用sismember指令可以很快得出一个用户是否是另一个用户的粉丝(得出按钮展示“已关注”还是“未关注”),这同样可以计算一个大V与另一个大V的粉丝重合度。

有序集合(zset)

相比set,zset可以进行灵活排序、分页等操作,同样它也不允许有重复元素,其内部结构和编码如下:

图1-5

其中zset-max-ziplist-entrieszset-max-ziplist-value都是redis的配置项。

指令集:

指令 解释 时间复杂度
zadd [k] [s1] [v1] [s2] [v2] … 添加成员 O(k*log(n)) k=插入个数,n=总个数
zscore [k] [v] 获取某成员分数 O(1)
zcard [k] 获取元素总数 O(1)
zcount [k] [min] [max] 获取score值在[min, max]区间的元素总数 O(log(n)) n=总个数
zincrby [k] [num] [v] 给v加num分 同上
zrank [k] [v] 返回v的正序排名 同上
zrevrank [k] [v] 返回v的倒序排名 同上
zrem [k] [v1] [v2] … 删除元素 O(k*log(n)) k=删除个数,n=总个数
zscan [k] 迭代出[k]里所有的元素,类似下面scan的用法 O(N) N=总个数
zrange [k] [start] [end] withsocres 按分数从小到大返回[start, end]区间内的数据(score+v都会返回) O(log(n)+k) k=获取数;n=总个数
zrevrange [k] [start] [end] withscores 按分数从大到小返回[start, end]区间内的数据(score+v都会返回) 同上
zrangebyscore [k] [min] [max] withscores 返回score值在[min, max]区间的数据(按从小到大排序) 同上
zrevrangebyscore [k] [max] [min] withscores 返回score值在[max, min]区间的数据(按从大到小排序) 同上
zremrangebyrank [k] [start] [end] 删除[start, end]名次区间内的数据 O(log(n)+k) k=删除数;n=总个数
zremrangebyscore [k] [min] [max] 删除分数在[min, max]区间内的数据 同上
zinterstore [dk] [num] [k1] [k2] … 将k1、k2等集合的交集存入dk中 O(nk)+O(mlog(m)) n=成员数最小集合的成员数,k=参与取交集的集合数,m=结果集中的成员数
zunionstore [dk] [num] [k1] [k2] … 将k1、k2等集合的并集存入dk中 O(n)+O(m*log(m)) n=所有集合成员数之和,m=结果集中的成员数

作为一个支持正序、倒序、分页的集合,它特别适合做一些排行榜类的需求,这样可以很方便的获取名次、分数等关键信息;

除此之外也可以做一些含排序操作的列表页:比如b站的个人空间中up主的稿件列表,可以按照时间、播放量、收藏量来进行正序和倒序展示,同时还支持分页,那么我们就可以建设3个统计维度的有序集合(分别以投稿时间、播放量、收藏量做score),value保存稿件id,然后按照排序类型、页码查询出单页数据,再利用单页的id集合批量查询稿件详细信息,最终将这些信息聚合成接口返回给客户端即可。

哈希(hash)

hash类型的数据本身就是一个k-v结构,也就是一个redis key对应多个k-v,结构和内部编码如下:

图1-6

其中hash-max-ziplist-entrieshash-max-ziplist-value都是redis的配置项。

指令集:

指令 解释 时间复杂度
hset [k] [KEY] [VALUE] 为[k]添加一对[KEY]-[VALUE] O(1)
hget [k] [KEY] 获取[k]里[KEY]对应的value O(1)
hdel [k] [KEY1] [KEY2] … 删除[k]里的某些[KEY] O(k) k=被删KEY的个数
hlen [k] 获取[k]里[KEY]的数量 O(1)
hgetall [k] 获取[k]里所有的[KEY]-[VALUE] O(n) n=KEY数
hscan [k] 迭代的方式获取[k]里所有的[KEY]-[VALUE],用法跟后面的scan类似 O(n) n=单次迭代结果数
hmget [k] [KEY1] [KEY2] … 批量获取[k]里一些[KEY]的[VALUE] O(k) k=获取的KEY数
hmset [k] [KEY1] [VALUE1] [KEY2] [VALUE2] … 为[k]添加多个[KEY]-[VALUE] O(k) k=插入的KEY数
hexists [k] [KEY] 判断[k]内是否存在[KEY]键 O(1)
hkeys [k] 获取[k]内所有的[KEY] O(n) n=KEY数
hvals [k] 获取[k]内所有的[VALUE] 同上
hsetnx [k] [KEY] [VALUE] 当[VALUE]不存在时才设置成功 O(1)
hincrby [k] [KEY] [num] 对[k]内[KEY]的值自增num O(1)
hincrbyfloat [k] [KEY] [num] 对[k]内[KEY]的浮点值自增num O(1)
hstrlen [k] [KEY] 获取[k]内[KEY]对应[VALUE]的length O(1)

用处主要是用来结构化的存储一些数据,比如可以把一个对象各个属性和属性值当成hash结构缓存入redis。

二、通用指令

下面是一些通用的指令,不同于上面的指令,这些指令不会针对某些数据结构生效,而是全局有效。

指令 解释 时间复杂度
keys [xx]* 获取所有前缀为“xx”的key,若直接为keys *就是获取所有key,重操作 O(n) n=key总数
scan [cursor] [xx]* [count] 非阻塞式获取所有key,相比keysscan是利用[cursor]做游标进行迭代的,[count]则是返回的条数,默认10条,每次迭代结果中会返回下次迭代所需的[cursor],这样就可以一步步迭代出来所有的key了,它不是一个重操作,也就不会长阻塞redis线程 O(n) n=单次迭代结果数
dbsize 获取当前db内的key总数 O(1)
exists [k1] [k2] … 是否存在[k1]、[k2]等键 O(n) n=查看key的个数
del [k1] [k2] … 删除[k1]、[k2]等键 O(n) n=删除key的个数
expire [k] [time] 让[k]time秒后过期 O(1)
persist [k] 取消[k]的过期时间 O(1)
ttl [k] 返回[k]的剩余过期时间,若返回-1,说明没设过期时间,-2表示[k]不存在,>0表示剩余的时间,单位秒 O(1)
type [k] 查看[k]对应值的类型(string、set、zset、list、hash) O(1)
object encoding [k] 查看[k]的内部编码(int、embstr、raw、ziplist、hashtable、linkedlist、skiplist、intset) O(1)
rename [k1] [k2] 将[k1]重命名为[k2] O(1)
renamenx [k1] [k2] 当k2不存在时才能重命名成功 O(1)
randomkey 随机返回一个key O(1)
dump [k] 将[k]的值dump成RDB格式的数据 O(1)+O(M*N) M=k对应的对象数;N=对象平均大小,对于值很小的情况,复杂度趋近O(1)
restore [k] [ttl] [v] 跟dump结合使用,[v]=dump出的数据,适用于数据转移(比如将redis1中的[k]转移到redis2中);[ttl]为过期时间,若为0,则表示没有过期时间(这种迁移方式非原子性) 同上,但有序集合为O(N * M * log(N))
migrate [ip] [port] `key “”[db] [timeout]copyreplacekeys` [k1] [k2]… 数据迁移(这个命令实际上是在源实例中执行一次dump+del,在目标实例中执行一次restore),[ip]+[port]为迁移的目标机器,`key
select [db] 选择redis数据库,db为数据库下标(一般常为0)redis3中已弱化db功能,redis cluster更是不支持多db,默认0号db O(1)