Redis 的数据类型

数据类型

Redis 支持常见的数据类型,有五种:String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合)。

随着 Redis 版本的更新,后面又支持了四种数据类型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。

image

String

String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M。

内部实现

String 类型的底层的数据结构实现主要是: int 和 SDS(简单动态字符串)。

SDS 和我们认识的 C 字符串不太一样,之所以没有使用 C 语言的字符串表示,因为 SDS 相比于 C 的原生字符串:

  • SDS 不仅可以保存文本数据,还可以保存二进制数据

    因为 SDS 使用 len 属性的值而不是空字符来判断字符串是否结束,并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据。所以,SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。

  • SDS 获取字符串长度的时间复杂度是 O(1)

    C 语言的字符串并不记录自身长度,所以,获取长度的复杂度为 O(n),而 SDS 结构里用 len 属性记录了字符串长度,所以复杂度为 O(1)。

  • Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出

    因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。

字符串对象的内部编码(encoding)有 3 种 :intrawembstr

image

  • 如果一个字符串对象保存的是整数值,并且这个整数值可以用 long 类型来表示,那么字符串对象会将整数值保存在字符串对象结构的 ptr 属性里面(将 void* 转换成 long),并将字符串对象的编码设置为 int

    image

  • 如果字符串对象保存的是一个字符串,并且这个字符申的长度小于等于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为 embstrembstr 编码是专门用于保存短字符串的一种优化编码方式:

    image

  • 如果字符串对象保存的是一个字符串,并且这个字符串的长度大于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为 raw

    image

注意,embstr 编码和 raw 编码的边界在 redis 不同版本中是不一样的:

  • redis 2.+ 是 32 字节

  • redis 3.0-4.0 是 39 字节

  • redis 5.0 是 44 字节

可以看到 embstr 和 raw 编码都会使用 SDS 来保存值,但不同之处在于 embstr 会通过一次内存分配函数来分配一块连续的内存空间来保存 redisObject 和 SDS,而 raw 编码会通过调用两次内存分配函数来分别分配两块空间来保存 redisObject 和 SDS。

这样做的好处在于:

  • embstr 编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次;

  • 释放 embstr 编码的字符串对象同样只需要调用一次内存释放函数;

  • 因为 embstr 编码的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用 CPU 缓存提升性能。

但 embstr 也有缺点,即如果字符串的长度增加需要重新分配内存时,整个 redisObject 和 sds 都需要重新分配空间,所以 embstr 编码的字符串对象实际上是只读的,redis 没有为 embstr 编码的字符串对象编写任何相应的修改程序。当我们对 embstr 编码的字符串对象执行任何修改命令(例如 append )时,程序会先将对象的编码从 embstr 转换成 raw,然后再执行修改命令。

常用指令

普通字符串的基本操作:

# 设置 key-value 类型的值
> SET name lin
OK
# 根据 key 获得对应的 value
> GET name
"lin"
# 判断某个 key 是否存在
> EXISTS name
(integer) 1
# 返回 key 所储存的字符串值的长度
> STRLEN name
(integer) 3
# 删除某个 key 对应的值
> DEL name
(integer) 1

批量设置 :

# 批量设置 key-value 类型的值
> MSET key1 value1 key2 value2 
OK
# 批量获取多个 key 对应的 value
> MGET key1 key2 
1) "value1"
2) "value2"

计数器(字符串的内容为整数的时候可以使用):

# 设置 key-value 类型的值
> SET number 0
OK
# 将 key 中储存的数字值增一
> INCR number
(integer) 1
# 将key中存储的数字值加 10
> INCRBY number 10
(integer) 11
# 将 key 中储存的数字值减一
> DECR number
(integer) 10
# 将key中存储的数字值键 10
> DECRBY number 10
(integer) 0

过期(默认为永不过期):

# 设置 key 在 60 秒后过期(该方法是针对已经存在的key设置过期时间)
> EXPIRE name  60 
(integer) 1
# 查看数据还有多久过期
> TTL name
(integer) 51

