Data Structure

Redis中常用数据结构

  • String
  • List
  • Hash
  • Set
  • Zet

String

实现原理

string有3种编码模式

  • INT:当字符串是数字,并且小于 long long时。
  • EMBSTR:字符串长度小于等于阈值。
  • RAW:上面的条件都不满足时,才用raw。

INT

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

EMBSTR

sdshdr的内存是和redisObject一块分配的,是连续的。所以叫做embed string。

因为内存分配需要

RAW

不难发现RAW和EMBSTR都有个sdshdr结构,会在Data Structure Impl文章中介绍。

应用场景

缓存对象

使用 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
#阅读量+1
> INCR aritcle:readcount:1001
(integer) 2
#阅读量+1
> INCR aritcle:readcount:1001
(integer) 3
# 获取对应文章的阅读量
> GET aritcle:readcount:1001
"3"

分布式锁

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

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

更具体的实现,见其他文章。

共享 Session 信息

通常我们在开发后台管理系统时,会使用 Session 来保存用户的会话(登录)状态,这些 Session 信息会被保存在服务器端,但这只适用于单系统应用,如果是分布式系统此模式将不再适用。

例如用户一的 Session 信息被存储在服务器一,但第二次访问时用户一被分配到服务器二,这个时候服务器并没有用户一的 Session 信息,就会出现需要重复登录的问题,问题在于分布式系统每次会把请求随机分配到不同的服务器。

分布式系统单独存储 Session 流程图:

因此,我们需要借助 Redis 对这些 Session 信息进行统一的存储和管理,这样无论请求发送到那台服务器,服务器都会去同一个 Redis 获取相关的 Session 信息,这样就解决了分布式系统下 Session 存储的问题。

分布式系统使用同一个 Redis 存储 Session 流程图:

List

实现原理

3.2版本之前,List对象有两种编码,方式

  • ZipList
  • LinkedList:双向链表

当满足如下条件时,用ZipList编码

  • 当列表对象保存的所有字符串对象长度都小于64字节;
  • 列表元素小于512个;(注意,这是List的限制,不是底层的ZipList的限制)

不满足时,自动转换成LinkedList编码。

Ziplist采用压缩列表实现,内存布局非常紧凑,指针域的空间占比非常少。

ZipList在数据较少时,用于节约内存,插入时也会造成多节点的内存复制。

LinkedList在数据多时,提升效率,但也会导致指针域占比高,占用不少内存。

3.2版本引入了quicklist,是ZipList和Linked的结合体。

LinkedList是一个节点放一个数据,现在QuickList是单个节点寸一个ZipList,即多个数据。

优势是

  1. 数据较少时,只有一个节点,即是ZipList。
  2. 数据多时,同时做了折中。

ZipList被ListPack取代

ZipList本身存在一个连锁更行的问题,所以在Redis7.0后,被ListPack给取代了,但本质都是压缩链表,内存是紧凑在一块的。

应用场景

简单消息队列

  • 不支持多个消费者消费同一条消息
  • 不支持消费组的实现

Set

实现原理

Set有两种编码方式,整数集合INTSET和HASHTABLE。

INTSET

当以下条件满足时,是用INTSET编码,反之使用HASHTABLE。

  • 元素都是整数
  • 元素数量不超过512个

INTSET实际上是有序的数组,通过二分查找来查询。布局如下

HASHTABLE

实际上就是一个哈希表。

应用场景

点赞

点赞作为一个高频率的操作,如果每次操作都读写数据库会增加数据库的压力,所以采用缓存+定时任务来实现。点赞数据是在redis中缓存半小时,同时定时任务是每隔5分钟执行一次,做持久化存储,这里的缓存时间和任务执行时间可根据项目情况而定。

共同关注

集合取交集。

抽奖活动

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

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

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

Hash

实现原理

Hset底层有两种编码格式,压缩列表和HASHTABLE。

满足以下两个条件时,使用压缩链表,反之使用HASHTABLE。

  • Hset对象保存的所有key和value的长度都小于64字节。
  • Hset对象元素小于512个。

其中压缩链表就是把key-value这整体作为entry存入压缩链表。

HASHTABLE的编码方式的话,和Set的区别在于,在Set中value始终为NULL,而HSet是对应的value。

应用场景

缓存对象

一个hash就是一个对象,存储对象的每一个元素。

在介绍 String 类型的应用场景时有所介绍,String + Json也是存储对象的一种方式,那么存储对象时,到底用 String + json 还是用 Hash 呢?

一般对象用 String + Json 存储,对象中某些频繁变化的属性可以考虑抽出来用 Hash 类型存储。

购物车

以用户 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 查询一次数据库,获取完整的商品的信息。

Zset

实现原理

Zset底层有两种编码方式,一种是压缩链表,一种是SkipList+HT。

满足下列条件时,采用压缩链表编码,反之采用SkipList+HT。

  • 列表保存的字符串对象都小于64字节
  • 列表对象元素个数少于128个。

应用场景

  1. 电话排序
  2. 姓名排序

Data Structure
https://messenger1th.github.io/2024/07/24/Redis/Data Structure/
作者
Epoch
发布于
2024年7月24日
许可协议