Redis 常见数据类型

官方文档 RedisCommands

1)Redis 的命令有上百个,如果纯靠死记硬背比较困难,但是如果理解 Redis 的一些机制,会发现这些命令有很强的通用性。

2)Redis 不是万金油,有些数据结构和命令必须在特定场景下使用,一旦使用不当可能对 Redis 本身或者应用本身造成致命伤害。

基本全局命令

Redis 有 5 种数据结构,但它们都是键值对种的值,对于键来说有一些通用的命令。

KEYS

返回所有满足样式(pattern)的 key。支持如下统配样式。

• h?llo 匹配 hello , hallo 和 hxllo (匹配任意一个字符)

• h*llo 匹配 hllo 和heeeello (匹配0个或多个字符)

• h[ae]llo 匹配hello 和hallo 但不匹配 hillo (只匹配a字符和e字符)

• h[^e]llo 匹配hallo , hbllo , ... 但不匹配 hello (只匹配非e字符的其他字符)

• h[a-b]llo 匹配hallo 和 hbllo (匹配a-b之间的所有字符)

语法:

KEYS pattern

命令有效版本:1.0.0 之后

时间复杂度:O(N)

返回值:匹配 pattern 的所有 key。

EXISTS

判断某个 key 是否存在。

语法:

EXISTS key [key ...]

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:key 存在的个数。

DEL

删除指定的 key。

语法:

DEL key [key ...]

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:删除掉的 key 的个数。

EXPIRE

为指定的 key 添加秒级的过期时间(Time To Live TTL)

语法:

EXPIRE key seconds

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:1 表示设置成功。0 表示设置失败。

TTL

获取指定 key 的过期时间,秒级。

TTL key

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:剩余过期时间。-1 表示没有关联过期时间,-2 表示 key 不存在。

💡 EXPIRE 和 TTL 命令都有对应的支持毫秒为单位的版本:PEXPIRE 和 PTTL


键的过期机制

TYPE

返回 key 对应的数据类型。

语法:

TYPE key

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值: none , string , list , set , zset , hash and stream .。

数据结构和内部编码

type 命令实际返回的就是当前键的数据结构类型,它们分别是:string(字符串)、list(列表)、hash(哈希)、set(集合)、zset(有序集合),但这些只是 Redis 对外的数据结构。

实际上 Redis 针对每种数据结构都有自己的底层内部编码实现,而且是多种实现,这样 Redis 会在合适的场景选择合适的内部编码

|--------|-----------------------------------------------------------|
| 数据结构 | 内部编码 |
| string | raw(最基本的字符串) int(计数) embstr(针对段字符串进行的特殊优化) |
| hash | hashtable(哈希表) ziplist(压缩列表,在哈希表里面元素比较少的时候,可能就优化成ziplist) |
| list | linkedlist(链表) ziplist(压缩列表) |
| set | hashtable(哈希表) intset(集合中存的都是整数) |
| zset | skiplist(跳表) ziplist(压缩列表) |

可以看到每种数据结构都有至少两种以上的内部编码实现,例如 list 数据结构包含了 linkedlist 和ziplist 两种内部编码。同时有些内部编码,例如 ziplist,可以作为多种数据结构的内部实现,可以通过 object encoding 命令查询内部编码:

String字符串

字符串类型是 Redis 最基础的数据类型,关于字符串需要特别注意:

1)首先 Redis 中所有的键的类型都是字符串类型,而且其他几种数据结构也都是在字符串类似基础上构建的,例如列表和集合的元素类型是字符串类型,所以字符串类型能为其他 4 种数据结构的学习奠定基础。

2)字符串类型的值实际可以是字符串,包含一般格式的字符串或者类似 JSON、XML 格式的字符串;数字,可以是整型或者浮点型;甚至是二进制流数据,例如图片、音频、视频等。不过一个字符串的最大值不能超过 512 MB。

由于 Redis 内部存储字符串完全是按照二进制流的形式保存的,所以 Redis 是不处理字符集编码问题的,客户端传入的命令中使用的是什么字符集编码,就存储什么字符集编码

典型使用场景

缓存(Cache)功能

Redis 作为缓冲层,MySQL 作为存储层,绝大部分请求的数据都是从 Redis 中获取。由于 Redis 具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。

Redis + MySQL 组成的缓存存储架构

  1. 假设业务是根据用户uid获取用户信息

    UserInfo getUserInfo(long uid)

  2. 首先从 Redis 获取用户信息,我们假设用户信息保存在 "user:info:<uid>" 对应的键中:

    // 根据 uid 得到 Redis 的键
    String key = "user:info:" + uid;
    // 尝试从 Redis 中获取对应的值
    String value = Redis 执行命令:get key;
    // 如果缓存命中(hit)
    if (value != null) {
    // 假设我们的用户信息按照 JSON 格式存储
    UserInfo userInfo = JSON 反序列化(value);
    return userInfo;
    }

  3. 如果没有从 Redis 中得到用户信息,及缓存 miss,则进一步从 MySQL 中获取对应的信息,随后写入缓存并返回:

    // 如果缓存未命中(miss)
    if (value == null) {
    // 从数据库中,根据 uid 获取用户信息
    UserInfo userInfo = MySQL 执行 SQL:select * from user_info where uid =
    <uid>
    // 如果表中没有 uid 对应的用户信息
    if (userInfo == null) {
    响应 404
    return null;
    }
    // 将用户信息序列化成 JSON 格式
    String value = JSON 序列化(userInfo);
    // 写入缓存,为了防止数据腐烂(rot),设置过期时间为 1 小时(3600 秒)
    Redis 执行命令:set key value ex 3600
    // 返回用户信息
    return userInfo;
    }