#设置 key-value 类型的值,并设置该key的过期时间为 60 秒
> SET key  value EX 60
OK
> SETEX key  60 value
OK

不存在就插入:

>SETNX key value
(integer) 1

应用场景

缓存

使用 String 来缓存对象有两种方式:

  • 直接缓存整个对象的 JSON

    例如,SET user:1 '{"name":"xiaolin", "age":18}'

  • 采用将 key 进行分离为 user:ID:属性,采用 MSET 存储,用 MGET 获取各属性值

    例如,MSET user:1:name xiaolin user:1:age 18 user:2:name xiaomei user:2:age 20

计数

因为 Redis 处理命令是单线程,所以执行命令的过程是原子的。因此 String 数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等等。

例如,计算文章的阅读量:

# 初始化文章的阅读量
> SET aritcle:readcount:1001 0
OK
# 阅读量+1
> INCR aritcle:readcount:1001
(integer) 1
# 获取对应文章的阅读量
> GET aritcle:readcount:1001
"1"

分布式锁

加锁

SET 命令有个 NX 参数可以实现「key不存在才插入」,可以用它来实现分布式锁:

  • 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;

  • 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。

一般而言,还会对分布式锁加上过期时间,分布式锁的命令如下:

SET lock_key 1740556422388674626 NX PX 10000

其中,

  • lock_key:就是 key 键;
  • 1740556422388674626:是客户端生成的唯一的标识;
  • NX:代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
  • PX:10000 表示设置 lock_key 的过期时间为 10s,避免客户端发生异常而无法释放锁。
释放锁

而解锁的过程就是将 lock_key 键删除,但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的值是否为加锁的客户端,是的话,才将 lock_key 键删除。

可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

共享 Session 信息

在分布式场景下,在多个服务之间共享用户 Session 信息:

image

List

List 列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。

列表的最大长度为 \(2^{32} - 1\),即每个列表支持超过 40 亿个元素。

内部实现

在 Redis 3.2 版本之前,List 类型的底层数据结构是由 双向链表(linkedlist)压缩列表(ziplist) 实现的:

  • 如果列表的元素个数小于 512 个,列表每个元素的值都小于 64 字节,Redis 会使用压缩列表作为 List 类型的底层数据结构;

    相关配置(默认值):

    list-max-ziplist-entries 512
    list-max-ziplist-value 64
    
  • 如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;

但是,在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 快速列表(quicklist) 实现了,替代了双向链表和压缩列表。

常用命令

image

# 将一个或多个值value插入到key列表的表头(最左边),最后的值在最前面
LPUSH key value [value ...] 
# 将一个或多个值value插入到key列表的表尾(最右边)
RPUSH key value [value ...]
# 移除并返回key列表的头元素
LPOP key
# 移除并返回key列表的尾元素
RPOP key 

# 返回列表key中指定区间内的元素,区间以偏移量start和stop指定,从0开始
LRANGE key start stop

# 从key列表表头弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
BLPOP key [key ...] timeout
# 从key列表表尾弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
BRPOP key [key ...] timeout

应用场景

略。

Hash

Hash 是一个键值对(key - value)集合,其中 value 的形式如: value=[{field1,value1},...{fieldN,valueN}]。Hash 特别适合用于存储对象。

Hash 与 String 对象的区别如下图所示:

image

内部实现

在 Redis 7.0 前,Hash 类型的底层数据结构是由 压缩列表哈希表 实现的:

  • 如果哈希类型元素个数小于 512 个,所有值小于 64 字节的话,Redis 会使用 压缩列表 作为 Hash 类型的底层数据结构;

    相关配置(默认值):

    hash-max-ziplist-entries 512
    hash-max-ziplist-value 64
    
  • 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构。

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 紧凑列表(listpack) 数据结构来实现的。

紧凑列表的特点就是用一块连续的内存空间来紧凑地保存数据,同时为了节省内存空间,紧凑列表使用了多种编码方式,来表示不同长度的数据,这些数据包括整数和字符串。

