Redis支持的数据类型
Redis(Remote Dictionary Server),远程字典服务,是用C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,提供多种语言的API。
参考文章:
Redis 支持的数据类型
- 5 种基础数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
 - 3 种特殊数据类型:HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)。
 - 此外,还有一些其他的,比如 Bloom filter(布隆过滤器)、Bitfield(位域)。
 
5 种基本数据类型
Redis 共有 5 种基本数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
这 5 种数据类型是直接提供给用户使用的,是数据的保存形式,其底层实现主要依赖这 8 种数据结构:简单动态字符串(SDS)、LinkedList(双向链表)、Dict(哈希表/字典)、SkipList(跳跃表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)。
Redis 5 种基本数据类型对应的底层数据结构实现如下表所示:
| String | List | Hash | Set | Zset | 
|---|---|---|---|---|
| SDS | LinkedList/ZipList/QuickList | Dict、ZipList | Dict、Intset | ZipList、SkipList | 
String(字符串)
String 是 Redis 中最简单同时也是最常用的一个数据类型。它是一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。
应用场景
- 常规数据: 缓存Session、Token、序列化后的对象、图片的路径。相关命令为
SET、GET。 - 计数:用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数。相关命令为
SET、GET、INCR、DECR。 - 分布式锁:利用 
SETNX key value命令可以实现一个最简易的分布式锁(有缺陷,不推荐)。 
Redis String 命令以及详细使用指南,请查看 Redis 官网对应的介绍:https://redis.io/commands/?group=string 。
Redis 构建了一种 简单动态字符串(Simple Dynamic String,SDS)。SDS 相比于 C 语言中的字符串有如下提升:
- 可以避免缓冲区溢出:C 语言中字符串被修改时,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。SDS 被修改时,会先根据 len 属性检查空间大小,不满足要求会先扩展至所需大小再进行修改操作。
 - 获取字符串长度的复杂度较低:C 语言中的字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。SDS 的长度获取直接读取 len 属性即可,时间复杂度为 O(1)。
 - 减少内存分配次数:C 语言的字符串修改字符串时,每次都需要重新分配内存。SDS 实现了空间预分配和惰性空间释放两种优化策略。当 SDS 需要增加字符串时,Redis 会为 SDS 分配好内存,并且根据特定的算法分配多余的内存,这样可以减少连续执行字符串增长操作所需的内存重分配次数。当 SDS 需要减少字符串时,这部分内存不会立即被回收,会被记录下来,等待后续使用(支持手动释放,有对应的 API)。
 - 二进制安全:C 语言中的字符串以空字符 
\0作为字符串结束的标识,而一些二进制文件(比如图片、视频、音频)就可能包括空字符,C 字符串无法正确保存。SDS 使用 len 属性判断字符串是否结束,不存在这个问题。 
List(列表)
Redis 中的 List 是链表数据结构的实现,但 C 语言没有实现链表,所以 Redis 实现了自己的链表数据结构。Redis 的 List 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
Redis List 命令以及详细使用指南,请查看 Redis 官网对应的介绍:https://redis.io/commands/?group=list 。
应用场景
- 信息流展示:最新文章、最新动态。相关命令为
LPUSH、LRANGE。 - 消息队列:可以用来做消息队列,只是功能过于简单且存在很多缺陷,不建议这样做。
 
