Redis-数据类型

本文最后更新于:1 年前

[TOC]

redis 总是键值对存储。

key 总是 string。

value 有五种类型。

string

!!概述

为什么要自己设计 SDS

支持任意字符

高效

string 数据结构是简单的 key-value 类型。虽然 Redis 是用 C 语言写的,但是 Redis 并没有使用 C 的字符串表示,而是自己构建了一种 简单动态字符串(simple dynamic string,SDS)。相比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据还可以保存二进制数据,并且**获取字符串长度复杂度为 O(1)**(C 字符串为 O(N)),除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。

image-20210804150655994

常用命令: set,get,strlen,exists,decr,incr,setex 等等。

应用场景: 一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。

127.0.0.1:6379> set key value #设置 key-value 类型的值
127.0.0.1:6379> get key # 根据 key 获得对应的 value
127.0.0.1:6379> exists key # 判断某个 key 是否存在
127.0.0.1:6379> strlen key # 返回 key 所储存的字符串值的长度。
127.0.0.1:6379> del key # 删除某个 key 对应的值
127.0.0.1:6379> get key

批量设置 :
127.0.0.1:6379> mset key1 value1 key2 value2 # 批量设置 key-value 类型的值
127.0.0.1:6379> mget key1 key2 # 批量获取多个 key 对应的 value

计数器(字符串的内容为整数的时候可以使用):
127.0.0.1:6379> set number 1
127.0.0.1:6379> incr number # 将 key 中储存的数字值增一
127.0.0.1:6379> get number
127.0.0.1:6379> decr number # 将 key 中储存的数字值减一
127.0.0.1:6379> get number

过期(默认为永不过期):
127.0.0.1:6379> expire key 60 # 数据在 60s 后过期
127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
127.0.0.1:6379> ttl key # 查看数据还有多久过期

list

list 即是 链表。链表是一种非常常见的数据结构,特点是易于数据元素的插入和删除并且可以灵活调整链表长度,但是链表的随机访问困难。Redis 的 list 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。

常用命令**: rpush,lpop,lpush,rpop,lrange,llen** 等。

应用场景: 发布与订阅或者说消息队列、慢查询。

通过 rpush/lpop 实现队列:
127.0.0.1:6379> rpush myList value1 # 向 list 的头部(右边)添加元素
127.0.0.1:6379> rpush myList value2 value3 # 向 list 的头部(最右边)添加多个元素
127.0.0.1:6379> lpop myList # 将 list 的尾部(最左边)元素取出
127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的 list 列表, 0 为 start,1 为 end
127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的所有元素,-1 表示倒数第一

通过 rpush/rpop 实现栈:
127.0.0.1:6379> rpush myList2 value1 value2 value3
127.0.0.1:6379> rpop myList2 # 将 list 的头部(最右边)元素取出

通过 lrange 查看对应下标范围的列表元素:
127.0.0.1:6379> rpush myList value1 value2 value3
127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的 list 列表, 0 为 start,1 为 end
127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的所有元素,-1 表示倒数第一
通过 lrange 命令,你可以基于 list 实现分页查询,性能非常高!

通过 llen 查看链表长度:
127.0.0.1:6379> llen myList

hash

hash 类似于 JDK1.8 前的 HashMap,**内部实现也差不多(数组 + 链表)**。不过,Redis 的 hash 做了更多优化。另外,hash 是一个 string 类型的 field 和 value 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。

渐进式 rehash()

而随着Redis的hash表越来越大,rehash的成本也会越来越高。Redis中实现了一种渐进式rehash的方案,他可以在哈希表rehash操作时,分多个步骤逐渐完成的方式,这样不会因为要一次性把所有元素都完成迁移而导致IO升高,线程阻塞。这个特性使得Redis可以在继续提供读写服务的同时,逐步迁移数据到新的哈希表,而不会对性能造成明显的影响。

hash 对象在扩容时使用了一种叫“渐进式 rehash”的方式,步骤如下:

1)计算新表 size(*2 且为 2 的幂次)、掩码,为新表 ht[1] 分配空间,让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
2)将 rehash 索引计数器变量 rehashidx 的值设置为 0,表示 rehash 正式开始。
3)在 rehash 进行期间,每次对字典执行添加、删除、査找、更新操作时,程序除了执行指定的操作以外,还会触发额外的 rehash 操作,在源码中的 _dictRehashStep 方法。

_dictRehashStep:从名字也可以看出来,大意是 rehash 一步,也就是 rehash 一个索引位置。

该方法会从 ht[0] 表的 rehashidx 索引位置上开始向后查找,找到第一个不为空的索引位置,将该索引位置的所有节点 rehash 到 ht[1],当本次 rehash 工作完成之后,将 ht[0] 索引位置为 rehashidx 的节点清空,同时将 rehashidx 属性的值加一。

4)将 rehash 分摊到每个操作上确实是非常妙的方式,但是万一此时服务器比较空闲,一直没有什么操作,难道 redis 要一直持有两个哈希表吗?
答案当然不是的。我们知道,redis 除了文件事件外,还有时间事件,redis 会定期触发时间事件,这些时间事件用于执行一些后台操作,其中就包含 rehash 操作:当 redis 发现有字典正在进行 rehash 操作时,会花费 1 毫秒的时间,一起帮忙进行 rehash。