常用命令

# 存储一个哈希表key的键值
HSET key field value   
# 获取哈希表key对应的field键值
HGET key field
# 在一个哈希表key中存储多个键值对
HMSET key field value [field value...] 
# 批量获取哈希表key中多个field键值
HMGET key field [field ...]
# 删除哈希表key中的field键值
HDEL key field [field ...]
# 返回哈希表key中field的数量
HLEN key
# 返回哈希表key中所有的键值
HGETALL key
# 为哈希表key中field键的值加上增量n
HINCRBY key field n

应用场景

缓存对象

Hash 类型的 (key,field, value) 的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。

我们可以使用如下命令,将用户对象的信息存储到 Hash 类型:

# 存储一个哈希表uid:1的键值
> HMSET uid:1 name Tom age 15
2
# 存储一个哈希表uid:2的键值
> HMSET uid:2 name Jerry age 13
2
# 获取哈希表用户id为1中所有的键值
> HGETALL uid:1
1) "name"
2) "Tom"
3) "age"
4) "15"
购物车

以用户 id 为 key,商品 id 为 field,商品数量为 value,恰好构成了购物车的3个要素

涉及的命令如下:

  • 添加商品:HSET cart:${用户id} ${商品id} 1

  • 添加数量:HINCRBY cart:${用户id} ${商品id} 1

  • 商品总数:HLEN cart:${用户id}

  • 删除商品:HDEL cart:${用户id} ${商品id}

  • 获取购物车所有商品:HGETALL cart:${用户id}

当前仅仅是将商品 ID 存储到了 Redis 中,在回显商品具体信息的时候,还需要拿着商品 id 查询一次数据库,获取完整的商品的信息。

Set

介绍

Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。

一个集合最多可以存储 2^32-1 个元素。概念和数学中个的集合基本类似,可以交集,并集,差集等等,所以 Set 类型除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。

内部实现

Set 类型的底层数据结构是由 哈希表整数集合 实现的:

  • 如果集合中的元素都是整数且元素个数小于 512个,Redis 会使用 整数集合 作为 Set 类型的底层数据结构;

    相关配置(默认值):

    set-maxintset-entries 512
    
  • 如果集合中的元素不满足上面条件,则 Redis 使用 哈希表 作为 Set 类型的底层数据结构。

常用命令

Set 常用操作:

# 往集合key中存入元素,元素存在则忽略,若key不存在则新建
SADD key member [member ...]
# 从集合key中删除元素
SREM key member [member ...] 
# 获取集合key中所有元素
SMEMBERS key
# 获取集合key中的元素个数
SCARD key
# 判断member元素是否存在于集合key中
SISMEMBER key member
# 从集合key中随机选出count个元素,元素不从key中删除
SRANDMEMBER key [count]
# 从集合key中随机选出count个元素,元素从key中删除
SPOP key [count]

Set 运算操作:

# 交集运算
SINTER key [key ...]
# 将交集结果存入新集合destination中
SINTERSTORE destination key [key ...]
# 并集运算
SUNION key [key ...]
# 将并集结果存入新集合destination中
SUNIONSTORE destination key [key ...]
# 差集运算
SDIFF key [key ...]
# 将差集结果存入新集合destination中
SDIFFSTORE destination key [key ...]

应用场景

Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。

点赞

Set 类型可以保证一个用户只能点一个赞,这里举例子一个场景,key 是文章id,value 是用户id。

uid:1 、uid:2、uid:3 三个用户分别对 article:1 文章点赞了。

# uid:1 用户对文章 article:1 点赞
> SADD article:1 uid:1
(integer) 1
# uid:2 用户对文章 article:1 点赞
> SADD article:1 uid:2
(integer) 1
# uid:3 用户对文章 article:1 点赞
> SADD article:1 uid:3
(integer) 1

取消点赞:

> SREM article:1 uid:1
(integer) 1

