我们都知道 Redis 提供了丰富的数据类型,常见的有五种:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
随着 Redis 版本的更新,后面又支持了四种数据类型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。
string
底层主要通过int和SDS简单字符串实现。
SDS比C语言中字符串的优点:
1、不仅可以存字符串,还可以存二进制数据,sds的API都是用二进制方式处理buf里的数据;
2.有一个记录长度的字段,因此只需要常数复杂度就可以获得sds的长度;
3.拼接字符串不会溢出,因为会检查。
字符串对象的内部编码(encoding)有 3 种 :int、raw和 embstr。
如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成 long),并将字符串对象的编码设置为int。
如果保存短字符串,就会用embstr编码,是一种针对段字符串的优化编码。
会通过一次内存分配函数来分配一块连续的内存空间来保存redisObject和SDS。如果是大字符串,就会用raw编码,一个指针指向sds,和对象不连续。
这么做好处,一次分配就一次释放,更方便,并且在一起的话CPU缓存提升命中率。空间局部性。
但是embstr导致字符串长度增加需要重新分配内存,就需要重新分配整个对象和数据。实际上,embstr编码的字符串是不能更改的。当我们对embstr编码的字符串对象执行任何修改命令(例如append)时,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令。
#常用指令
SET name zx
GET name
EXISTS name
基本就是大写单词的缩写。
应用场景:
常规计数
因为 Redis 处理命令是单线程,所以执行命令的过程是原子的。因此 String 数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等等。
分布式锁
SET 命令有个 NX 参数可以实现「key不存在才插入」,可以用它来实现分布式锁:
如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
如果 key 存在,则会显示插入失败,可以用来表示加锁失败。
一般而言,还会对分布式锁加上过期时间,分布式锁的命令如下:
SET lock_key unique_value NX PX 10000 (PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。
而解锁的过程就是将 lock_key 键删除,但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。
共享session信息
session信息保存在服务器端,但是服务器可能有多个,如果下一次分配到不同的服务器,就需要重新登录。因此,我们需要借助 Redis 对这些 Session 信息进行统一的存储和管理,这样无论请求发送到那台服务器,服务器都会去同一个 Redis 获取相关的 Session 信息,这样就解决了分布式系统下 Session 存储的问题。
LIST
List 列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。
List 类型的底层数据结构是由双向链表或压缩列表(元素个数小于某个值,且每个元素小于64字节)实现的。但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。
LPUSH key value [value …] 将一个或多个值value插入到key列表的表头(最左边)
应用场景
消息队列。并且满足三个需求,分别是消息保序、处理重复的消息和保证消息可靠性。
消息保序先进先出本身就可以保证。但是在pop读取消息时有一个性能风险点。如果消费者想要及时处理消息,就需要在程序中不停地调用 RPOP 命令(比如使用一个while(1)循环)这就会导致消费者程序的 CPU 一直消耗在执行 RPOP 命令上。为了解决这个问题,Redis提供了 BRPOP 命令。BRPOP命令也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据。
如何实现处理重复消息。首先每个消息有一个全局ID,消费者对比消息ID和已经处理的ID,决定是不是重复。这个全局ID是要自己输入的。比如
例如,我们执行以下命令,就把一条全局 ID 为 111000102、库存量为 99 的消息插入了消息队列:
LPUSH mq “111000102:stock:99”
实现可靠性
当消费者程序从 List 中读取一条消息后,List 就不会再留存这条消息了。所以,如果消费者程序在处理消息的过程出现了故障或宕机,就会导致消息没有处理完成,那么,消费者程序再次启动后,就没法再次从 List 中读取消息了。
为了留存消息,List 类型提供了 BRPOPLPUSH 命令,这个命令的作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存。
这样一来,如果消费者程序读了消息但没能正常处理,等它重启后,就可以从备份 List 中重新读取消息并进行处理了。
List缺点就是一条信息只能一个消费者读取。后来版本的有STREAM类型,实现消费组读取。
Hash 是一个键值对(key - value)集合,其中 value 的形式如: value=[{field1,value1},…{fieldN,valueN}]。Hash 特别适合用于存储对象。
Hash 类型的底层数据结构是由压缩列表或哈希表实现的:在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
主要用来缓存对象。比如在关系数据库中
存储一个哈希表uid:1的键值
HMSET uid:1 name Tom age 15
存储一个哈希表uid:2的键值
HMSET uid:2 name Jerry age 13
获取哈希表用户id为1中所有的键值
HGETALL uid:1
- “name”
- “Tom”
- “age”
- “15”
购物车,用户ID就是键,商品 id 为 field,商品数量为 value。
Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。所以 Set 类型除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。Set 类型的底层数据结构是由哈希表或整数集合实现的:
往集合key中存入元素,元素存在则忽略,若key不存在则新建
SADD key member [member …]
从集合key中删除元素
SREM key member [member …]
获取集合key中所有元素
SMEMBERS key
获取集合key中的元素个数
SCARD key
交集运算
SINTER key [key …]
将交集结果存入新集合destination中
SINTERSTORE destination key [key …]
这里有一个潜在的风险。Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。
在主从集群中,为了避免主库因为 Set 做聚合计算(交集、差集、并集)时导致主库被阻塞,我们可以选择一个从库完成聚合统计,或者把数据返回给客户端,由客户端来完成聚合统计。
主要用于点赞统计,共同关注数量,抽奖活动。
Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序结合的元素值,一个是排序值。
Zset 类型的底层数据结构是由压缩列表或跳表实现的:
有序集合比较典型的使用场景就是排行榜
Bitmap 类型非常适合二值状态统计的场景,这里的二值状态就是指集合元素的取值就只有 0 和 1 两种,在记录海量数据时,Bitmap 能够有效地节省内存空间。
#签到统计,登录状态
简单来说 HyperLogLog 提供不精确的去重计数。
HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的内存空间总是固定的、并且是很小的。
所以,非常适合统计百万级以上的网页 UV 的场景。
LBS 应用访问的数据是和人或物关联的一组经纬度信息,而且要能查询相邻的经纬度范围,GEO 就非常适合应用在 LBS 服务的场景中。
#内部实现
GEO 本身并没有设计新的底层数据结构,而是直接使用了 Sorted Set 集合类型。
GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。