通过增加缓存功能,在理想情况下,每个用户信息,一个小时期间只会有一次 MySQL 查询,极大地提升了查询效率,也降低了 MySQL 的访问数。

计数(Counter)功能

许多应用都会使用 Redis 作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步处理或者落地到其他数据源。例如视频网站的视频播放次数可以使用Redis 来完成:用户每播放一次视频,相应的视频播放数就会自增 1。

// 在 Redis 中统计某视频的播放次数
long incrVideoCounter(long vid) {
    key = "video:" + vid;
    long count = Redis 执行命令:incr key
    return counter;
}
// 实际中要开发一个成熟、稳定的真实计数系统,要面临的挑战远不止如此简单:防作弊、按
// 照不同维度计数、避免单点问题、数据持久化到底层数据源等。

共享会话(Session)

一个分布式 Web 服务将用户的 Session 信息(例如用户登录信息)保存在各自的服务器中,但这样会造成一个问题:出于负载均衡的考虑,分布式服务会将用户的访问请求均衡到不同的服务器上,并且通常无法保证用户每次请求都会被均衡到同一台服务器上,这样当用户刷新一次访问是可能会发现需要重新登录,这个问题是用户无法容忍的。

Session 分散存储

为了解决这个问题,可以使用 Redis 将用户的 Session 信息进行集中管理,如图 2-13 所示,在这种模式下,只要保证 Redis 是高可用和可扩展性的,无论用户被均衡到哪台 Web 服务器上,都集中从Redis 中查询、更新 Session 信息。

Redis 集中管理 Session

手机验证码

很多应用出于安全考虑,会在每次进行登录时,让用户输入手机号并且配合给手机发送验证码,然后让用户再次输入收到的验证码并进行验证,从而确定是否是用户本人。为了短信接口不会频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过 5 次。

短信验证码

此功能可以用以下伪代码说明基本实现思路:

String 发送验证码(phoneNumber) {
    key = "shortMsg:limit:" + phoneNumber;
    // 设置过期时间为 1 分钟(60 秒)
    // 使用 NX,只在不存在 key 时才能设置成功
    bool r = Redis 执行命令:set key 1 ex 60 nx
    if (r == false) {
        // 说明之前设置过该手机的验证码了
        long c = Redis 执行命令:incr key
    if (c > 5) {
        // 说明超过了一分钟 5 次的限制了
        // 限制发送
        return null;
    }
}
// 说明要么之前没有设置过手机的验证码;要么次数没有超过 5 次
String validationCode = 生成随机的 6 位数的验证码();
    validationKey = "validation:" + phoneNumber;
    // 验证码 5 分钟(300 秒)内有效
    Redis 执行命令:set validationKey validationCode ex 300;
    // 返回验证码,随后通过手机短信发送给用户
    return validationCode ;
}
// 验证用户输入的验证码是否正确
bool 验证验证码(phoneNumber, validationCode) {
    validationKey = "validation:" + phoneNumber;
    String value = Redis 执行命令:get validationKey;
    if (value == null) {
        // 说明没有这个手机的验证码记录,验证失败
        return false;
    }
    if (value == validationCode) {
        return true;
    } else {
        return false;
    }
}

常见命令

SET

将 string 类型的 value 设置到 key 中。如果 key 之前存在,则覆盖,无论原来的数据类型是什么。之前关于此 key 的 TTL (获取指定key的过期时间)也全部失效。

SET key value [expiration EX seconds|PX milliseconds] 1 [NX|XX]
set key value ex 10 相当于 set key value expire key 10
// 但是单个命令是原子的,保证这条语句是在同一时间执行的,分开写不能保证语句是同一时间执行的

命令有效版本:1.0.0 之后

时间复杂度:O(1)

SET 命令支持多种选项来影响它的行为:

• EX seconds ------ 使用秒作为单位设置 key 的过期时间。

• PX milliseconds ------ 使用毫秒作为单位设置 key 的过期时间。

• NX ------ 只在 key 不存在时才进行设置,即如果 key 之前已经存在,设置不执行。

• XX ------ 只在 key 存在时才进行设置,即如果 key 之前不存在,设置不执行。

注意:由于带选项的 SET 命令可以被 SETNX 、SETEX 、PSETEX 等命令代替,所以之后的版本中,Redis 可能进行合并。

返回值:

• 如果设置成功,返回 OK。

• 如果由于 SET 指定了 NX 或者 XX 但条件不满足,SET 不会执行,并返回 (nil)。

GET

获取 key 对应的 value。如果 key 不存在,返回 nil。如果 value 的数据类型不是 string,会报错。

语法:

GET key

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:key 对应的 value,或者 nil 当 key 不存在。

MGET和MSET

MGET 一次性获取多个 key 的值。如果对应的 key 不存在或者对应的数据类型不是 string,返回 nil。

MSET 一次性设置多个 key 的值。