获取文章所有点赞用户:

> SMEMBERS article:1
1) "uid:3"
2) "uid:2"

获取文章的点赞用户数量:

> SCARD article:1
(integer) 2

判断用户是否对文章点赞了:

> SISMEMBER article:1 uid:1
(integer) 0  # 返回 0 说明没点赞,返回 1 则说明点赞了

共同关注

Set 类型支持交集运算,所以可以用来计算共同关注的好友、公众号等。

key 可以是用户 id,value 则是已关注的公众号的 id。

id:1 用户关注公众号 id 为 5、6、7、8、9,uid:2 用户关注公众号 id 为 7、8、9、10、11。

# uid:1 用户关注公众号 id 为 5、6、7、8、9
> SADD uid:1 5 6 7 8 9
(integer) 5
# uid:2  用户关注公众号 id 为 7、8、9、10、11
> SADD uid:2 7 8 9 10 11
(integer) 5

uid:1 和 uid:2 共同关注的公众号:

# 获取共同关注
> SINTER uid:1 uid:2
1) "7"
2) "8"
3) "9"

给 uid:2 推荐 uid:1 关注的公众号:

> SDIFF uid:1 uid:2
1) "5"
2) "6"

验证某个公众号是否同时被 uid:1 或 uid:2 关注:

> SISMEMBER uid:1 5
(integer) 1 # 返回 0,说明关注了
> SISMEMBER uid:2 5
(integer) 0 # 返回 0,说明没关注

抽奖活动

存储某活动中中奖的用户名 ,Set 类型因为有去重功能,可以保证同一个用户不会中奖两次。

key 为抽奖活动名,value 为员工名称,把所有员工名称放入抽奖箱 :

>SADD lucky Tom Jerry John Sean Marry Lindy Sary Mark
(integer) 5

如果允许重复中奖,可以使用 SRANDMEMBER 命令:

# 抽取 1 个一等奖:
> SRANDMEMBER lucky 1
1) "Tom"
# 抽取 2 个二等奖:
> SRANDMEMBER lucky 2
1) "Mark"
2) "Jerry"
# 抽取 3 个三等奖:
> SRANDMEMBER lucky 3
1) "Sary"
2) "Tom"
3) "Jerry"

如果不允许重复中奖,可以使用 SPOP 命令:

# 抽取一等奖1个
> SPOP lucky 1
1) "Sary"
# 抽取二等奖2个
> SPOP lucky 2
1) "Jerry"
2) "Mark"
# 抽取三等奖3个
> SPOP lucky 3
1) "John"
2) "Sean"
3) "Lindy"

Zset

介绍

Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成:元素值 和 排序值。

有序集合保留了集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序

image

内部实现

在 Redis 7.0 前,Zset 类型的底层数据结构是由 压缩列表跳表 实现的:

  • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用 压缩列表 作为 Zset 类型的底层数据结构;

  • 如果有序集合的元素不满足上面的条件,Redis 会使用 跳表 作为 Zset 类型的底层数据结构;

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

常用命令

Zset 常用操作:

  • 向有序集合 key 添加带分值元素:

    ZADD key score member [[score member]...]
    
  • 从有序集合 key 中删除元素:

    ZREM key member [member...]
    
  • 返回有序集合 key 中元素 member 的分值

    ZSCORE key member
    
  • 返回有序集合 key 中元素个数

    ZCARD key
    
  • 为有序集合 key 中元素 member 的分值加上 increment

    ZINCRBY key increment member
    
  • 正序获取有序集合key从start下标到stop下标的元素

    ZRANGE key start stop [WITHSCORES]
    
  • 倒序获取有序集合key从start下标到stop下标的元素

    ZREVRANGE key start stop [WITHSCORES]
    
  • 返回有序集合中指定分数区间内的成员,分数由低到高排序

    ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
    
  • 返回指定成员区间内的成员,按字典正序排列, 分数必须相同

    ZRANGEBYLEX key min max [LIMIT offset count]
    
  • 返回指定成员区间内的成员,按字典倒序排列, 分数必须相同

    ZREVRANGEBYLEX key max min [LIMIT offset count]
    

