--- icon: logos:redis title: Redis 基本数据类型 cover: https://raw.githubusercontent.com/dunwu/images/master/snap/20230901071808.png date: 2020-06-24 10:45:38 categories: - 数据库 - KV数据库 - redis tags: - 数据库 - KV 数据库 - redis - 数据类型 permalink: /pages/76331397/ --- # Redis 基本数据类型 > 关键词:`String`、`Hash`、`List`、`Set`、`Zset` Redis 提供了多种数据类型,每种数据类型有丰富的命令支持。 Redis 支持的基本数据类型:STRING、HASH、LIST、SET、ZSET Redis 支持的高级数据类型:BitMap、HyperLogLog、GEO、Stream 使用 Redis ,不仅要了解其数据类型的特性,还需要根据业务场景,灵活的、高效的使用其数据类型来建模。  ## String ### String 简介 String 类型是键值对结构。 String 类型是**二进制安全**的。二进制安全是指,String 类型不仅可以保存文本数据,还可以保存任意格式的二进制数据,如:图片、音频、视频、压缩文件等。 默认情况下,String 类型的值最大可为 **512 MB**。  ### String 实现 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 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。 **字符串对象的编码可以是 `int` 、 `raw` 或者 `embstr`** 。    字符串对象保存各类型值的编码方式: | 值 | 编码 | | :-------------------------------------------------------------------------------------------------------------------------- | :------------------ | | 可以用 `long` 类型保存的整数。 | `int` | | 可以用 `long double` 类型保存的浮点数。 | `embstr` 或者 `raw` | | 字符串值, 或者因为长度太大而没办法用 `long` 类型表示的整数, 又或者因为长度太大而没办法用 `long double` 类型表示的浮点数。 | `embstr` 或者 `raw` | 如果一个字符串对象保存的是整数值, 并且这个整数值可以用 `long` 类型来表示, 那么字符串对象会将整数值保存在字符串对象结构的 `ptr` 属性里面(将 `void*` 转换成 `long` ), 并将字符串对象的编码设置为 `int` 。 【示例】set 整数值 ```shell > SET number 10086 OK > OBJECT ENCODING number "int" ``` 如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度大于 `39` 字节, 那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值, 并将对象的编码设置为 `raw` 。 ```c > SET story "Long, long, long ago there lived a king ..." OK > STRLEN story (integer) 43 > OBJECT ENCODING story "raw" ``` 如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度小于等于 `39` 字节, 那么字符串对象将使用 `embstr` 编码的方式来保存这个字符串值。`embstr` 编码是专门用于保存短字符串的一种优化编码方式。 【示例】set 字符串值 ```c > SET msg "hello" OK > OBJECT ENCODING msg "embstr" ``` ### String 命令 | 命令 | 说明 | | -------- | ----------------------------------- | | `SET` | 存储一个字符串值 | | `SETNX` | 仅当键不存在时,才存储字符串值 | | `GET` | 获取指定 key 的值 | | `MGET` | 获取一个或多个指定 key 的值 | | `INCRBY` | 将 key 中储存的数字加上指定的增量值 | | `DECRBY` | 将 key 中储存的数字减去指定的减量值 | > 更多命令请参考:[Redis String 类型官方命令文档](https://redis.io/commands#string) 【示例】SET、GET、DEL 操作 ```shell # 将 key(name) 的 value 保存为 dunwu > set name dunwu OK # 获取 key(name) 的 value > get name "dunwu" # 将 key(name) 的 value 保存为 unknown(覆盖原 value) > set name unknown OK > get name "unknown" # 检查 key(name) 是否存在 > exists name (integer) 1 # 删除 key(name) > del name (integer) 1 > exists name (integer) 0 > get name (nil) ``` 【示例】SETNX 操作 ```shell # 检查 key(lock) 是否存在 > exists lock (integer) 0 # 将 key(lock) 的 value 保存为 1,保存成功 > setnx lock 1 (integer) 1 # 将 key(lock) 的 value 保存为 2,由于 key 已存在,保存失败 > setnx lock 2 (integer) 0 # 获取 key(lock) 的 value > get lock "1" ``` 【示例】MSET、MGET 操作 ```shell # 批量设置 one、two、three 这 3 个 key > mset one 1 tow 2 three 3 OK # 批量获取 one、two、three 3 个 key 的 value > mget one tow three 1) "1" 2) "2" 3) "3" ``` 【示例】INCR、DECR、INCRBY、DECRBY 操作 ```shell # 将 key(counter) 的 value 保存为 0 > set counter 0 OK # 将 key(counter) 的 value 加 1 > incr counter (integer) 1 # 将 key(counter) 的 value 加 9 > incrby counter 9 (integer) 10 # 将 key(counter) 的 value 减 1 > decr counter (integer) 9 # 将 key(counter) 的 value 减 9 > decrby counter 9 (integer) 0 ``` ### String 应用 **适用场景:缓存、计数器、共享 Session** #### 缓存对象 使用 String 来缓存对象有两种方式: (1)缓存对象的 JSON 值 ```shell > set user:1 {"name":"dunwu","sex":"man"} ``` (2)将 key 分离为 user:ID: 属性的形式,采用 MSET 存储,用 MGET 获取各属性值 ```shell > mset user:1:name dunwu user:1:sex man OK > mget user:1:name user:1:sex 1) "dunwu" 2) "man" ``` #### 计数器 【需求场景】 统计网站某内容的点击量、收藏量、点赞数等等。 【解决方案】 > 使用 Redis 的 String 类型存储一个计数器。 维护计数器的常见操作如下: - 增加统计值 - 使用 `INCR`、`DECR` 命令 - 减少统计值 - 使用 `INCRBY`、`DECRBY` 操作 【示例代码】 ```shell # 初始化 ID 为 1024 的博文访问量为 0 > set blog:view:1024 0 OK # ID 为 1024 的博文访问量加 1 > incr blog:view:1024 (integer) 1 # ID 为 1024 的博文访问量加 1 > incr blog:view:1024 (integer) 2 # 查看 ID 为 1024 的博文访问量 > get blog:view:1024 "2" ``` #### 分布式锁 (1)申请锁 SET 命令有个 NX 参数可以实现“key 不存在才插入”,可以用它来实现分布式锁: - 如果 key 不存在,则显示插入成功,可以用来表示加锁成功; - 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。 一般而言,还会对分布式锁加上过期时间,分布式锁的命令如下: ```shell SET key value NX PX 30000 ``` - key - 就是分布式锁的关键字; - value - 是客户端生成的唯一的标识; - NX - 表示只有 `key` 不存在的时候才会设置成功。(如果此时 redis 中存在这个 key,那么设置失败,返回 `nil`) - PX 30000 - 表示:30s 后,key 会被删除(这意味着锁被释放了)。设置过期时间,是为了防止出现各种意外,导致锁始终无法释放的情况。 (2)释放锁 释放锁就是删除 key ,但是一般可以用 `lua` 脚本删除,判断 value 一样才删除,这是为了保证释放锁操作和申请所操作是同一个客户端。由于涉及两个操作,为了保证原子性,可以使用 lua 脚本来实现,因为 Redis 执行 Lua 脚本时,是以原子性方式执行的。 ```Lua -- 删除锁的时候,找到 key 对应的 value,跟自己传过去的 value 做比较,如果是一样的才删除。 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end ``` #### 共享 Session 信息 在分布式场景下,一个用户的 Session 如果只存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器上,该服务器没有用户的 Session,就可能导致用户需要重新进行登录等操作。 分布式 Session 的几种实现策略: 1. 粘性 session 2. 应用服务器间的 session 复制共享 3. 基于缓存的 session 共享 ✅ 基于缓存的 session 共享实现 > **使用一个单独的存储服务器存储 Session 数据**,可以存在 MySQL 数据库上,也可以存在 Redis 或者 Memcached 这种内存型数据库。 > > 缺点:需要去实现存取 Session 的代码。  ## Hash ### Hash 简介  Hash 是一个键值对(key - value)集合,其中 value 的形式如: `value=[{field1,value1},...{fieldN,valueN}]`。Hash 特别适合用于存储对象。 ### Hash 实现 哈希对象的编码可以是 `ziplist` 或者 `hashtable` 。 `ziplist` 编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时, 程序会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾。   `hashtable` 编码的哈希对象使用字典作为底层实现, 哈希对象中的每个键值对都使用一个字典键值对来保存。  当哈希对象同时满足以下两个条件时, 使用 `ziplist` 编码;否则,使用 `hashtable` 编码。 1. 哈希对象保存的所有键值对的键和值的字符串长度都小于 `64` 字节(可由 `hash-max-ziplist-value` 配置); 2. 哈希对象保存的键值对数量小于 `512` 个(可由 `hash-max-ziplist-entries` 配置); > 注意:这两个条件的上限值是可以修改的, 具体请看配置文件中关于 `hash-max-ziplist-value` 选项和 `hash-max-ziplist-entries` 选项的说明。 ### Hash 命令 | 命令 | 行为 | | --------- | -------------------------- | | `HSET` | 将指定字段的值设为 value | | `HGET` | 获取指定字段的值 | | `HGETALL` | 获取所有键值对 | | `HMSET` | 设置多个键值对 | | `HMGET` | 获取所有指定字段的值 | | `HDEL` | 删除指定字段 | | `HINCRBY` | 为指定字段的整数值加上增量 | | `HKEYS` | 获取所有字段 | > 更多命令请参考:[Redis Hash 类型官方命令文档](https://redis.io/commands#hash) ```shell # 存储一个哈希表 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 应用 > **Hash 类型适用于存储结构化数据**。 #### 缓存对象 Hash 类型的(key,field,value)的结构与对象的(对象 id,属性,值)的结构相似,也可以用来存储对象。 我们以用户信息为例,它在关系型数据库中的结构是这样的: 我们可以使用如下命令,将用户对象的信息存储到 Hash 类型: ```shell # 存储一个哈希表 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" ``` Redis Hash 存储其结构如下图: 在介绍 String 类型的应用场景时有所介绍,String + Json 也是存储对象的一种方式,那么存储对象时,到底用 String + json 还是用 Hash 呢? 一般对象用 String + Json 存储,对象中某些频繁变化的属性可以考虑抽出来用 Hash 类型存储。 #### 购物车 【需求场景】 用户浏览电商平台,添加商品到购物车,并支持查看购物车。需要考虑未登录的情况。 【解决方案】 > 可以使用 HASH 类型来实现购物车功能。 > > 以用户 session 为 key,存储了商品 ID 和商品数量的映射。其中,商品 id 为 field,商品数量为 value。 > > 为什么不使用用户 ID? > > 因为很多场景下需要支持用户在免登陆的情况下使用购物车的,因为未登录,所以无法知道用户的用户 ID,这种情况下使用用户 session 更合适。并且由于绑定的是 session,可以在清空 session 时,顺便清空购物车缓存,更加方便。 维护购物车的常见操作如下: - 添加商品 - `HSET cart:{session} {商品 id} 1` - 添加数量 - `HINCRBY cart:{session} {商品 id} 1` - 商品总数 - `HLEN cart:{session}` - 删除商品 - `HDEL cart:{session} {商品 id}` - 获取购物车所有商品 - `HGETALL cart:{session}` 当前仅仅是将商品 ID 存储到了 Redis 中,在回显商品具体信息的时候,还需要拿着商品 id 查询一次数据库,获取完整的商品的信息。 ## List Redis 中的 List 类型就是有序列表。 ### List 简介  List 列表是简单的字符串列表,**按照插入顺序排序**,可以从头部或尾部向 List 列表添加元素。 列表的最大长度为 `2^32 - 1`,也即每个列表支持超过 `40 亿`个元素。 ### List 实现 列表对象的编码可以是 `ziplist` 或者 `linkedlist` 。 `ziplist` 编码的列表对象使用压缩列表作为底层实现, 每个压缩列表节点(entry)保存了一个列表元素。  `inkedlist` 编码的列表对象使用双链表作为底层实现。  当列表对象可以同时满足以下两个条件时, 列表对象使用 `ziplist` 编码;否则,使用 `linkedlist` 编码 1. 列表对象保存的所有字符串元素的长度都小于 `64` 字节; 2. 列表对象保存的元素数量小于 `512` 个; > 注意 > > 以上两个条件的上限值是可以修改的, 具体请看配置文件中关于 `list-max-ziplist-value` 选项和 `list-max-ziplist-entries` 选项的说明。 ### List 命令 | 命令 | 行为 | | -------- | ------------------------------------------ | | `LPUSH` | 将给定值推入列表的右端。 | | `RPUSH` | 将给定值推入列表的右端。 | | `LPOP` | 从列表的左端弹出一个值,并返回被弹出的值。 | | `RPOP` | 从列表的右端弹出一个值,并返回被弹出的值。 | | `LRANGE` | 获取列表在给定范围上的所有值。 | | `LINDEX` | 获取列表在给定位置上的单个元素。 | | `LREM` | 从列表的左端弹出一个值,并返回被弹出的值。 | | `LTRIM` | 只保留指定区间内的元素,删除其他元素。 | > 更多命令请参考:[Redis List 类型官方命令文档](https://redis.io/commands#list) ```shell # 将一个或多个值 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 ``` ### List 应用 #### 消息队列 消息队列在存取消息时,必须要满足三个需求,分别是**消息保序、处理重复的消息和保证消息可靠性**。 Redis 的 List 和 Stream 两种数据类型,就可以满足消息队列的这三个需求。我们先来了解下基于 List 的消息队列实现方法,后面在介绍 Stream 数据类型时候,在详细说说 Stream。 _1、如何满足消息保序需求?_ List 本身就是按先进先出的顺序对数据进行存取的,所以,如果使用 List 作为消息队列保存消息的话,就已经能满足消息保序的需求了。 List 可以使用 LPUSH + RPOP(或者反过来,RPUSH+LPOP)命令实现消息队列。 - 生产者使用 `LPUSH key value[value...]` 将消息插入到队列的头部,如果 key 不存在则会创建一个空的队列再插入消息。 - 消费者使用 `RPOP key` 依次读取队列的消息,先进先出。 不过,在消费者读取数据时,有一个潜在的性能风险点。 在生产者往 List 中写入数据时,List 并不会主动地通知消费者有新消息写入,如果消费者想要及时处理消息,就需要在程序中不停地调用 `RPOP` 命令(比如使用一个 while(1) 循环)。如果有新消息写入,RPOP 命令就会返回结果,否则,RPOP 命令返回空值,再继续循环。 所以,即使没有新消息写入 List,消费者也要不停地调用 RPOP 命令,这就会导致消费者程序的 CPU 一直消耗在执行 RPOP 命令上,带来不必要的性能损失。 为了解决这个问题,Redis 提供了 BRPOP 命令。**BRPOP 命令也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据**。和消费者程序自己不停地调用 RPOP 命令相比,这种方式能节省 CPU 开销。 _2、如何处理重复的消息?_ 消费者要实现重复消息的判断,需要 2 个方面的要求: - 每个消息都有一个全局的 ID。 - 消费者要记录已经处理过的消息的 ID。当收到一条消息后,消费者程序就可以对比收到的消息 ID 和记录的已处理过的消息 ID,来判断当前收到的消息有没有经过处理。如果已经处理过,那么,消费者程序就不再进行处理了。 但是 **List 并不会为每个消息生成 ID 号,所以我们需要自行为每个消息生成一个全局唯一 ID**,生成之后,我们在用 LPUSH 命令把消息插入 List 时,需要在消息中包含这个全局唯一 ID。 例如,我们执行以下命令,就把一条全局 ID 为 111000102、库存量为 99 的消息插入了消息队列: ```shell > LPUSH mq "111000102:stock:99" (integer) 1 ``` _3、如何保证消息可靠性?_ 当消费者程序从 List 中读取一条消息后,List 就不会再留存这条消息了。所以,如果消费者程序在处理消息的过程出现了故障或宕机,就会导致消息没有处理完成,那么,消费者程序再次启动后,就没法再次从 List 中读取消息了。 为了留存消息,List 类型提供了 `BRPOPLPUSH` 命令,这个命令的**作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存**。 这样一来,如果消费者程序读了消息但没能正常处理,等它重启后,就可以从备份 List 中重新读取消息并进行处理了。 好了,到这里可以知道基于 List 类型的消息队列,满足消息队列的三大需求(消息保序、处理重复的消息和保证消息可靠性)。 - 消息保序:使用 LPUSH + RPOP; - 阻塞读取:使用 BRPOP; - 重复消息处理:生产者自行实现全局唯一 ID; - 消息的可靠性:使用 BRPOPLPUSH > List 作为消息队列有什么缺陷? **List 不支持多个消费者消费同一条消息**,因为一旦消费者拉取一条消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费。 要实现一条消息可以被多个消费者消费,那么就要将多个消费者组成一个消费组,使得多个消费者可以消费同一条消息,但是 **List 类型并不支持消费组的实现**。 这就要说起 Redis 从 5.0 版本开始提供的 Stream 数据类型了,Stream 同样能够满足消息队列的三大需求,而且它还支持“消费组”形式的消息读取。 #### 输入自动补全 【需求场景】 根据用户输入,自动补全信息,如:联系人、商品名等。 - 典型场景一 - 社交网站后台记录用户最近联系过的 100 个好友,当用户查找好友时,根据输入的关键字自动补全姓名。 - 典型场景二 - 电商网站后台记录用户最近浏览过的 10 件商品,当用户查找商品是,根据输入的关键字自动补全商品名称。 【解决方案】 > 使用 Redis 的 List 类型存储一个最近信息列表,然后在需要自动补全信息时展示相应数量的数据。 维护最近信息列表的常见操作如下: - 如果指定信息已经存在于最近信息列表里,那么从列表里移除。使用 `LREM` 命令。 - 将指定信息添加到最近信息列表的头部。使用 `LPUSH` 命令。 - 添加操作完成后,如果最近信息列表中的数量超过上限 N,进行裁剪操作。使用 `LTRIM` 命令。 ## Set Redis 中的 Set 类型就是无序且去重的集合。 ### Set 简介