Hash(哈希)
Redis 中的 Hash 是一个 String 类型的 field-value(键值对) 的映射表,适合用于存储对象,后续操作时可以直接修改这个对象中的某些字段的值。
Hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,Redis 的 Hash 做了更多优化。
Redis Hash 命令以及详细使用指南,请查看 Redis 官网对应的介绍:https://redis.io/commands/?group=hash 。
应用场景
- 对象数据存储:用户信息、商品信息、文章信息、购物车信息。相关命令为
HSET(设置单个字段的值)、HMSET(设置多个字段的值)、HGET(获取单个字段的值)、HMGET(获取多个字段的值)。 
Set(集合)
Redis 中的 Set 类型是一种无序集合,集合中的元素没有先后顺序但都唯一,类似于 Java 的 HashSet 。当要存储列表数据,又不希望出现重复数据时,Set 是一个很好的选择,并且 Set 提供了判断某个元素是否在一个 Set 集合内的重要接口,这个也是 List 所不能提供的。
可基于 Set 轻易实现交集、并集、差集的操作,比如实现如共同关注、共同粉丝、共同喜好等功能就是求交集的过程。
Redis Set 命令以及详细使用指南,请查看 Redis 官网对应的介绍:https://redis.io/commands/?group=set 。
应用场景
- 存放的数据不能重复:网站 UV 统计(数据量巨大的场景还是 
HyperLogLog更适合一些)、文章点赞、动态点赞等场景。相关命令为SCARD(获取集合数量) 。 - 获取多个数据源交集、并集和差集:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等场景。相关命令为
SINTER(交集)、SINTERSTORE(交集)、SUNION(并集)、SUNIONSTORE(并集)、SDIFF(差集)、SDIFFSTORE(差集)。 - 随机获取数据源中的元素:抽奖系统、随机点名等场景。相关命令为
SPOP(随机获取集合中的元素并移除,适合不允许重复中奖的场景)、SRANDMEMBER(随机获取集合中的元素,适合允许重复中奖的场景)。 
Sorted set(有序集合)
Sorted Set 类似于 Set,但Sorted Set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像 Java 中 HashMap 和 TreeSet 的结合体。
Redis Sorted Set 命令以及详细使用指南,请查看 Redis 官网对应的介绍:https://redis.io/commands/?group=sorted-set 。
应用场景
- 随机获取数据源中的元素根据某个权重进行排序:各种排行榜,如直播间送礼物、朋友圈的微信步数、王者荣耀中的段位、话题热度排行榜等。相关命令为
ZRANGE(从小到大排序)、ZREVRANGE(从大到小排序)、ZREVRANK(指定元素排名)。 - 存储的数据有优先级或者重要程度:优先级任务队列。相关命令为
ZRANGE(从小到大排序)、ZREVRANGE(从大到小排序)、ZREVRANK(指定元素排名)。 
String 对比 Hash
- 对象存储方式:String 存储的是序列化后的对象数据,存放的是整个对象,操作简单直接。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。
 - 内存消耗:Hash 通常比 String 更节省内存,特别是在字段较多且字段长度较短时。Redis 对小型 Hash 进行优化(如使用 ziplist 存储),进一步降低内存占用。
 - 复杂对象存储:String 在处理多层嵌套或复杂结构的对象时更方便,因为无需处理每个字段的独立存储和操作。
 - 性能:String 的操作通常具有 O(1) 的时间复杂度,因为它存储的是整个对象,操作简单直接,整体读写的性能较好。Hash 由于需要处理多个字段的增删改查操作,在字段较多且经常变动的情况下,可能会带来额外的性能开销。
 
总结:
- 在绝大多数情况下,String 更适合存储对象数据,尤其是当对象结构简单且整体读写是主要操作时。
 - 如果你需要频繁操作对象的部分字段或节省内存,Hash 可能是更好的选择。
 
3 种特殊数据类型
Redis 支持 3 种特殊的数据类型:Bitmap (位图)、HyperLogLog(基数统计)、Geospatial (地理位置)。
Bitmap (位图)
Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap,只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。
可将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。
应用场景
- 保存状态信息(0/1 即可表示):用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。相关命令为
SETBIT、GETBIT、BITCOUNT、BITOP。 
| 命令 | 介绍 | 
|---|---|
| SETBIT key offset value | 设置指定 offset 位置的值 | 
| GETBIT key offset | 获取指定 offset 位置的值 | 
| BITCOUNT key start end | 获取 start 和 end 之间值为 1 的元素个数 | 
| BITOP operation destkey key1 key2 … | 对一个或多个 Bitmap 进行运算,可用运算符有 AND, OR, XOR 以及 NOT | 
HyperLogLog(基数统计)
HyperLogLog 是一种有名的基数计数概率算法 ,基于 LogLog Counting(LLC)优化改进得来,并不是 Redis 特有的,Redis 只是实现了这个算法并提供了一些开箱即用的 API。Redis 提供的 HyperLogLog 占用空间非常小,只需要 12k 的空间就能存储接近2^64个不同元素。另外,Redis 优化了 HyperLogLog 的存储结构,采用两种方式计数:
- 稀疏矩阵:计数较少的时候,占用空间很小。
 - 稠密矩阵:计数达到某个阈值的时候,占用 12k 的空间。
 