MGET key [key ...]
MSET key value [key value ...]

命令有效版本:1.0.0 之后

时间复杂度:O(N) N 是 key 数量

MGET 返回值:对应 value 的列表

MSET返回值:永远是 OK


多次get和单词mget

使用 mget / mset 由于可以有效地减少了网络时间,所以性能相较更高。会使用批量操作,可以有效提高业务处理效率,但是要注意,每次批量操作所发送的键的数量也不是无节制的,否则可能造成单一命令执行时间过长,导致 Redis 阻塞。

SETNX

设置 key-value 但只允许在 key 之前不存在的情况下。

SETNX key value

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:1 表示设置成功。0 表示没有设置。

计数命令

INCR

将 key 对应的 string 表示的数字加一。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型,则报错。

INCR key

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:integer 类型的加完后的数值。

INCRBY

将 key 对应的 string 表示的数字加上对应的值。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型,则报错。

INCRBY key decrement

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:integer 类型的加完后的数值。

DECR

将 key 对应的 string 表示的数字减一。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型,则报错。

DECR key

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:integer 类型的减完后的数值。

DECYBY

将 key 对应的 string 表示的数字减去对应的值。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型,则报错。/dmk

DECRBY key decrement

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:integer 类型的减完后的数值。

INCRBYFLOAT

将 key 对应的 string 表示的浮点数加上对应的值。如果对应的值是负数,则视为减去对应的值。如果key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的不是 string,或者不是一个浮点数,则报错。允许采用科学计数法表示浮点数。

INCRBYFLOAT key increment

命令有效版本:2.6.0 之后

时间复杂度:O(1)

返回值:加/减完后的数值。

很多存储系统和编程语言内部使用 CAS 机制实现计数功能,会有一定的 CPU 开销,但在 Redis 中完全不存在这个问题,因为 Redis 是单线程架构,任何命令到了 Redis 服务端都要顺序执行。

其他命令

APPEND

如果 key 已经存在并且是一个 string,命令会将 value 追加到原有 string 的后边。如果 key 不存在,则效果等同于 SET 命令。

APPEND KEY VALUE

命令有效版本:2.0.0 之后

时间复杂度:O(1). 追加的字符串一般长度较短, 可以视为 O(1).

返回值:追加完成之后 string 的长度。

GETRANGE

返回 key 对应的 string 的子串,由 start 和 end 确定(左闭右闭)。可以使用负数表示倒数。-1 代表倒数第一个字符,-2 代表倒数第二个,其他的与此类似。超过范围的偏移量会根据 string 的长度调整成正确的值。

GETRANGE key start end

命令有效版本:2.4.0 之后

时间复杂度:O(N). N 为 [start, end] 区间的长度. 由于 string 通常比较短, 可以视为是 O(1)

返回值:string 类型的子串

SETRANGE

覆盖字符串的一部分,从指定的偏移开始。

SETRANGE key offset value

命令有效版本:2.2.0 之后

时间复杂度:O(N), N 为 value 的长度. 由于一般给的 value 比较短, 通常视为 O(1).

返回值:替换后的 string 的长度。

STRLEN

获取 key 对应的 string 的长度。当 key 存放的类似不是 string 时,报错。

STRLEN key

命令有效版本:2.2.0 之后

时间复杂度:O(1)

返回值:string 的长度。或者当 key 不存在时,返回 0。

字符串类型命令

Hash哈希

几乎所有的主流编程语言都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数组、映射。在 Redis 中,哈希类型是指值本身又是一个键值对结构,形如 key = "key",value = { {field1, value1 }, ..., {fieldN, valueN } }

字符串和哈希类型对比

哈希类型中的映射关系通常称为 field-value,用于区分 Redis 整体的键值对(key-value),注意这里的 value 是指 field 对应的值,不是键(key)对应的值,请注意 value 在不同上下文的作用。

命令

HSET/HGET

HSET key field value [field value ...]
HGET key field

HEXISTS

判断 hash 中是否有指定的字段。

HEXISTS key field

命令有效版本:2.0.0 之后

时间复杂度:O(1)

返回值:1 表示存在,0 表示不存在。

HDEL

删除 hash 中指定的字段。

HDEL key field [field ...]

命令有效版本:2.0.0 之后

时间复杂度:删除一个元素为 O(1). 删除 N 个元素为 O(N).

返回值:本次操作删除的字段个数。

HKEYS

获取hash中的所有字段

HKEYS key

命令有效版本:2.0.0 之后

时间复杂度:O(N), N 为 field 的个数.

返回值:字段列表。

HVALS

获取hash中所有的值

HVALS key

命令有效版本:2.0.0 之后

时间复杂度:O(N), N 为 field 的个数.

返回值:所有的值。

HGETALL

获取hash中所有字段以及对应的值

HGETALL key

命令有效版本:2.0.0 之后

时间复杂度:O(N), N 为 field 的个数.

返回值:字段和对应的值。

HMGET

一次获取hash中多个字段

HMGET key field [field ...]

命令有效版本:2.0.0 之后

时间复杂度:只查询一个元素为 O(1), 查询多个元素为 O(N), N 为查询元素个数.

返回值:字段对应的值或者 nil。