Zset 运算操作:

  • 并集计算(相同元素分值相加),numberkeys 一共多少个 key,WEIGHTS 每个key对应的分值乘积

    ZUNIONSTORE destkey numberkeys key [key...]
    
  • 交集计算(相同元素分值相加),numberkeys 一共多少个 key,WEIGHTS 每个key对应的分值乘积

    ZINTERSTORE destkey numberkeys key [key...]
    

相比于 Set 类型,ZSet 类型不支持差集运算。

应用场景

Zset 类型可以根据元素的权重来排序,我们可以自己来决定每个元素的权重值。比如说,我们可以根据元素插入 Sorted Set 的时间确定权重值,先插入的元素权重小,后插入的元素权重大。

在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,可以优先考虑使用 Sorted Set。

排行榜

有序集合比较典型的使用场景就是排行榜。

例如,学生成绩的排名榜、游戏积分排行榜、视频播放排名、电商系统中商品的销量排名等。

我们以博客点赞排名为例,John 有五篇博文,分别获得赞为:200、40、100、50、150。

# arcticle:1 文章获得了200个赞
> ZADD user:john:ranking 200 arcticle:1
(integer) 1
# arcticle:2 文章获得了40个赞
> ZADD user:john:ranking 40 arcticle:2
(integer) 1
# arcticle:3 文章获得了100个赞
> ZADD user:john:ranking 100 arcticle:3
(integer) 1
# arcticle:4 文章获得了50个赞
> ZADD user:john:ranking 50 arcticle:4
(integer) 1
# arcticle:5 文章获得了150个赞
> ZADD user:john:ranking 150 arcticle:5
(integer) 1

文章 arcticle:4 新增一个赞,可以使用 ZINCRBY 命令:

> ZINCRBY user:john:ranking 1 arcticle:4
"51"

查看某篇文章的赞数,可以使用 ZSCORE 命令:

> ZSCORE user:john:ranking arcticle:4
"50"

获取文章赞数最多的 3 篇文章,可以使用 ZREVRANGE 命令:

# WITHSCORES 表示把 score 也显示出来
> ZREVRANGE user:john:ranking 0 2 WITHSCORES
1) "arcticle:1"
2) "200"
3) "arcticle:5"
4) "150"
5) "arcticle:3"
6) "100"

获取小林 100 赞到 200 赞的文章,可以使用 ZRANGEBYSCORE 命令:

> ZRANGEBYSCORE user:john:ranking 100 200 WITHSCORES
1) "arcticle:3"
2) "100"
3) "arcticle:5"
4) "150"
5) "arcticle:1"
6) "200"

电话、姓名排序

使用有序集合的 ZRANGEBYLEXZREVRANGEBYLEX 可以帮助我们实现电话号码或姓名的排序。

这里,我们以 ZRANGEBYLEX 为例。

注意:不要在分数不一致的 SortSet 集合中去使用 ZRANGEBYLEXZREVRANGEBYLEX 指令,因为获取的结果会不准确。

电话排序

我们可以将电话号码存储到 SortSet 中,然后根据需要来获取号段:

> ZADD phone 0 13100111100 0 13110114300 0 13132110901 
(integer) 3
> ZADD phone 0 13200111100 0 13210414300 0 13252110901 
(integer) 3
> ZADD phone 0 13300111100 0 13310414300 0 13352110901 
(integer) 3

获取所有号码:

> ZRANGEBYLEX phone - +
1) "13100111100"
2) "13110114300"
3) "13132110901"
4) "13200111100"
5) "13210414300"
6) "13252110901"
7) "13300111100"
8) "13310414300"
9) "13352110901"

获取 132 号段的号码:

> ZRANGEBYLEX phone \[132 \(133
1) "13200111100"
2) "13210414300"
3) "13252110901"

获取132、133号段的号码:

> ZRANGEBYLEX phone \[132 \(134
1\) "13200111100"
2\) "13210414300"
3\) "13252110901"
4\) "13300111100"
5\) "13310414300"
6\) "13352110901"

姓名排序

> zadd names 0 Toumas 0 Jake 0 Bluetuo 0 Gaodeng 0 Aimini 0 Aidehua
(integer) 6

获取所有人的名字:

> ZRANGEBYLEX names - +
1\) "Aidehua"
2\) "Aimini"
3\) "Bluetuo"
4\) "Gaodeng"
5\) "Jake"
6\) "Toumas"

获取名字中大写字母A开头的所有人:

> ZRANGEBYLEX names \[A \(B
1) "Aidehua"
2) "Aimini"

获取名字中大写字母 C 到 Z 的所有人:

> ZRANGEBYLEX names \[C \[Z
1) "Gaodeng"
2) "Jake"
3) "Toumas"

BitMap

介绍

Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。

由于 bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。

image

内部实现

Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。

String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。

常用命令

bitmap 基本操作:

  • 设置值,其中value只能是 0 和 1

    SETBIT key offset value
    
  • 获取值

    GETBIT key offset
    
  • 获取指定范围内值为 1 的个数

    BITCOUNT key start end
    

    其中,start 和 end 以字节为单位

bitmap 运算操作

  • BITOP 命令:

    BITOP <AND | OR | XOR | NOT> destkey key [key ...]
    

    BITOP 命令可以将多个 key 的值进行位运算,并将结果保存在 destkey 中。

  • BITCOUNT 命令:

    BITCOUNT key [start end [BYTE | BIT]]
    

    BITCOUNT 命令可以统计 key 的值里面置位的数量,并将结果保存在 destkey 中。

应用场景

Bitmap 类型非常适合二值状态统计的场景,这里的二值状态就是指集合元素的取值就只有 0 和 1 两种,在记录海量数据时,Bitmap 能够有效地节省内存空间。

签到统计

在签到打卡的场景中,我们只用记录签到(1)或未签到(0),所以它就是非常典型的二值状态。

签到统计时,每个用户一天的签到用 1 个 bit 位就能表示,一个月(假设是 31 天)的签到情况用 31 个 bit 位就可以,而一年的签到也只需要用 365 个 bit 位,根本不用太复杂的集合类型。

假设我们要统计 ID 100 的用户在 2022 年 6 月份的签到情况,就可以按照下面的步骤进行操作:

  • 第一步,执行下面的命令,记录该用户 6 月 3 号已签到

    SETBIT uid:sign:100:202206 2 1
    
  • 第二步,检查该用户 6 月 3 日是否签到。

    GETBIT uid:sign:100:202206 2
    
  • 第三步,统计该用户在 6 月份的签到次数。

    BITCOUNT uid:sign:100:202206
    

这样,我们就知道该用户在 6 月份的签到情况了。

统计这个月首次打卡时间

Redis 提供了 BITPOS key bitValue [start] [end] 指令,返回数据表示 Bitmap 中第一个值为 bitValue 的 offset 位置。

在默认情况下, 命令将检测整个位图, 用户可以通过可选的 start 参数和 end 参数指定要检测的范围。

因此,我们可以通过 BITPOS 命令来获取 userID = 100 在 2022 年 6 月份首次打卡日期:

BITPOS uid:sign:100:202206 1

需要注意的是,因为 offset 从 0 开始的,所以我们需要将返回的 value + 1 。

判断用户登陆态

Bitmap 提供了 GETBIT、SETBIT 操作,通过一个偏移值 offset 对 bit 数组的 offset 位置的 bit 位进行读写操作,需要注意的是 offset 从 0 开始。

只需要一个 key = login_status 表示存储用户登陆状态集合数据, 将用户 ID 作为 offset,在线就设置为 1,下线设置 0。通过 GETBIT判断对应的用户是否在线。 5000 万用户只需要 6 MB 的空间。