基数计数概率算法为了节省内存并不会直接存储元数据,而是通过一定的概率统计方法预估基数值(集合中包含元素的个数)。因此, HyperLogLog 的计数结果并不是一个精确值,存在一定的误差(标准误差为 0.81% )。
应用场景
- 数量巨大(百万、千万级别以上)的计数:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计。相关命令为
PFADD、PFCOUNT。 
| 命令 | 介绍 | 
|---|---|
| PFADD key element1 element2 … | 添加一个或多个元素到 HyperLogLog 中 | 
| PFCOUNT key1 key2 | 获取一个或者多个 HyperLogLog 的唯一计数。 | 
| PFMERGE destkey sourcekey1 sourcekey2 … | 将多个 HyperLogLog 合并到 destkey 中,destkey 会结合多个源,算出对应的唯一计数。 | 
Geospatial (地理位置)
Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,基于 Sorted Set 实现。通过 GEO 可轻松实现两个位置距离的计算、获取指定位置附近的元素等功能。
应用场景
- 管理使用地理空间数据:附近的人。相关命令为 
GEOADD、GEORADIUS、GEORADIUSBYMEMBER。 
| 命令 | 介绍 | 
|---|---|
| GEOADD key longitude1 latitude1 member1 … | 添加一个或多个元素对应的经纬度信息到 GEO 中 | 
| GEOPOS key member1 member2 … | 返回给定元素的经纬度信息 | 
| GEODIST key member1 member2 M/KM/FT/MI | 返回两个给定元素之间的距离 | 
| GEORADIUS key longitude latitude radius distance | 获取指定位置附近 distance 范围内的其他元素,支持 ASC(由近到远)、DESC(由远到近)、Count(数量) 等参数 | 
| GEORADIUSBYMEMBER key member radius distance | 类似于 GEORADIUS 命令,只是参照的中心点是 GEO 中的元素 | 
Bloom filter(布隆过滤器)
简介
布隆过滤器(Bloom Filter,BF)是 Bloom 于 1970 年提出的。可以看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于 List、Map、Set 等数据结构,它占用空间更少并且效率更高,但其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。
Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1(代表 false 或者 true),这也是 Bloom Filter 节省内存的核心所在。这样来算的话,申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 KB ≈ 122KB 的空间。
总结:一个名叫 Bloom 的人提出了一种来检索元素是否在给定大集合中的数据结构,这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大。
原理
当一个元素加入布隆过滤器中时,会进行如下操作:
- 使用布隆过滤器中的哈希函数(多个)对元素值进行哈希计算,得到哈希值(有几个哈希函数得到几个哈希值)。
 - 根据得到的哈希值,在位数组中把对应下标的值置为 1。
 
当需要判断一个元素是否存在于布隆过滤器时,会进行如下操作:
- 对给定元素再次进行相同的哈希计算;
 - 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
 
注意:不同的字符串可能哈希出来的位置相同,这种情况可以适当增加位数组大小或者调整我们的哈希函数。
综上:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
使用场景
- 判断给定数据是否存在:判断一个数字是否存在于包含大量数字的数字集中(数字集很大,上亿)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤(判断一个邮件地址是否在垃圾邮件列表中)、黑名单功能(判断一个 IP 地址或手机号码是否在黑名单中)等等。
 - 去重:爬给定网址的时候对已经爬取过的 URL 去重、对巨量的 QQ 号/订单号去重。
 