在使用 HGETALL 时,如果哈希元素个数比较多,会存在阻塞 Redis 的可能。如果开发人员只需要获取部分 field,可以使用 HMGET,如果一定要获取全部 field,可以尝试使用 HSCAN命令,该命令采用渐进式遍历哈希类型

HLEN

获取hash中所有字段的个数

HLEN key

命令有效版本:2.0.0 之后

时间复杂度:O(1)

返回值:字段个数。

HSETNX

在字段不存在的情况下,设置 hash 中的字段和值。

HSETNX key field value

命令有效版本:2.0.0 之后

时间复杂度:O(1)

返回值:1 表示设置成功,0 表示失败。

HINCRBY

将 hash 中字段对应的数值添加指定的值。

HINCRBY key field increment

命令有效版本:2.0.0 之后

时间复杂度:O(1)

返回值:该字段变化之后的值。

HINCRBYFLOAT

HINCRBY 的浮点数版本。

HINCRBYFLOAT key field increment

命令有效版本:2.6.0 之后

时间复杂度:O(1)

返回值:该字段变化之后的值。

内部编码

哈希的内部编码有两种:

• ziplist(压缩列表):当哈希类型元素个数小于 hash-max-ziplist-entries 配置(默认 512 个)、同时所有值都小于 hash-max-ziplist-value 配置(默认 64 字节)时,Redis 会使用 ziplist 作为哈希的内部实现,ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable 更加优秀。

• hashtable(哈希表):当哈希类型无法满足 ziplist 的条件时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1)。

  1. 当 field 个数比较少且没有大的 value 时,内部编码为 ziplist:

    127.0.0.1:6379> hmset hashkey f1 v1 f2 v2
    OK
    127.0.0.1:6379> object encoding hashkey
    "ziplist"

  2. 当有 value 大于 64 字节时,内部编码会转换为 hashtable:

    127.0.0.1:6379> hset hashkey f3 "one string is bigger than 64 bytes ... 省略..."
    OK
    127.0.0.1:6379> object encoding hashkey
    "hashtable"

  3. 当 field 个数超过 512 时,内部编码也会转换为 hashtable:

    127.0.0.1:6379> hmset hashkey f1 v1 h2 v2 f3 v3 ... 省略 ... f513 v513
    OK
    127.0.0.1:6379> object encoding hashkey
    "hashtable"

使用场景

为关系型数据表记录的两条用户信息,用户的属性表现为表的列,每条用户信息表现为行。如果映射关系表示这两个用户信息

关系型数据表保存用户信息

映射关系表示用户信息

相比于使用 JSON 格式的字符串缓存用户信息,哈希类型变得更加直观,并且在更新操作上变得更灵活。可以将每个用户的 id 定义为键后缀,多对 field-value 对应用户的各个属性,类似如下伪代码:

UserInfo getUserInfo(long uid) {
    // 根据 uid 得到 Redis 的键
    String key = "user:" + uid;
    // 尝试从 Redis 中获取对应的值
    userInfoMap = Redis 执行命令:hgetall key;
    // 如果缓存命中(hit)
    if (value != null) {
        // 将映射关系还原为对象形式
        UserInfo userInfo = 利用映射关系构建对象(userInfoMap);
        return userInfo;
    }
    // 如果缓存未命中(miss)
    // 从数据库中,根据 uid 获取用户信息
    UserInfo userInfo = MySQL 执行 SQL:select * from user_info where uid = <uid>
    // 如果表中没有 uid 对应的用户信息
    if (userInfo == null) {
        响应 404
        return null;
    }
    // 将缓存以哈希类型保存
    Redis 执行命令:hmset key name userInfo.name age userInfo.age city
    userInfo.city
    // 写入缓存,为了防止数据腐烂(rot),设置过期时间为 1 小时(3600 秒)
    Redis 执行命令:expire key 3600
    // 返回用户信息
    return userInfo;
}

但是需要注意的是哈希类型和关系型数据库有两点不同之处:

• 哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的 field,而关系型数据库一旦添加新的列,所有行都要为其设置值,即使为 null,如图 2-18 所示。

• 关系数据库可以做复杂的关系查询,而 Redis 去模拟关系型复杂查询,例如联表查询、聚合查询等基本不可能,维护成本高。

List列表

列表类型是用来存储多个有序的字符串,a、b、c、d、e 五个元素从左到右组成了一个有序的列表,列表中的每个字符串称为元素(element),一个列表最多可以存储 个元素。在 Redis 中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。

命令

LPUSH/RPUSH

将一个或者多个元素从左侧放入(头插)到 list 中。

LPUSH key element [element ...]
RPUSH key element [element ...]

命令有效版本:1.0.0 之后

时间复杂度:只插入一个元素为 O(1), 插入多个元素为 O(N), N 为插入元素个数.

返回值:插入后 list 的长度。

LPUSHX/RPUSHX

在 key 存在时,将一个或者多个元素从左/右侧侧放入(头/尾插)到 list 中。不存在,直接返回

LPUSHX key element [element ...]
RPUSHX key element [element ...]

命令有效版本:2.0.0 之后

时间复杂度:只插入一个元素为 O(1), 插入多个元素为 O(N), N 为插入元素个数.

返回值:插入后 list 的长度。

LRANGE

获取从 start 到 end 区间的所有元素,左闭右闭。