假如我们要判断 ID = 10086 的用户的登陆情况:

  • 第一步,执行以下指令,表示用户已登录

    SETBIT login_status 10086 1
    
  • 第二步,检查该用户是否登陆,返回值 1 表示已登录

    GETBIT login_status 10086
    
  • 第三步,登出,将 offset 对应的 value 设置成 0

    SETBIT login_status 10086 0
    

连续签到用户总数

如何统计出这连续 7 天连续打卡用户总数呢?

我们把每天的日期作为 Bitmap 的 key,userId 作为 offset,若是打卡则将 offset 位置的 bit 设置成 1。

key 对应的集合的每个 bit 位的数据则是一个用户在该日期的打卡记录。

一共有 7 个这样的 Bitmap,如果我们能对这 7 个 Bitmap 的对应的 bit 位做 『与』运算。同样的 UserID offset 都是一样的,当一个 userID 在 7 个 Bitmap 对应对应的 offset 位置的 bit = 1 就说明该用户 7 天连续打卡。

结果保存到一个新 Bitmap 中,我们再通过 BITCOUNT 统计 bit = 1 的个数便得到了连续打卡 7 天的用户总数了。

Redis 提供了 BITOP operation destkey key [key ...] 这个指令用于对一个或者多个 key 的 Bitmap 进行位元操作。

operation 可以是 and、OR、NOT、XOR。当 BITOP 处理不同长度的字符串时,较短的那个字符串所缺少的部分会被看作 0 。空的 key 也被看作是包含 0 的字符串序列。

假设要统计 3 天连续打卡的用户数,则是将三个 bitmap 进行 AND 操作,并将结果保存到 destmap 中,接着对 destmap 执行 BITCOUNT 统计,如下命令:

BITOP AND destmap bitmap:01 bitmap:02 bitmap:03
BITCOUNT destmap

即使一天产生一个亿的数据,Bitmap 占用的内存也不大,大约占 12 MB 的内存(10^8/8/1024/1024),7 天的 Bitmap 的内存开销约为 84 MB。同时我们最好给 Bitmap 设置过期时间,让 Redis 删除过期的打卡数据,节省内存。

HyperLogLog

介绍

HyperLogLog 是 Redis 2.8.9 版本新增的数据类型,是一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数。

注意,HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。所以,简单来说 HyperLogLog 提供不精确的去重计数

HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的内存空间总是固定的、并且是很小的。

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 \(2^{64}\) 个不同元素的基数,与 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。

这什么概念?
以 Java 语言为例,一般 long 类型占用 8 字节,而 1 字节有 8 位,即 1 byte = 8 bit,即 long 数据类型最大可以表示的数是:\(2^{63} - 1\)。假设此时有 \(2^{63} - 1\) 个数,从 \(0 ~ 2^{63} - 1\),那么,内存总数,就是 ((2^63-1) * 8/1024)K,存储空间远远超过 12K ,而 HyperLogLog 却可以用 12K 就能统计完。

内部实现

HyperLogLog 的实现涉及到很多数学问题,可以参考:HyperLogLog

常见命令

HyperLogLog 命令很少,就三个。

  • 添加指定元素到 HyperLogLog 中

    PFADD key element [element ...]
    
  • 返回给定 HyperLogLog 的基数估算值

    PFCOUNT key [key ...]
    
  • 将多个 HyperLogLog 合并为一个 HyperLogLog

    PFMERGE destkey sourcekey [sourcekey ...]
    

应用场景

百万级网页 UV 计数

在统计 UV 时,可以用 PFADD 命令把访问页面的每个用户都添加到 HyperLogLog 中。

PFADD page1:uv user1 user2 user3 user4 user5

接下来,就可以用 PFCOUNT 命令直接获得 page1 的 UV 值了,这个命令的作用就是返回 HyperLogLog 的统计结果

PFCOUNT page1:uv