去重场景也需要用到判断给定数据是否存在,因此布隆过滤器主要是为了解决海量数据的存在性问题。
编码实战
java手动实现
步骤如下:
- 一个合适大小的位数组保存数据
 - 几个不同的哈希函数
 - 添加元素到位数组(布隆过滤器)的方法实现
 - 判断给定元素是否存在于位数组(布隆过滤器)的方法实现。
 
代码如下:
1  | import java.util.BitSet;  | 
测试代码如下所示,得出结果为 false false true true
1  | String value1 = "https://javaguide.cn/";  | 
利用 Guava 自带的布隆过滤器
在项目中引入 Guava 的依赖:
1  | <dependency>  | 
实际使用如下:
1  | // 创建布隆过滤器对象  | 
当
mightContain()返回 true 时, 可以99%确定该元素在过滤器中,返回 false 时,可以 100%确定该元素不存在于过滤器中。缺陷:只能单机使用,容量扩展也不容易,而现在互联网一般都是分布式的场景,需要用到 Redis 中的布隆过滤器
Redis 中的布隆过滤器
简介
Redis v4.0 之后有了 Module(模块/插件) 功能,Redis Modules 让 Redis 可以使用外部模块扩展其功能 。布隆过滤器就是其中的 Module。官网推荐了一个 RedisBloom 作为 Redis 布隆过滤器的 Module,地址:https://github.com/RedisBloom/RedisBloom
其他还有:
- redis-lua-scaling-bloom-filter(lua 脚本实现):https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter
 - pyreBloom(Python 中的快速 Redis 布隆过滤器):https://github.com/seomoz/pyreBloom
 
RedisBloom 提供了多种语言的客户端支持,包括:Python、Java、JavaScript 和 PHP。
使用 Docker 安装
直接在 Google 搜索 docker redis bloomfilter,具体地址:https://hub.docker.com/r/redislabs/rebloom/(介绍的很详细 )。
具体操作如下:
1  | docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest  | 
注意:当前 rebloom 镜像已经被废弃,官方推荐使用redis-stack
常用命令
注意:key : 布隆过滤器的名称,item : 添加的元素。
BF.ADD:将元素添加到布隆过滤器中,如果该过滤器尚不存在,则创建该过滤器。格式:BF.ADD {key} {item}。BF.MADD: 将一个或多个元素添加到“布隆过滤器”中,并创建一个尚不存在的过滤器。该命令的操作方式BF.ADD与之相同,只不过它允许多个输入并返回多个值。格式:BF.MADD {key} {item} [item ...]。BF.EXISTS: 确定元素是否在布隆过滤器中存在。格式:BF.EXISTS {key} {item}。BF.MEXISTS:确定一个或者多个元素是否在布隆过滤器中存在。格式:BF.MEXISTS {key} {item} [item ...]。-  
BF.RESERVE: 自定义参数的布隆过滤器。格式:BF.RESERVE {key} {error_rate} {capacity} [EXPANSION expansion]。 - key:布隆过滤器的名称
 - error_rate : 期望的误报率。该值必须介于 0 到 1 之间。例如,对于期望的误报率 0.1%(1000 中为 1),error_rate 应该设置为 0.001。该数字越接近零,则每个项目的内存消耗越大,并且每个操作的 CPU 使用率越高。
 - capacity: 过滤器的容量。当实际存储的元素个数超过这个值之后,性能将开始下降。实际的降级将取决于超出限制的程度。随着过滤器元素数量呈指数增长,性能将线性下降。
 - expansion:可选参数。如果创建了一个新的子过滤器,则其大小将是当前过滤器的大小乘以