LRANGE key start stop

命令有效版本:1.0.0 之后

时间复杂度:O(N)

返回值:指定区间的元素。

L/RPOP

从 list 左/右侧取出元素(即头/尾删)。

LPOP key
RPOP key

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:取出的元素或者 nil

LINDEX

获取从左数第 index 位置的元素。

LINDEX key index

命令有效版本:1.0.0 之后

时间复杂度:O(N)

返回值:取出的元素或者 nil。

LINSERT

在特定位置插入元素。

LINSERT key <BEFORE | AFTER> 1 pivot element

命令有效版本:2.2.0 之后

时间复杂度:O(N)

返回值:插入后的 list 长度。

LLEN

获取 list 长度。

LLEN key

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:list 的长度。

阻塞版本命令

blpop 和 brpop 是 lpop 和 rpop 的阻塞版本,和对应非阻塞版本的作用基本一致,除了:

• 在列表中有元素的情况下,阻塞和非阻塞表现是一致的。但如果列表中没有元素,非阻塞版本会理解返回 nil,但阻塞版本会根据 timeout,阻塞一段时间,期间 Redis 可以执行其他命令,但要求执行该命令的客户端会表现为阻塞状态(如图 2-22 所示)。

• 命令中如果设置了多个键,那么会从左向右进行遍历键,一旦有一个键对应的列表中可以弹出元素,命令立即返回。

• 如果多个客户端同时多一个键执行 pop,则最先执行命令的客户端会得到弹出的元素。

阻塞版本的 blpop 和 非阻塞版本 lpop 的区别

列表命令小结

内部编码

列表类型的内部编码有两种:

• ziplist(压缩列表):当列表的元素个数小于 list-max-ziplist-entries 配置(默认 512 个),同时列表中每个元素的长度都小于 list-max-ziplist-value 配置(默认 64 字节)时,Redis 会选用ziplist 来作为列表的内部编码实现来减少内存消耗。

• linkedlist(链表):当列表类型无法满足 ziplist 的条件时,Redis 会使用 linkedlist 作为列表的内部实现。

  1. 当元素个数较少且没有大元素时,内部编码为 ziplist:

    127.0.0.1:6379> rpush listkey e1 e2 e3
    OK
    127.0.0.1:6379> object encoding listkey
    "ziplist"

  2. 当元素个数超过 512 时,内部编码为 linkedlist:

    127.0.0.1:6379> rpush listkey e1 e2 e3 ... 省略 e512 e513
    OK
    127.0.0.1:6379> object encoding listkey
    "linkedlist"

  3. 当某个元素的长度超过 64 字节时,内部编码为 linkedlist

    127.0.0.1:6379> rpush listkey "one string is bigger than 64 bytes ... 省略 ..."
    OK
    127.0.0.1:6379> object encoding listkey
    "linkedlist"

使用场景

消息队列

Redis 可以使用 lpush + brpop 命令组合实现经典的阻塞式生产者-消费者模型队列,生产者客户端使用 lpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式地从队列中"争抢" 队首元素。通过多个客户端来保证消费的负载均衡和高可用性。

Redis 阻塞消息队列模型


分频道的消息队列

Redis 同样使用 lpush + brpop 命令,但通过不同的键模拟频道的概念,不同的消费者可以通过 brpop 不同的键值,实现订阅不同频道的理念。


微博 Timeline

每个用户都有属于自己的 Timeline(微博列表),现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。

  1. 每篇微博使用哈希结构存储,例如微博中 3 个属性:title、timestamp、content:

    hmset mblog:1 title xx timestamp 1476536196 content xxxxx
    ...
    hmset mblog:n title xx timestamp 1476536196 content xxxxx

  2. 向用户 Timeline 添加微博,user:<uid>:mblogs 作为微博的键:

    lpush user:1:mblogs mblog:1 mblog:3
    ...
    lpush user:k:mblogs mblog:9

  3. 分页获取用户的 Timeline,例如获取用户的前 10 篇微博:

    keylist = lrange user:1:mblogs 0 9
    for key in keylist {
    hgetall key
    }

Set集合

集合类型也是保存多个字符串类型的元素的,但和列表类型不同的是,集合中 元素之间是无序的,元素不允许重复,。一个集合中最多可以存储 个元素。Redis 除了支持集合内的增删查改操作,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多问题。

集合类型

普通命令

SADD

将一个或者多个元素添加到 set 中。注意,重复的元素无法添加到 set 中。

SADD key member [member ...]

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:本次添加成功的元素个数。

SMEMBERS

获取一个 set 中的所有元素,注意,元素间的顺序是无序的。

SMEMBERS key

命令有效版本:1.0.0 之后

时间复杂度:O(N)

返回值:所有元素的列表。

SCARD

获取一个 set 的基数(cardinality),即 set 中的元素个数。

SCARD key

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:set 内的元素个数。

SPOP

从 set 中删除并返回一个或者多个元素。注意,由于 set 内的元素是无序的,所以取出哪个元素实际是未定义行为,即可以看作随机的。

SPOP key [count]

命令有效版本:1.0.0 之后

时间复杂度:O(N), n 是 count

返回值:取出的元素。

SMOVE

将一个元素从源 set 取出并放入目标 set 中。

SMOVE source destination member

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:1 表示移动成功,0 表示失败。

