数据类型篇
Redis 常见数据类型和应用场景
Redis提供了丰富的数据类型 常见的有5种:String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合)。
随着Redis版本更新,后面又支持了四种数据类型:BitMap(2.2新增)、HyperLogLog(2.8新增)、GEO(3.2新增)、Stream(5.0新增)。
String
介绍
String是最基本的key-value结构,key是唯一标识,value是具体的值,value不仅可以是字符串,也可以是数字(整数或者浮点数),value容纳的数据上限为512M。
内部实现
String 类型的底层的数据结构实现主要是 long 和 SDS(简单动态字符串)。
SDS和C字符串不太一样,之所以没用C语言的字符串,是因为SDS相比于C的原生字符串:
-
SDS不仅可以保存文本数据,还可以保存二进制文件。因为SDS使用len属性的值而不是空字符来判断字符串是否结束,并且SDS的所有API都会以处理二进制的方式来处理SDS存放在buf[]数组里的数据,因此SDS不仅能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。
-
SDS获取字符串长度的时间复杂度是O(1)。C语言的字符串不记录自身长度,故获取长度的复杂度为O(n);而SDS结构里用len属性记录了字符串长度。
-
Redis的SDS API是安全的,拼接字符串不会造成缓冲区溢出。因为SDS在拼接字符串前会检查SDS空间是否满足要求,如果空间不够会自动扩容。
字符串对象的内部编码(encoding)有 3 种 :int、embstr和raw 。
-
如果存储的字符串是整数值,并且这个整数值可以用long类型表示且大小在LONG_MAX范围内,则会采用int编码:直接将数据保存在RedisObject的ptr指针位置(刚好8字节)。
-
如果字符串对象保存的是一个字符串,且此字符串长度小于等于44字节(redis5.0后),那么字符串对象会使用SDS来保存,编码设置为embstr,此编码专用于保存短字符串。
-
如果字符串对象保存的是一个字符串,且此字符串长度大于44字节,那么字符串对象会使用SDS来保存,编码设置为raw。
embstr和raw区别:embstr通过一次内存分配函数来分配一块连续的内存空间来保存redisObject和SDS,而raw会调用两次。
好处:embstr编码的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用CPU缓存提升性能。
坏处:如果字符串长度增加需要重新分配内存时,整个robj(redisObject)和sds都需要重新分配空间,因此执行任何修改命令时,程序会先将对象的编码从embstr转成raw,再执行修改命令。
应用场景
缓存对象
使用string来缓存对象有两种方式:
-
直接缓存整个对象的JSON,命令例子:SET user:1 '{"name":"caesar","age":18}'。
-
采用将key分离为user:ID:属性, MSET 存储,MGET 获取各属性值。
常规计数
因为redis处理命令是单线程,所以执行命令的过程是原子的,因此string数据类型适合计数场景,比如计算访问次数,点赞、转发、库存数量等等。
分布式锁
SET命令有个NX参数可以实现[key不存在时才插入],可以用它来实现分布式锁。
如果key不存在则显示插入成功,可用来表示加锁成功。
如果key存在则显示插入失败,可用来表示加锁失败。
共享session信息
通常开发后台管理系统时,会使用session保存用户的会话(登录)状态,这些session会保存在服务器端,但只适用单系统应用不适用分布式系统。
eg:用户1的session信息被存储到服务器1,但第二次访问时 用户1被分配到服务器2,这时服务器并没有用户1的session信息,就会出现需要重复登陆的问题。问题在于分布式系统会把请求随机分配到不同服务器。
借助redis对这些sission信息统一管理,这样无论请求发送到哪一台服务器,服务器都会去同一个redis获取相关sission信息。
List
介绍
List是简单的字符串列表,按照插入顺序排序,可以从首、尾操作列表中的元素。
内部实现
-
LinkedList :普通链表,可以从双端访问,内存占用较高,内存碎片较多
-
ZipList :压缩列表,可以从双端访问,内存占用低,存储上限低(redis7.0后废弃,交友listpack数据结构来实现了)
-
QuickList:LinkedList + ZipList,可以从双端访问,内存占用较低,包含多个ZipList,存储上限高
3.2版本前redis采用LinkedList和ZipList来实现List,当元素数量小于512并且元素大小小于64字节时采用ZipList编码,超过则采用LinkedList编码。
3.2版本后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。
应用场景
消息队列
消息队列在存取消息时,必须要满足三个需求,分别是消息保序、处理重复的消息和保证消息可靠性。Redis 的 List 和 Stream 两种数据类型,就可以满足消息队列的这三个需求。
1、如何满足消息保序需求?
list本身就是按照先进先出的顺序对数据存取的,故已经满足保序。
不过,消费者在读取消息时潜在一个性能风险。
生产者往list中写入消息时并不会通知消费者有新消息写入,如果消费者想及时获取消息,需要不停调用RPOP,所以即使没有新消息写入,消费者也会不停调用,这样会导致消费者的CPU一直消耗在调用RPOP命令上,造成不必要的性能损耗。
为了解决这一问题,redis提供了BRPOP命令,即 阻塞式读取,客户端在没有读到队列数据时会自动阻塞,直到有新的数据写入队列,再开始读取新数据。(节省了CPU开销)
2、如何处理重复的消息?
消费者要实现重复消息的判断,需要 2 个方面的要求:
-
每个消息都要有一个全局ID
-
消费者需记录已经处理过的消息的ID。当消费者收到一条消息时需要判断此消息是否已经处理过,若处理过则不再进行处理。
但是List不会为每个消息生成ID号,这需要我们自行为每条消息生成一个全局唯一ID,并在LPUSH时将命令插入消息。
3、如何保证消息可靠性?
当消费者从list中读取一条消息后,List不会再将此消息留存,故 消费者在处理消息时出现了宕机或故障就会导致消息没有处理完,重启后也无法再次从List中读取消息。
为了留存消息,List类型提供了BRPOPLPUSH命令,作用是让消费者程序从一个list中读取消息的同时,redis会把该消息插入到另一个list留存(也可叫做 备份list)
这样一来,如果消费者程序在处理消息时发生故障,重启后可从list备份中重新读取消息并处理。
list作为消息队列的缺陷:
-
List不支持多个消费者消费同一条消息。(消息被消费后 list不留存)
-
List类型不支持消费者组的实现。
Set
介绍
Set是Redis中的单列集合,满足下列特点:
-
无序
-
元素唯一
-
可进行交集、并集、差集操作
list类型和set类型的区别:
-
list可存储重复元素,set只能存储唯一元素。
-
存储元素时list有序,set无序。
内部实现
Set 类型的底层数据结构是由哈希表或整数集合实现的:
Dict(哈希表)中的key用来存储元素,value统一为null。当存储的所有数据都是整数,并且元素数量不超过set-max-intset-entries(默认512)时,Set会采用IntSet编码,以节省内存。
应用场景
集合的几个特性:无序,唯一,支持交差并等操作。
因此Set类型比较适合用来数据去重和保障数据唯一性,还可用来统计多个集合的交、差、并集。
潜在性能风险:由于set的交差并集计算复杂度比较高,在数据量大的情况下,如果直接执行这些计算,会导致Redis实例阻塞。
点赞
当设计 key 是文章id,value 是用户id时,即可保证一个用户只能点一个赞。
共同关注
点赞
当设计 key 是用户id,value 是公众号时,即可利用set的交差并操作来计算共同关注的公众号等。
抽奖活动
存储某活动中中奖的用户名 ,Set 类型因为有去重功能,可以保证同一个用户不会中奖两次。key为抽奖活动名,value为员工名称。
Zset
介绍
ZSet也就是SortedSet,其中每一个元素都需要指定一个score值和member值:
-
可以根据score值排序
-
member必须唯一
-
可以根据member查询分数
内部实现
Zset 类型的底层数据结构是由压缩列表或跳表(实现ZSet时 也包括了dict 下面图解中有反映)实现的:
当元素数量不多时,HT和SkipList的优势不明显,而且更耗内存。因此zset还会采用ZipList结构来节省内存,不过需要同时满足两个条件:
-
元素数量小于zset_max_ziplist_entries,默认值128
-
每个元素都小于zset_max_ziplist_value字节,默认值64
ziplist本身没有排序功能,而且没有键值对的概念,因此需要有zset通过编码实现:
-
ZipList是连续内存,因此score和element是紧挨在一起的两个entry, element在前,score在后
-
score越小越接近队首,score越大越接近队尾,按照score值升序排列
应用场景
Zset类型(Sorted Set,有序集合),可根据元素的权重来排序,我们可以自己决定每个元素的权重值。比如,我们可以根据元素插入Sorted Set的时间确定权重值,先插入的权重小,后插入的权重大。
排行榜
有序集合比较经典的场景就是排行榜。eg:学生成绩的排行榜,游戏积分排行榜,视频播放排名,电商系统中商品的销量排名等。
Hash
介绍
hash是一个键值对集合,特别适用于存储对象。
hash与string对象的区别如下:
Hash结构与Redis中的Zset非常类似,其区别如下:
-
zset的键是member,值是score;hash的键和值都是任意值
-
zset要根据score排序;hash则无需排序
内部实现
Hash 类型的底层数据结构是由压缩列表或哈希表实现的:
如果哈希类型元素个数小于512(默认值),所有值小于64字节,redis会使用ziplist(压缩列表)作为哈希的底层数据结构。若不满足,redis会使用哈希表作为哈希的底层数据结构。
ZipList中相邻的两个entry 分别保存field和value
应用场景
缓存对象
购物车
用户id为key,商品id为field,商品数量为value
BitMap
介绍
bitmap,位图。是一连串二进制数组,可通过偏移量offset定位元素,BitMap通过最小单位bit来进行0、1的设置,表示某个元素的值或状态,时间复杂度为O(1)。
bit是计算机中最小的单位,使用起来非常节省空间,特别适用于一些数据量大且使用二值统计的场景。
内部实现
Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。
string类型是会保存为二进制的字节数组,可以把bitmap看为一个bit数组。
应用场景
签到统计
在签到打卡的场景中,我们只用记录签到(1)或未签到(0),所以它就是非常典型的二值状态。
判断用户登陆态
连续签到用户总数
HyperLogLog
介绍
HyperLogLog是一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数。 统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。
在redis中每个HyperLogLog键只需要花费12KB内存就可以计算接近2^64个不同元素的基数,非常节省空间。
应用场景
百万级网页 UV 计数
GEO
介绍
主要用于存储地理位置信息并对其进行操作。
内部实现
GEO 本身并没有设计新的底层数据结构,而是直接使用了 Sorted Set 集合类型。
GEO 类型使用GeoHash编码方法实现了经纬度到Sorted Set中元素权重分数的转换,这其中的两个关键
机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编
码值来表示,并把编码值作为Sorted Set元素的权重分数。这样一来,我们就可以把经纬度保存到Sorted Set中,利用Sorted Set提供的“按权重进行有序范围查找”的特性,实现LBS服务(位置信息服务 Location-Based Service)中频繁使用的“搜索附近”的需求。
应用场景
滴滴叫车
假设车辆 ID 是 33,经纬度位置是(116.034579,39.030452),我们可以用一个 GEO 集合保存所有车辆的经纬度,集合 key 是 cars:locations。
执行下面命令就可以把 ID 号为 33 的车辆的当前经纬度位置存入 GEO 集合中:
GEOADD cars:locations 116.034579 39.030452 33
当用户想要寻找自己附近的网约车时,LBS 应用就可以使用 GEORADIUS 命令。
执行下面命令,redis会根据输入的用户的经纬度信息,查找以此经纬度为中心 5km内的车辆信息。
GEOADD cars:locations 116.034579 39.030452 5 km ASC COUNT 10
Stream
介绍
Redis Stream是Redis 5.0版本新增加的数据类型,Redis专门为消息队列设计的数据类型。
在Redis 5.0 Stream 没出来之前,消息队列的实现方式都有着各自的缺陷,例如:
-
发布订阅模式,不能持久化也就无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷;
-
List实现消息队列的方式不能重复消费,一个消息消费完就会被删除,而且生产者需要自行实现全局唯一ID。
基于以上问题,Redis 5.0便推出了Stream类型也是此版本最重要的功能,用于完美地实现消息队列,它
支持消息的持久化、支持自动生成全局唯一ID、支持ack确认消息的模式、支持消费组模式等,让消息
队列更加的稳定和可靠。
应用场景
消息队列
如果想要实现阻塞读(当没有数据时,阻塞住),可以调用XRAED时设定BLOCK配置项,实现类似于
BRPOP的阻塞读取操作。
Stream的基础方法,使用xadd 存入消息和xread 循环阻塞读取消息的方式可以实现简易版的消息队列,
交互流程如下图所示:
Stream 特有的功能
stream可以创建消费组。
消息队列中的消息一旦被消费组里的一个消费者读取了,就不能再被该消费组内的其他消费者读取了,即
同一个消费组里的消费者不能消费同一条消息。
但是,不同消费组的消费者可以消费同一条消息(但是有前提条件,创建消息组的时候,不同消费组指定
了相同位置开始读取消息)。
使用消费组的目的是让组内的多个消费者共同分担读取消息,所以,我们通常会让每个消费者读取部分消
息,从而实现消息读取负载在多个消费者间是均衡分布的。
基于Stream实现的消息队列,如何保证消费者在发生故障或宕机再次重启后,仍然可以读取未处
理完的消息?
Streams 会自动使用内部队列(也称为PENDING List)留存消费组里每个消费者读取的消息,直到消费者
使用XACK命令通知Streams"消息已经处理完成”。
消费确认增加了消息的可靠性,一般在业务处理完成之后,需要执行XACK命令确认消息已经被消费完
成,整个流程的执行如下图所示:
如果消费者没有成功处理消息,它就不会给Streams发送XACK命令,消息仍然会留存。此时,消费者可
以在重启后,用XPENDING命令查看已读取、但尚未确认处理完成的消息。
Stream消息队列与专业的消息队列的差距
一个专业的消息队列需要做到:
-
消息不丢
-
消息可堆积
1、Redis Stream 消息会丢失吗?
使用一个消息队列,其实就分为三大块:生产者、队列中间件、消费者,所以要保证消息就是保证三个环
节都不能丢失数据。
-
redis生产者会不会丢消息取决于消费者是否处理好返回值和异常(如adk确认响应),如果处理好,那么此阶段不会丢失消息。
-
Redis 消费者也不会丢失消息,因为Stream(MQ中间件)会自动使用内部队列(也称为PENDING List)留存消费组里每个消费者读取的消息,但是未被确认的消息。
-
Redis 消息中间件则会丢消息,Redis在以下2个场景下,都会导致数据丢失:
-
AOF持久化配置为每秒写盘,但这个写盘过程是异步的,Redis宕机时会存在数据丢失的可能
-
主从复制也是异步的,主从切换时,也存在丢失数据的可能口。
2、Redis Stream 消息可堆积吗?
当指定队列最大长度时,队列长度超过上限后,旧消息会被删除,只保留固定长度的新消息。这么来看,
Stream 在消息积压时,如果指定了最大长度,还是有可能丢失消息的。
因此,把Redis当作队列来使用时,会面临的2个问题:
-
Redis 本身可能会丢数据;
-
面对消息挤压,内存资源会紧张;
所以,能不能将Redis作为消息队列来使用,关键看你的业务场景:
-
如果你的业务场景足够简单,对于数据丢失不敏感,而且消息积压概率比较小的情况下,把Redis当作
队列是完全可以的。
-
如果你的业务有海量消息,消息积压的概率比较大,并且不能接受数据丢失,那么还是用专业的消息队
列中间件吧。
总结
Redis 常见的五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sorted set:有序集合)。
这五种数据类型都由多种数据结构实现的,主要是出于时间和空间的考虑,当数据量小的时候使用更简单
的数据结构,有利于节省内存,提高性能。
可以看到,Redis数据类型的底层数据结构随着版本的更新也有所不同,比如:
-
在Redis 3.0版本中List对象的底层数据结构由「双向链表」或「压缩表列表」实现,但是在3.2版本之后,List数据类型底层数据结构是由quicklist 实现的;
-
在最新的Redis代码中,压缩列表数据结构已经废弃了,交由listpack数据结构来实现了
Redis 五种数据类型的应用场景:
-
String 类型的应用场景:缓存对象、常规计数、分布式锁、共享session信息等。
-
List类型的应用场景:消息队列(有两个问题:1.生产者需要自行实现全局唯一ID;2.不能以消费组形式消费数据)等。
-
Hash类型:缓存对象、购物车等。
-
Set类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
-
Zset类型:排序场景,比如排行榜、电话和姓名排序等。
Redis 后续版本又支持四种数据类型,它们的应用场景如下:
-
BitMap(2.2版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
-
HyperLogLog(2.8版新增):海量数据基数统计的场景,比如百万级网页UV计数等;
-
GEO(3.2版新增):存储地理位置信息的场景,比如滴滴叫车;
-
Stream(5.0版新增):消息队列,相比于基于List类型实现的消息队列,有这两个特有的特性:自动
生成全局唯一消息ID,支持以消费组形式消费数据。
笔者述:以上内容为本人复习redis数据类型时根据`小林coding`和`黑马程序员`的知识库总结所得,若内容有分歧或不清楚的地方欢迎评论,更详细的内容可见:小林coding图解计算机网络、操作系统、计算机组成、数据库,让天下没有难懂的八股文!https://xiaolincoding.com/