5)随着操作的不断执行,最终在某个时间点上,ht[0] 的所有键值对都会被 rehash 至 ht[1],此时 rehash 流程完成,会执行最后的清理工作:释放 ht[0] 的空间、将 ht[0] 指向 ht[1]、重置 ht[1]、重置 rehashidx

渐进式 rehash 的优点

渐进式 rehash 的好处在于它采取分而治之的方式,将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式 rehash 而带来的庞大计算量

在进行渐进式 rehash 的过程中,字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间,字典的删除、査找、更新等操作会在两个哈希表上进行。例如,要在字典里面査找一个键的话,程序会先在 ht[0] 里面进行査找,如果没找到的话,就会继续到 ht[1] 里面进行査找,诸如此类。

另外,在渐进式 rehash 执行期间,新增的键值对会被直接保存到 ht[1], ht[0] 不再进行任何添加操作,这样就保证了 ht[0] 包含的键值对数量会只减不增,并随着 rehash 操作的执行而最终变成空表。

常用命令: hset,hmset,hexists,hget,hgetall,hkeys,hvals 等

应用场景: 系统中对象数据的存储。 hash 数据结构来存储用户信息,商品信息等等。

set

set 类似于 Java 中的 HashSet 。Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。

可以基于 set 轻易实现交集、并集、差集的操作。

常用命令: sadd,spop,smembers,sismember,scard,sinterstore,sunion 等。

应用场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景。比如:你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。
127.0.0.1:6379> smembers mySet # 查看 set 中所有的元素
127.0.0.1:6379> scard mySet # 查看 set 的长度
127.0.0.1:6379> sismember mySet value1 # 检查某个元素是否存在 set 中,只能接收单个元素
127.0.0.1:6379> sadd mySet2 value2 value3
127.0.0.1:6379> sinterstore mySet3 mySet mySet2 # 获取 mySet 和 mySet2 的交集并存放在 mySet3 中

sorted set

概述

数据结构转换

总的来说就是,当元素数量少于128,每个元素的长度都小于64字节的时候,使用ZipList(ListPack),否则,使用SkipList

跳表

多级索引

和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap 和 TreeSet 的结合体。

常用命令: zadd,zcard,zscore,zrange,zrevrange,zrem 等。

应用场景: 需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息

Sorted Set 为什么同时使用字典和跳跃表?

Sorted Set 能支持范围查询,这是因为它的核心数据结构设计采用了跳表,而它又能O(1)的复杂度获取元素权重,这是因为它同时采用了哈希表进行索引。

zset的数据结构,其中包含了两个成员,分别是哈希表dict和跳表zsl。

dict存储 member->score 之间的映射关系,所以 ZSCORE 的时间复杂度为 O(1)。skiplist 是一个「有序链表 + 多层索引」的结构,查询元素的复杂度是 O(logN),所以他的查询效率很高。

主要是为了提升性能。

单独使用字典:在执行范围型操作,比如 zrank、zrange,字典需要进行排序,至少需要 O(NlogN) 的时间复杂度及额外 O(N) 的内存空间。
单独使用跳跃表:根据成员查找分值操作的复杂度从 O(1) 上升为 O(logN)

Sorted Set 为什么使用跳跃表,而不是红黑树?

主要有以下几个原因:
1)跳表的性能和红黑树差不多。
2)跳表更容易实现和调试

实现滑动窗口限流

我们就可以把login接口这个需要做限流的资源名作为key在redis中进行存储,然后value我们现在ZSET这种数据结构,把他的score设置为当前请求的时间戳,member的话建议用请求的详情的hash进行存储(或者UUID、MD5什么的),避免在并发时,时间戳一致出现scode和memberv一样导致被zadd幂等的问题。

image-20240325203227879

只保留在特定时间窗口内的请求记录,而丢弃窗口之外的记录。

主要步骤如下:

  1. 定义滑动窗口的时间范围,例如,窗口大小为60秒。
  2. 每次收到一个请求时,我们就定义出一个zset然后存储到redis中。
  3. 然后再通过ZREMRANGEBYSCORE命令来删除分值小于窗口起始时间戳(当前时间戳-60s)的数据。
  4. 最后,再使用ZCARD命令来获取有序集合中的成员数量,即在窗口内的请求量。

bitmap

bitmap 存储的是连续的二进制数字(0 和 1),通过 bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 bitmap 本身会极大的节省储存空间。

常用命令: setbit 、getbit 、bitcount、bitop

应用场景: 适合需要保存状态信息(比如是否签到、是否登录…)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)

redis 还能用来做什么

Redis最主要的功能就是拿来做缓存,来提升系统的性能,但是除了做缓存以外,他还能做很多事(但是,能做并不代表就适合,并不代表就一定要用它):

消息队列:Redis 支持发布/订阅模式和Stream,可以作为轻量级消息队列使用,用于异步处理任务或处理高并发请求。