SREM

将指定的元素从 set 中删除。

SREM key member [member ...]

命令有效版本:1.0.0 之后

时间复杂度:O(N), N 是要删除的元素个数.

返回值:本次操作删除的元素个数。

集合间的操作

交集(inter)、并集(union)、差集(diff)

SINTER

获取给定 set 的交集中的元素。

SINTER key [key ...]

命令有效版本:1.0.0 之后

时间复杂度:O(N * M), N 是最小的集合元素个数. M 是最大的集合元素个数.

返回值:交集的元素。

SINTERSTORE

获取给定 set 的交集中的元素并保存到目标 set 中。

SINTERSTORE destination key [key ...]

命令有效版本:1.0.0 之后

时间复杂度:O(N * M), N 是最小的集合元素个数. M 是最大的集合元素个数.

返回值:交集的元素个数。

SUNION

获取给定 set 的并集中的元素。

SUNION key [key ...]

命令有效版本:1.0.0 之后

时间复杂度:O(N), N 给定的所有集合的总的元素个数.

返回值:并集的元素。

SUNIONSTORE

获取给定 set 的并集中的元素并保存到目标 set 中。

SUNIONSTORE destination key [key ...]

命令有效版本:1.0.0 之后

时间复杂度:O(N), N 给定的所有集合的总的元素个数.

返回值:并集的元素个数。

SDIFF

获取给定 set 的差集中的元素。

SDIFF key [key ...]

命令有效版本:1.0.0 之后

时间复杂度:O(N), N 给定的所有集合的总的元素个数.

返回值:差集的元素。

SDIFFSTORE

获取给定 set 的差集中的元素并保存到目标 set 中。

SDIFFSTORE destination key [key ...]

命令有效版本:1.0.0 之后

时间复杂度:O(N), N 给定的所有集合的总的元素个数.

返回值:差集的元素个数。

集合命令小结

内部编码

• intset(整数集合):当集合中的元素都是整数并且元素的个数小于 set-max-intset-entries 配置(默认 512 个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使用。

• hashtable(哈希表):当集合类型无法满足 intset 的条件时,Redis 会使用 hashtable 作为集合的内部实现。

  1. 当元素个数较少并且都为整数时,内部编码为 intset:

    127.0.0.1:6379> sadd setkey 1 2 3 4
    (integer) 4
    127.0.0.1:6379> object encoding setkey
    "intset"

  2. 当元素个数超过 512 个,内部编码为 hashtable:

    127.0.0.1:6379> sadd setkey 1 2 3 4
    (integer) 513
    127.0.0.1:6379> object encoding setkey
    "hashtable"

  3. 当存在元素不是整数时,内部编码为 hashtable:

    127.0.0.1:6379> sadd setkey a
    (integer) 1
    127.0.0.1:6379> object encoding setkey
    "hashtable"

使用场景

集合类型比较典型的使用场景是标签(tag)。例如 A 用户对娱乐、体育板块比较感兴趣,B 用户对历史、新闻比较感兴趣,这些兴趣点可以被抽象为标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于增强用户体验和用户黏度都非常有帮助。 例如一个电子商务网站会对不同标签的用户做不同的产品推荐。

下面的演示通过集合类型来实现标签的若干功能。

  1. 给用户添加标签

    sadd user:1:tags tag1 tag2 tag5
    sadd user:2:tags tag2 tag3 tag5
    ...
    sadd user:k:tags tag1 tag2 tag4

  2. 给标签添加用户

    sadd tag1:users user:1 user:3
    sadd tag2:users user:1 user:2 user:3
    ...
    sadd tagk:users user:1 user:4 user:9 user:28

  3. 删除用户下的标签

    srem user:1:tags tag1 tag5
    ...

  4. 删除标签下的用户

    srem tag1:users user:1
    srem tag5:users user:1
    ...

  5. 计算用户的共同兴趣标签

    sinter user:1 1:tags user:2:tags

Zset有序集合

有序集合相对于字符串、列表、哈希、集合来说会有一些陌生。它保留了集合不能有重复成员的特点,但与集合不同的是,有序集合中的每个元素都有一个唯一的浮点类型的分数(score)与之关联,着使得有序集合中的元素是可以维护有序性的,但这个有序不是用下标作为排序依据而是用这个分数。

有序集合提供了获取指定分数和元素范围查找、计算成员排名等功能,合理地利用有序集合,可以帮助我们在实际开发中解决很多问题。

有序集合中的元素是不能重复的,但分数允许重复。类比于一次考试之后,每个人一定有一个唯一的分数,但分数允许相同。

普通命令

ZADD

添加或者更新指定的元素以及关联的分数到 zset 中,分数应该符合 double 类型,+inf/-inf 作为正负极限也是合法的。

ZADD 的相关选项:

• XX:仅仅用于更新已经存在的元素,不会添加新元素。

• NX:仅用于添加新元素,不会更新已经存在的元素。

• CH:默认情况下,ZADD 返回的是本次添加的元素个数,但指定这个选项之后,就会还包含本次更新的元素的个数。

• INCR:此时命令类似 ZINCRBY 的效果,将元素的分数加上指定的分数。此时只能指定一个元素和分数。

ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member ...]

命令有效版本:1.2.0 之后

