Skip to content

数据类型篇

String

String是最基本的key-value结构,key是唯一标识,value是具体的值。value最大数据长度是512M.

内部实现:

主要用的是int和SDS。

SDS和普通的C语言字符串不太一样,之所以没有使用C语言的字符串表示,是因为SDS的优点:

  • SDS不仅可以保存文本数据,还可以保存二进制数据。传统C语言用字符串末尾的/0来判断字符串是否结束,而SDS是用属性值len。

字符串对象的内部编码有三种:int、raw和embstr。

image

如果一个字符串对象保存的是整数值并可以用long表示,那么字符串对象会把整数值保存在字符串对象结构的ptr属性里面,并且把字符串对象的编码设置成int。

如果字符串对象保存字符串,并且长度<=32字节,那么字符串对象就用的是简单动态字符串SDS来保存,对象编码为embstr,是专门用于保存短字符串的一种优化编码方式。

使用场景:

直接缓存对象,以json的形式。

常规计数,分布式锁,key就是锁名字,value是加锁的客户端,保证只有加锁的客户端才可以释放锁。共享session信息。

List

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

内部实现

底层数据结构是由双向链表或者压缩列表实现的。最大长度为2^32-1,也就是每个列表支持超过40亿个元素。

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

如果不满足,那么就使用双向链表作为List类型的底层数据结构。

但是在Redis3.2之后,List的底层数据结构就改成了quicklist实现。

应用场景

  • 消息队列 消息队列在存取消息的时候,必须满足三个需求,分别是消息保序,处理重复的消息和保证消息可靠性。
  1. 如何满足消息保序需求?

为什么要满足消息是有序的?假设一种业务场景,创建订单 -> 支付订单 -> 发货 若消息顺序颠倒为发货-> 支付,那么业务上就导致了异常,不应该出现先发货再支付的情况。

List本身是按照先进先出的顺序对数据进行存储,可以使用LPUSH + RPOP命令实现消息队列。

生产者使用 LPUSH key value 将消息插入到队列的头部,消费者用RPOP key依次读取队列的消息,先进先出。

不过消费者在读取数据的时候有潜在的风险性能点。

生产者往list写入数据,消费者并不会知道有新的消息写入了,需要不断调用RPOP命令。如果有新消息写入,RPOP就会返回结果,否则返回空值。

所以如果长时间没消息写入,消费者就会一直空耗CPU。为了解决这个问题,Redis提供了BRPOP命令,称为阻塞式读取,客户端在没读取到数据的时候自动阻塞,直到有新的数据写入队列再开始读取新的数据。

image 2. 如何处理重复的消息?

需要保证两步:

  • 每个消息都有一个全局的ID
  • 消费者要记录已经处理过的消息ID,收到一条消息,消费者程序会对比收到的消息ID和记录的已处理过的消息ID判断当前消息是否已经处理。

但是List不会为每个消息生成ID号,要自行为每个消息生成一个全局唯一ID。

比如:

redis
LPUSH mq "111000102:stock:99"
(integer) 1

上面这条命令就把一条全局ID为111000102、库存量为99的消息插入了消息队列。

  1. 如何保证消息的可靠性?

当消费者读取了一条消息之后,List里面就没有这条消息了。如果处理过程出现了故障,消息就没有处理完成,丢失掉了。

为了留存消息,List类型提供了BRPOPLPUSH 命令。这个命令是让消费者程序从一个List中读取消息,同时,Redis会把这个消息再插入到另一个List,可以叫做备份List留存。

这样,如果消费者程序读取了消息但是没能正常处理,等它重启后,就可以从备份List中重新读取消息并处理。

到这里为止,基于List的消息队列就可以满足消息队列的三大需求:消息有序、处理重复消息和保证消息可靠性。

  • 消息保序:用LPUSH + RPOP;
  • 重复处理消息:生产者自行生成全局唯一ID。
  • 消息可靠性:BRPOPLPUSH

List作为消息队列的缺陷

List不支持多个消费者消费同一条消息。在rocketMQ中,可以有多个消费者组订阅同一个消息,然后每个消费者组独立维护消费进度。

但是从Redis5.0开始,Stream数据类型可以满足消息队列的需求,也支持消费者组的形式进行消息读取。

Hash

内部实现

由压缩列表或者哈希表实现。

  • 如果哈希类型元素个数小于512,所有的值小于64字节,由压缩列表作为实现,否则是哈希表。

在Redis7.0中,压缩列表数据结构已经废弃了,交给listpack数据结构实现。

应用场景

存储对象。

但是实际上,用String + json也是存储对象的一种方式。那么实际是用json还是用hash?

一般对象用String + json存储,对象中某些频繁变化的属性可以考虑用Hash存储。

比如说像用户的购物车,以用户的ID为key,商品ID为field,商品数量为value。购物车是变动频繁的,很符合购物车的特征。

Set

介绍

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

一个集合最多存储2^32-1个元素。Set类型除了增删改查,还支持多个集合取交并差集。

如果集合中元素都是整数并且个数小于512,默认用整数集合作为Set底层数据结构。否则使用哈希表。