expansion。默认扩展值为 2。这意味着每个后续子过滤器将是前一个子过滤器的两倍。 
实际使用
1  | 127.0.0.1:6379> BF.ADD myFilter java  | 
Bitfield(位域)
Bitfields(位域)与Bitmap一样,在Redis中不是一种独立的数据类型,而是一种基于字符串的数据结构,用于处理位级别的操作。允许用户将一个Redis字符串视作由一系列二进制位组成的数组,并对这些位进行高效的访问和操作。通过Bitfield,开发者可以将多个小的整数存储到一个较大的位图中,或者将一个庞大的键分割为多个较小的键进行存储,从而极大地提高了内存的使用效率。
基本概念
位域(Bitfield):在Redis中,位域是一种特殊的数据结构,用于存储和操作二进制位数据。
字符串与位域:虽然Redis字符串是最基本的数据类型之一,支持丰富的操作,但在处理位级数据时,字符串的效率较低。相比之下,位域通过压缩存储多个小的整数或状态信息,极大地提高了内存的使用效率
主要操作
bitfield是redis的一个命令,语法如下:
1  | BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]  | 
获取操作
语法:BITFIELD key [GET type offset]
功能:获取指定键的位域值。
参数:
key:要操作的Redis键。GET:表示要从字符串值中读取位。type:指定读取数据的类型(u表示无符号整数,i表示有符号整数)。offset:位字段的起始偏移位置,从0开始计数。
设置操作
语法:BITFIELD key [SET type offset value]
功能:设置指定位域的值并返回其原值。
参数:
SET:表示要设置字符串值中的位。type:指定读取数据的类型(u表示无符号整数,i表示有符号整数)。offset:位字段的起始偏移位置,从0开始计数。value:是要设置的值。
自增操作
语法:BITFIELD key [INCRBY type offset increment]
功能:对指定位域的值进行自增操作。
参数:
key:要操作的Redis键。INCRBY:表示自增。type:指定读取数据的类型(u表示无符号整数,i表示有符号整数)。offset:位字段的起始偏移位置,从0开始计数。increment:是自增的数值。
溢出控制
Bitfield提供了三种溢出控制方式:
WRAP:使用回绕方法处理有符号整数和无符号整数的溢出情况。SAT:使用饱和计算处理溢出,超过最大值再增加则数值不变。下溢计算的结果为最小整数值,上溢计算的结果为最大的整数值。FAIL:命令将拒绝执行那些会导致上溢或者下溢情况出现的计算,并向用户返回空值表示计算未被执行。
应用场景
集合运算:使用位掩码来表示集合中的元素,可以高效地进行集合运算,如并集、交集和差集。如使用一个整数的每一位来表示一个用户的兴趣标签,然后通过位运算来快速查询某个用户感兴趣的所有内容。
计数器:BitField可以用来实现高效的计数器,尤其是在需要对大量离散事件进行计数时。如使用一个BitField来记录某个在线游戏中的玩家死亡次数,或者记录某个网站的访问次数。
状态压缩:对于需要存储大量状态信息的情况,使用BitField可以将状态信息压缩到更少的存储空间中。如使用一个BitField来表示一个游戏中的多个游戏角色的状态,如是否在线、是否死亡等。
权限控制:BitField可以用于实现复杂的权限控制系统,通过位运算来快速检查用户是否具有某个权限。如使用一个BitField来表示用户的权限集合,然后通过位运算来检查用户是否具有访问某个资源的权限。
空间优化:对于需要存储大量二进制数据的情况,使用BitField可以节省存储空间。如使用一个BitField来表示一张图像的颜色信息,而不是使用完整的字节或整数来存储每个像素的颜色值。
网络协议优化:在网络通信中,使用BitField可以有效地压缩和传输数据。如使用一个BitField来表示一个TCP数据包中的标志位,从而减少数据包的大小和提高传输效率。
注意:虽然BitField提供了高效的存储和操作位级数据的能力,但它也有一些限制和缺点,如不支持事务、不支持范围查询等。因此,在使用BitField时,需要根据具体的应用场景和需求来权衡其优缺点。
在Java中的使用
1  | import org.redisson.Redisson;  | 