时间复杂度:O(log(N))

返回值:本次添加成功的元素个数。

ZCARD

获取一个 zset 的基数(cardinality),即 zset 中的元素个数。

ZCARD key

命令有效版本:1.2.0 之后

时间复杂度:O(1)

返回值:zset 内的元素个数。

ZCOUNT

返回分数在 min 和 max 之间的元素个数。默认情况下,min 和 max 都是包含的,可以通过 ( 排除。

ZCOUNT key min max

命令有效版本:2.0.0 之后

时间复杂度:O(log(N))

返回值:满足条件的元素列表个数。

ZRANGE

返回指定区间里的元素,分数按照升序。带上 WITHSCORES 可以把分数也返回。

ZRANGE key start 1 stop [WITHSCORES]

此处的 [start, stop] 为下标构成的区间. 从 0 开始, 支持负数.

命令有效版本:1.2.0 之后

时间复杂度:O(log(N)+M)

返回值:区间内的元素列表。

ZREVRANGE

返回指定区间里的元素,分数按照降序。带上 WITHSCORES 可以把分数也返回。

这个命令可能在 6.2.0 之后废弃,并且功能合并到 ZRANGE 中。

ZREVRANGE key start stop [WITHSCORES]

命令有效版本:1.2.0 之后

时间复杂度:O(log(N)+M)

返回值:区间内的元素列表。

ZRANGEBYSCORE

返回分数在 min 和 max 之间的元素,默认情况下,min 和 max 都是包含的,可以通过 ( 排除。

这个命令可能在 6.2.0 之后废弃,并且功能合并到 ZRANGE 中。

ZRANGEBYSCORE key min 1 max [WITHSCORES]

命令有效版本:1.0.5 之后

时间复杂度:O(log(N)+M)

返回值:区间内的元素列表。

ZPOPMAX

删除并返回分数最高的 count 个元素。

ZPOPMAX key [count]

命令有效版本:5.0.0 之后

时间复杂度:O(log(N) * M)

返回值:分数和元素列表。

BZPOPMAX

ZPOPMAX 的阻塞版本。

BZPOPMAX key [key ...] timeout

命令有效版本:5.0.0 之后

时间复杂度:O(log(N))

返回值:元素列表。

ZPOPMIN

删除并返回分数最低的 count 个元素。

ZPOPMIN key [count]

命令有效版本:5.0.0 之后

时间复杂度:O(log(N) * M)

返回值:分数和元素列表。

BZPOPMIN

ZPOPMIN 的阻塞版本。

BZPOPMIN key [key ...] timeout

命令有效版本:5.0.0 之后

时间复杂度:O(log(N))

返回值:元素列表。

ZRANK

返回指定元素的排名,升序。

ZRANK key member

命令有效版本:2.0.0 之后

时间复杂度:O(log(N))

返回值:排名。

ZREVRANK

返回指定元素的排名,降序。

ZREVRANK key member

命令有效版本:2.0.0 之后

时间复杂度:O(log(N))

返回值:排名。

ZSCORE

返回指定元素的分数。

ZSCORE key member

命令有效版本:1.2.0 之后

时间复杂度:O(1)

返回值:分数。

ZREM

删除指定的元素。

ZREM key member [member ...]

命令有效版本:1.2.0 之后

时间复杂度:O(M*log(N))

返回值:本次操作删除的元素个数。

ZREMRANGEBYRANK

按照排序,升序删除指定范围的元素,左闭右闭。

ZREMRANGEBYRANK key start stop

命令有效版本:2.0.0 之后

时间复杂度:O(log(N)+M)

返回值:本次操作删除的元素个数。

ZREMRANGEBYSCORE

按照分数删除指定范围的元素,左闭右闭。

ZREMRANGEBYSCORE key min max

命令有效版本:1.2.0 之后

时间复杂度:O(log(N)+M)

返回值:本次操作删除的元素个数。

ZINCRBY

为指定的元素的关联分数添加指定的分数值。

ZINCRBY key increment member

命令有效版本:1.2.0 之后

时间复杂度:O(log(N))

返回值:增加后元素的分数。

ZINTERSTORE

求出给定有序集合中元素的交集并保存进目标有序集合中,在合并过程中以元素为单位进行合并,元素对应的分数按照不同的聚合方式和权重得到新的分数。

ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight
[weight ...]] [AGGREGATE <SUM | MIN | MAX>]

命令有效版本:2.0.0 之后

时间复杂度:O(N*K)+O(M*log(M)) N 是输入的有序集合中, 最小的有序集合的元素个数; K 是输入了

几个有序集合; M 是最终结果的有序集合的元素个数.

返回值:目标集合中的元素个数

2 表示后面会指定 2 个有序集合 作为输入集合,分别是 zset1 和 zset2。 如果你有更多的有序集合参与交集计算,可以增加这个数字,并在后面列出这些集合的名称。

ZUNIONSTORE

求出给定有序集合中元素的并集并保存进目标有序集合中,在合并过程中以元素为单位进行合并,元素对应的分数按照不同的聚合方式和权重得到新的分数。

ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight
[weight ...]] [AGGREGATE <SUM | MIN | MAX>]

命令有效版本:2.0.0 之后

时间复杂度:O(N)+O(M*log(M)) N 是输入的有序集合总的元素个数; M 是最终结果的有序集合的元素