应用场景

比较适合数据去重和保障数据唯一性。 Set的差集、并集和交集的计算复杂度较高,所以数据量较大的时候会导致Redis实例卡住。

在主从集群中,为了避免主库因为Set做集合运算被阻塞,可以用从库完成聚合统计。

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

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

抽奖活动

可以把候选用户存到Set里面。然后用命令随机抽取出用户,就是中奖的ID。

允许重复中奖,用SRANDMEMBER命令。 set random member。

如果不允许重复中奖,就用SPOP命令。

Zset

介绍

Zset是有序集合类型,相比于Set多了一个排序属性score。对于Zset,每个存储元素有两个值。一个是有序结合的元素值,另一个是排序值。

内部实现

由压缩列表或者跳表实现:集合元素小于128,每个元素小于64字节,Redis会用压缩列表作为Zset类型的底层数据结构。否则就是跳表。

Redis7以后,压缩列表数据结构已经废弃了,交给了listpack数据结构实现。

添加命令:ZADD key score member。

应用场景

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

比较典型的应用场景就是排行榜。例如学生成绩的排行榜、游戏积分排行榜等。

以点赞排名为例子,巴拉巴啊。

BitMap

介绍

Bitmap,即为位图。是一串连续的二进制数组。可以通过偏移量定位元素。

内部实现

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

常用场景

签到统计。key是用户和年份、月份信息,然后value就是bit,比如

redis
SETBIT uid:sign:100:202206 2 1

上面的命令就是给uid为100的用户,在2022年06月,偏移量为2,也就是六月三日的地方置为1.

如果要检查该用户六月三日是否签到,就用get

redis
GETBIT uid:sign:100:202206 2

也可以统计用户在六月份的签到次数

redis
BITCOUNT uid:sign:100:202206

也可以统计这个月的首次打卡时间:问题等价于返回bitmap中第一个值为bitvalue的offset位置:

redis
BITPOS uid:sign:100:202206 1

上面的命令检查bitmap中从头开始第一个为1的bit的偏移量。这里也可以指定上可选的start和end在结尾,指定要检测的范围。

判断用户登录态

也很简单,假如用户的ID是递增的,那么用户登录了,就给对应的位置置为1.

HyperLogLog

这是用于统计基数的数据集合类型。基数统计就是指统计一个集合中不重复的元素个数。但是要注意,HyperLogLog是基于概率的,标准误差率是0.81%。

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

在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64个不同元素的基数。

应用场景

百万级网页的UV计数

GEO

RedisGEO是Redis3.2版本新加的数据类型,主要存储地理位置信息,并对存储的信息进行操作。

内部实现

GEO直接使用了Sorted Set集合类型。

Stream

介绍

Redis Stream是Redis5.0新增的数据类型,专门为消息队列涉及的数据类型。

在Stream之前,消息队列的实现方式有各自的缺陷:

  • 发布订阅模式:不能持久化,无法可靠地保存消息,对于离线重连的客户端不能读取历史消息。
  • List实现的消息队列无法实现重复消费,一个消息消费完就会被删除,而且生产者需要自行实现全局唯一ID。

基于以上问题,Redis5.0推出了Steam类型,也就是这个版本最重要的功能,用于完美地支持消息队列。它支持消息持久化、支持全局生成唯一ID、支持ack确认消息的模式、支持消费组模式等,让消息队列更加地稳定和可靠。

常见命令

  • XADD:插入消息,保证有序,自动生成全局唯一ID。
  • XLEN:查询消息长度
  • XREAD:读取消息,按ID读取数据
  • XDEL:根据消息ID删除消息
  • DEL:删除整个Stream
  • XRANGE:读取区间消息
  • XREADGROUP:按消费者组形式读取消息
redis
XADD mymq * name xiaolin
"1654254953808-0"

上面语句的的意思是,往mymq这个消息队列中插入一条消息,键是key,value是xiaolin。 插入成功后会返回全局唯一的ID:

  • 第一部分是数据插入时,以毫秒为计数的时间戳,第二部分表示在当前毫秒的第n条信息。

消费者通过XREAD从消息队列中读取消息时,可以指定一个消息ID,并从这个消息ID的下一条消息开始进行读取。

redis的的消息中间件会丢失消息,如AOF是每秒写盘,但是是异步的,Redis宕机时可能丢失数据,主从复制也是异步的,主从切换时也可能丢失数据。

Redis Stream消息可堆积吗?

Redis的数据存储在内存中,如果消息积压,内存持续增长,如果超过机器 内存上限,会面临被OOM的风险。

Redis的发布/订阅机制为什么不可以作为消息队列?

发布订阅机制存在以下缺点:

  1. 发布订阅机制没有基于任何数据类型实现,没有数据持久化的的能力,不会被写入AOF和RDB中,当Redis宕机,数据会全部丢失。
  2. 发布订阅模式是发后即忘的模式,如果有订阅者重连,无法消费之前的历史消息。
  3. 当消费端有消息积压时,消费端会被却强行断开,这个参数是在配置文件中设置的。

总结

加油!

wow!