排行榜:利用Redis 的有序集合和列表结构,可以成为设计实时排行榜的绝佳选择,例如各类热门排行榜、热门商品列表等。

分布式锁:Redis 的单线程特性可以保证多个客户端之间对同一把锁的操作是原子性的,可以轻松实现分布式锁,用于控制多个进程对共享资源的访问。

地理位置应用:Redis 支持GEO,支持地理位置定位和查询,可以存储地理位置信息并通过 Redis 的查询功能获取附近的位置信息。

分布式限流:Redis提供了令牌桶和漏桶算法的实现,可以用于实现分布式限流。

分布式Session:可以使用Redis实现分布式Session管理,保证多台服务器之间用户的会话状态同步。

布隆过滤器:Redis提供了布隆过滤器(Bloom Filter)数据结构的实现,可以高效地检测一个元素是否存在于一个集合中

redis key 设计原则

key
可读性:一个Key应该具有比较好的可读性,让人能看得懂是什么意思,而不是含糊不清。key 名称以 key 所代表的 value 类型结尾,以提高可读性。例如:user:basic.info:userid:string。

简洁性:Key 应该保持简洁,避免过长的命名,以节省内存和提高性能。一个好的做法是使用短、有意义的 Key,但也不要过于简单以避免与其他 Key 冲突。

避免特殊字符:避免在 Key 中使用特殊字符,以确保 Key 的可读性和可操作性。命名中尽量只包含:大小写字母、数字、竖线、下划线、英文点号(.)和英文半角冒号(:)。

命名空间:使用命名空间来区分不同部分的 Key。例如,可以为用户数据使用 “user:” 前缀,为缓存数据使用 “cache:” 前缀。

长度限制:避免在 Key 的长度过长,会占用内存空间。

value
数据类型选择:根据数据的特性选择合适的数据格式。Redis 支持字符串、列表、哈希、集合和有序集合等多种数据类型,选择合适的数据格式可以提高操作效率。

避免大Key:如果Value很大,那么对应的Key就称之为大Key,大Key会带来很多问题应该尽量避免。可以尝试将大数据分割为多个小 Value,以提高性能和降低内存使用。

过期时间:为 Value 设置适当的过期时间以自动清理不再需要的数据,以减少内存占用。

压缩:如果数据具有可压缩性,可以在存储之前进行压缩,以减少内存使用。

合理控制和使用数据结构内存编码优化配置:例如 ziplist 是一种特殊的数据结构,它可以将小型列表、哈希表和有序集合存储在一个连续的内存块中,从而节省了内存空间。但由于 ziplist 没有索引,因此在对 ziplist 进行查找、插入或删除操作时,需要进行线性扫描,这可能会导致性能下降。在实际应用中,应该根据具体情况来决定是否使用 ziplist。如果数据量较小且需要频繁进行遍历操作,那么使用 ziplist 可能是一个不错的选择。但是,如果数据量较大且需要频繁进行插入、删除或查找操作,那么使用 ziplist 可能会影响性能,应该考虑使用其他数据结构来代替。

Lua 和 redis 事务

原子性

Redis的事务在执行过程中,如果有某一个命令失败了,是不影响后续命令的执行的,而Lua脚本中,如果执行过程中某个命令执行失败了,是会影响后续命令执行的。

交互

在Redis的事务执行时,每一条命令都需要和Redis服务器进行一次交互,我们可以在Redis事务过程中,MULTI 和 EXEC 之间发送多个 Redis 命令给到Redis服务器,这些命令会被服务器缓存起来,但并不会立即执行。但是每一条命令的提交都需要进行一次网络交互。

而Lua脚本则不需要,只需要一次性的把整个脚本提交给Redis即可。网络交互比事务要少。

前后依赖

在 Redis 的事务中,事务内的命令都是独立执行的,并且在没有执行EXEC命令之前,命令是没有被真正执行的,所以后续命令是不会也不能依赖于前一个命令的结果的。

而在Lua 脚本中是可以依赖前一个命令的结果的,Lua 脚本中的多个命令是依次执行的,我们可以利用前一个命令的结果进行后续的处理。

流程编排

借助Lua脚本,我们可以实现非常丰富的各种分支流程控制,以及各种运算相关操作。而Redis的事务本身是不支持这些操作的。

不支持回滚主要的原因是支持回滚将对 Redis 的简洁性和性能产生重大影响。

乐观锁

所谓乐观锁,其实就是基于CAS的机制,CAS的本质是Compare And Swap,就是需要知道一个key在修改前的值,去进行比较。

在Redis中,想要实现这个功能,我们可以依赖 WATCH 命令。这个命令一旦运行,他会确保只有在 WATCH 监视的键在调用 EXEC 之前没有改变时,后续的事务才会执行。

实现 redis 锁

一个分布式锁有很多基本要求,比如说锁的互斥性、可重入性、锁的性能等问题。

对于锁的互斥性,可以借助setnx来保证,因为这个操作本身就是一个原子性操作,并且结合Redis的单线程的机制,就可以保证互斥性。

可重入:value 是代表客户端的唯一值,例如 uuid,logID

有效时间:超时时间 10s

wait time:每次 sleep 200 ms,自旋 1s