个数.

返回值:目标集合中的元素个数

内部编码

  1. ziplist(压缩列表):当有序集合的元素个数小于 zset-max-ziplist-entries 配置(默认 128 个),同时每个元素的值都小于 zset-max-ziplist-value 配置(默认 64 字节)时,Redis 会用 ziplist 来作为有序集合的内部实现,ziplist 可以有效减少内存的使用。
  2. skiplist(跳表):当 ziplist 条件不满足时,有序集合会使用 skiplist 作为内部实现,因为此时ziplist 的操作效率会下降。

当元素个数较少且每个元素较小时,内部编码为 ziplist:
127.0.0.1:6379> zadd zsetkey 50 e1 60 e2 30 e3
(integer) 3
127.0.0.1:6379> object encoding zsetkey
"ziplist"


当元素个数超过 128 个,内部编码 skiplist
127.0.0.1:6379> zadd zsetkey 50 e1 60 e2 30 e3 ... 省略 ... 82 e129
(integer) 129
127.0.0.1:6379> object encoding zsetkey
"skiplist"


当某个元素大于 64 字节时,内部编码 skiplist:
127.0.0.1:6379> zadd zsetkey 50 "one string bigger than 64 bytes ... 省略 ..."
(integer) 1
127.0.0.1:6379> object encoding zsetkey
"skiplist"

使用场景

有序集合比较典型的使用场景就是排行榜系统。例如常见的网站上的热榜信息,榜单的维度可能是多方面的:按照时间、按照阅读量、按照点赞量。本例中我们使用点赞数这个维度,维护每天的热榜:

假如用户A发布了一篇文章,并获得了3个赞,可以使用有序集合的zadd和zincrby功能,之后在获得赞继续使用zincrby,如果用户删除文章了,需要将用户从榜单中删除掉,可以使用zrem。展示获赞最多的10个用户,可以使用zrevrange命令实现

渐进式遍历

Redis 使用 scan 命令进行渐进式遍历键,进而解决直接使用 keys 获取键时可能出现的阻塞问题。每次 scan 命令的时间复杂度是 O(1),但是要完整地完成所有键的遍历,需要执行多次 scan。

首次 scan 从 0 开始.

当 scan 返回的下次位置为 0 时, 遍历结束.

SCAN

以渐进式的方式进行键的遍历。

SCAN cursor [MATCH pattern] [COUNT 1 count] [TYPE type]

命令有效版本:2.8.0 之后

时间复杂度:O(1)

返回值:下一次 scan 的游标(cursor)以及本次得到的键。

渐进性遍历 scan 虽然解决了阻塞的问题,但如果在遍历期间键有所变化(增加、修改、删除),可能导致遍历时键的重复遍历或者遗漏,这点务必在实际开发中考虑。

数据库管理

Redis 提供了几个面向 Redis 数据库的操作,分别是 dbsize、select、flushdb、flushall 命令,

切换数据库

select dbIndex

许多关系型数据库,例如 MySQL 支持在一个实例下有多个数据库存在的,但是与关系型数据库用字符来区分不同数据库名不同,Redis 只是用数字作为多个数据库的实现。Redis 默认配置中是有 16个数据库。select 0 操作会切换到第一个数据库,select 15 会切换到最后一个数据库。0 号数据库和15 号数据库保存的数据是完全不冲突的,即各种有各自的键值对。默认情况下,我们处于数据库 0。

Redis 管理的数据库

Redis 中虽然支持多数据库,但随着版本的升级,其实不是特别建议使用多数据库特性。如果真的需要完全隔离的两套键值对,更好的做法是维护多个 Redis 实例,而不是在一个Redis 实例中维护多数据库。这是因为本身 Redis 并没有为多数据库提供太多的特性,其次无论是否有多个数据库,Redis 都是使用单线程模型,所以彼此之间还是需要排队等待命令的执行。同时多数据库还会让开发、调试和运维工作变得复杂。所以实践中,始终使用数据库 0 其实是一个很好的选择。

清除数据库

flushdb / flushall 命令用于清除数据库,区别在于 flushdb 只清除当前数据库,flushall 会清楚所有数据库。

相关推荐
RainbowSea32 分钟前
6. MySQL 索引的数据结构(详细说明)
数据库·sql·mysql
jk_1011 小时前
MATLAB中strip函数用法
java·服务器·数据库
XiYang-DING1 小时前
【数据库系统概论】第七章 数据库设计
数据库
尘鹄1 小时前
一文讲懂Go语言如何使用配置文件连接数据库
开发语言·数据库·后端·golang
窜天猴牛逼1 小时前
IO多路复用(epoll)/数据库(sqlite)
数据库
老哥不老1 小时前
结合 Pandas 使用 SQLite3 实战
数据库·sqlite·pandas
eddie_k22 小时前
MySQL主从架构配合ShardingJdbc实现读写分离
数据库·mysql·架构
P7进阶路2 小时前
nginx 代理 redis
运维·redis·nginx
八股文领域大手子4 小时前
责任链模式+策略模式在项目中的实践
java·数据库·redis·sql·mysql
亥时科技5 小时前
园区环境数字化管理系统(源码+文档+讲解+演示)
java·数据库·开源·源代码管理