需要注意,HyperLogLog 的统计规则是基于概率完成的,所以,它给出的统计结果是有一定误差的,标准误算率是 0.81%。
这也就意味着,你使用 HyperLogLog 统计的 UV 是 100 万,但实际的 UV 可能是 101 万。如果需要精确统计结果的话,最好使用 Set 或 Hash 类型。

GEO

GEO 是 Redis 3.2 版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。

在日常生活中,我们越来越依赖搜索“附近的餐馆”、在打车软件上叫车,这些都离不开基于位置信息服务(Location-Based Service,LBS)的应用。LBS 应用访问的数据是和人或物关联的一组经纬度信息,而且要能查询相邻的经纬度范围,GEO 就非常适合应用在 LBS 服务的场景中。

内部实现

GEO 本身并没有设计新的底层数据结构,而是直接使用了 Sorted Set 集合类型。

GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。

这样一来,我们就可以把经纬度保存到 Sorted Set 中,利用 Sorted Set 提供的“按权重进行有序范围查找”的特性,实现 LBS 服务中频繁使用的“搜索附近”的需求。

常用命令

  • 存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中。

    GEOADD key longitude latitude member [longitude latitude member ...]
    
  • 从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nil

    GEOPOS key member [member ...]
    
  • 返回两个给定位置之间的距离

    GEODIST key member1 member2 [m|km|ft|mi]
    
  • 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合

    GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
    

应用场景

滴滴叫车

这里以滴滴叫车的场景为例,介绍下具体如何使用 GEO 命令:GEOADDGEORADIUS 这两个命令。

假设车辆 ID 是 33,经纬度位置是(116.034579,39.030452),我们可以用一个 GEO 集合保存所有车辆的经纬度,集合 key 是 cars:locations。

把 ID 号为 33 的车辆的当前经纬度位置存入 GEO 集合中:

GEOADD cars:locations 116.034579 39.030452 33

当用户想要寻找自己附近的网约车时,LBS 应用就可以使用 GEORADIUS 命令。

例如,LBS 应用执行下面的命令时,Redis 会根据输入的用户的经纬度信息(116.054579,39.030452 ),查找以这个经纬度为中心的 5 公里内的车辆信息,并返回给 LBS 应用。

GEORADIUS cars:locations 116.054579 39.030452 5 km ASC COUNT 10

Stream

介绍

Stream 是 Redis 5.0 版本新增加的数据类型,Redis 专门为消息队列设计的数据类型。

在 Redis 5.0 Stream 没出来之前,消息队列的实现方式都有着各自的缺陷,例如:

  • 发布订阅模式:不能持久化,因此,无法可靠的保存消息,并且,对于离线重连的客户端不能读取历史消息

  • List 实现消息队列的方式:不能重复消费,一个消息消费完就会被删除,生产者需要自行实现全局唯一 ID

基于以上问题,Redis 5.0 便推出了 Stream 类型也是此版本最重要的功能,用于完美地实现消息队列,它支持消息的持久化、支持自动生成全局唯一 ID支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。

常见命令

Stream 消息队列操作命令:

  • XADD:插入消息,保证有序,可以自动生成全局唯一 ID;

  • XLEN :查询消息长度;

  • XREAD:用于读取消息,可以按 ID 读取数据;

  • XDEL : 根据消息 ID 删除消息;

  • DEL :删除整个 Stream;

  • XRANGE :读取区间消息

  • XREADGROUP:按消费组形式读取消息;

  • XPENDINGXACK

    • XPENDING:可以用来查询每个消费组内所有消费者「已读取、但尚未确认」的消息;

    • XACK:用于向消息队列确认消息处理已完成;

应用场景

消息队列

略。

总结

Redis 常见的五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及 Zset(sorted set:有序集合)。

这五种数据类型都由多种数据结构实现的,主要是出于时间和空间的考虑,当数据量小的时候使用更简单的数据结构,有利于节省内存,提高性能。


参考:

posted @ 2024-01-04 19:47  LARRY1024  阅读(72)  评论(0编辑  收藏  举报