【Redis】Redis中的常见数据类型(一)

文章目录


前言

Redis 提供了 5 种数据结构,理解每种数据结构的特点对于 Redis 开发运维非常重要,同时掌握每 种数据结构的常见命令,会在使用Redis 的时候做到游刃有余。本篇文章将带大家认识Redis中的部分数据结构的特点及其命令还有其应用场景。为介绍的会在下篇文章为大家详解!

在正式介绍数据结构之前,了解⼀下 Redis 的⼀些全局命令、数据结构和内部编码、单线程命令处理机制是十分必要的,接下来我们先对这些内容进行介绍。

主要体现在两个方面:

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

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

一、Redis前置知识

1. 全局命令

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

KEYS

返回所有满⾜样式(pattern)的 key。⽀持如下统配样式。 • h?llo 匹配 hello , hallo 和 hxllo

• h*llo 匹配 hllo 和 heeeello

• h[ae]llo 匹配 hello 和 hallo 但不匹配 hillo

• h[^e]llo 匹配 hallo , hbllo , ... 但不匹配 hello

• h[a-b]llo 匹配 hallo 和 hbllo

语法:

clike 复制代码
KEYS pattern

时间复杂度:O(N)

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

示例:

clike 复制代码
redis> MSET firstname Jack lastname Stuntman age 35
"OK"
redis> KEYS *name*
1) "firstname"
2) "lastname"
redis> KEYS a??
1) "age"
redis> KEYS *
1) "age"
2) "firstname"
3) "lastname"

EXISTS

判断某个 key 是否存在。 语法:

clike 复制代码
 EXISTS key [key ...]

时间复杂度:O(1)

返回值:key 存在的个数

clike 复制代码
redis> SET key1 "Hello"
"OK"
redis> EXISTS key1
(integer) 1
redis> EXISTS nosuchkey
(integer) 0
redis> SET key2 "World"
"OK"
redis> EXISTS key1 key2 nosuchkey
(integer) 2

DEL

删除指定的 key。

语法:

clike 复制代码
DEL key [key ...]

时间复杂度:O(1)

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

⽰例:

clike 复制代码
redis> SET key1 "Hello"
"OK"
redis> SET key2 "World"
"OK"
redis> DEL key1 key2 key3
(integer) 2
EXPIRE

EXPIRE

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

语法:

clike 复制代码
EXPIRE key seconds

时间复杂度:O(1)

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

示例:

clike 复制代码
redis> SET mykey "Hello"
"OK"
redis> EXPIRE mykey 10
(integer) 1
redis> TTL mykey
(integer) 10

TTL

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

语法:

clike 复制代码
 TTL key

时间复杂度:O(1)

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

clike 复制代码
redis> SET mykey "Hello"
"OK"
redis> EXPIRE mykey 10
(integer) 1
redis> TTL mykey
(integer) 10

注意:

EXPIRE 和 TTL 命令都有对应的⽀持毫秒为单位的版本:PEXPIRE 和 PTTL,使用方法两者相同

关于键过期机制,可以参考下图所示:

TYPE

返回 key 对应的数据类型。

语法:

clike 复制代码
TYPE key

时间复杂度:O(1)

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

示例:

clike 复制代码
redis> SET key1 "value"
"OK"
redis> LPUSH key2 "value"
(integer) 1
redis> SADD key3 "value"
(integer) 1
redis> TYPE key1
"string"
redis> TYPE key2
"list"
redis> TYPE key3
"set"

2、数据结构和内部编码

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

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

数据结构 内部编码
string raw ,int , embstr
hash hashtable , ziplist
list linkedlist , ziplist
set hashtable , inset
zset skiplist ,ziplist

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

clike 复制代码
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> lpush mylist a b c
(integer) 3
127.0.0.1:6379> object encoding hello
"embstr"
127.0.0.1:6379> object encoding mylist
"quicklist

可以看到 hello 对应值的内部编码是 embstr,键 mylist 对应值的内部编码是 ziplist。

Redis 这样设计有两个好处: 1)可以改进内部编码,而对外的数据结构和命令没有任何影响,这样一旦开发出更优秀的内部编码, 无需改动外部数据结构和命令,例如 Redis 3.2 提供了 quicklist,结合了 ziplist 和 linkedlist 两者的优势,为列表类型提供了⼀种更为优秀的内部编码实现,而对用户来说基本⽆感知。

2)多种内部编码实现可以在不同场景下发挥各自的优势,例如 ziplist 比较节省内存,但是在列表元素比较多的情况下,性能会下降,这时候 Redis 会根据配置选项将列表类型的内部实现转换为linkedlist,整个过程用户同样无感知。

3. 单线程架构

Redis 使用了单线程架构来实现⾼性能的内存数据库服务,本节⾸先通过多个客户端命令调用的例子说明 Redis 单线程命令处理机制,接着分析 Redis 单线程模型为什么性能如此之高,最终给出为什么理解单线程模型是使用和运维 Redis 的关键。

1 . 引出单线程模型

现在开启了三个 redis-cli 客户端同时执行命令。

客⼾端 1 设置⼀个字符串键值对:

clike 复制代码
127.0.0.1:6379> set hello world

客⼾端 2 对 counter 做自增操作:

clike 复制代码
127.0.0.1:6379> incr counter

客⼾端 3 对 counter 做自增操作:

clike 复制代码
127.0.0.1:6379> incr counter

我们已经知道从客户端发送的命令经历了:发送命令、执行命令、返回结果三个阶段,其中我们 重点关注第 2 步。我们所谓的 Redis 是采⽤单线程模型执⾏命令的是指:虽然三个客户端看起来是同 时要求 Redis 去执行命令的,但微观角度,这些命令还是采用线性⽅式去执⾏的,只是原则上命令的 执行顺序是不确定的,但⼀定不会有两条命令被同步执行,如下图所示,可以想象 Redis内部只有⼀个服务窗⼝,多个客⼾端按照它们达到的先后顺序被排队在窗口前,依次接受 Redis 的服务,所以两条 incr 命令无论执行顺序,结果⼀定是 2,不会发生并发问题,这个就是 Redis 的单线程执行模型



  1. 为什么单线程还能这么快

通常来讲,单线程处理能力要比多线程差,例如有 10 000 公⽄货物,每辆⻋的运载能⼒是每次200 公斤,那么要 50 次才能完成;但是如果有 50 辆⻋,只要安排合理,只需要依次就可以完成任 务。那么为什么 Redis 使⽤单线程模型会达到每秒万级别的处理能⼒呢?可以将其归结为三点:

a. 纯内存访问。Redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,这是 Redis 达到每秒万级别访问的重要基础。

b. 非阻塞 IO。Redis 使⽤ epoll 作为 I/O 多路复用技术的实现,再加上 Redis 自身的事件处理模型 将 epoll 中的连接、读写、关闭都转换为事件,不在网络 I/O 上浪费过多的时间。

c. 单线程避免了线程切换和竞态产生的消耗。单线程可以简化数据结构和算法的实现,让程序模 型更简单;其次多线程避免了在线程竞争同⼀份共享数据时带来的切换和等待消耗。

Redis 使用 I/O 多路复用模型:

虽然单线程给 Redis 带来很多好处,但还是有⼀个致命的问题:对于单个命令的执行时间都是有 要求的。如果某个命令执行过长,会导致其他命令全部处于等待队列中,迟迟等不到响应,造成客户端的阻塞,对于 Redis 这种高性能的服务来说是非常严重的,所以 Redis 是面向快速执行场景的数据库。

二、String 字符串

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

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

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

字符串数据类型

1. 常见命令

SET

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

语法:

clike 复制代码
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]

时间复杂度: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)。

示例:

clike 复制代码
redis> EXISTS mykey
(integer) 0
redis> SET mykey "Hello"
OK
redis> GET mykey
"Hello"
redis> SET mykey "World" NX
(nil)
redis> DEL mykey
(integer) 1
redis> EXISTS mykey
(integer) 0
redis> SET mykey "World" XX
(nil)
redis> GET mykey
(nil)
redis> SET mykey "World" NX
OK
redis> GET mykey
"World"
redis> SET mykey "Will expire in 10s" EX 10
OK
redis> GET mykey
"Will expire in 10s"
redis> GET mykey # 10秒之后
(nil)

GET

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

语法:

clike 复制代码
GET key

时间复杂度:O(1)

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

示例:

clike 复制代码
redis> GET nonexisting
(nil)
redis> SET mykey "Hello"
"OK"
redis> GET mykey
"Hello"
redis> DEL mykey
(integer) 1
redis> EXISTS mykey
(integer) 0
redis> HSET mykey name Bob
(integer) 1
redis> GET mykey
(error) WRONGTYPE Operation against a key holding the wrong kind of value

MGET

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

语法:

clike 复制代码
MGET key [key ...]

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

返回值:对应 value 的列表

示例:

clike 复制代码
redis> SET key1 "Hello"
"OK"
redis> SET key2 "World"
"OK"
redis> MGET key1 key2 nonexisting
1) "Hello"
2) "World"
3) (nil)

MSET

⼀次性设置多个 key 的值。

语法:

clike 复制代码
 MSET key value [key value ...]

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

返回值:永远是 OK

示例:

clike 复制代码
redis> MSET key1 "Hello" key2 "World"
"OK"
redis> GET key1
"Hello"
redis> GET key2
"World

多次 get vs 单次 mget见下图:

使用 mget / mset 由于可以有效地减少了⽹络时间,所以性能相较更高。假设网络耗 时 1 毫秒,命令执行时间耗时 0.1 毫秒,则执行时间如下表所示。

操作 时间
1000次get 1000 x 1 + 1000 x 0.1 = 1100 毫秒
1 次 mget 1000 个键 1 x 1 + 1000 x 0.1 = 101 毫秒

学会使用批量操作,可以有效提高业务处理效率,但是要注意,每次批量操作所发送的键的数量也不是无节制的,否则可能造成单⼀命令执行时间过长,导致 Redis 阻塞。

SETNX

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

语法:

clike 复制代码
SETNX key value

时间复杂度:O(1)

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

示例:

clike 复制代码
redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"

SET、SETNX 和 SETXX 的执行流程如下图。

2. 计数命令

INCR

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

语法:

clike 复制代码
INCR key

时间复杂度:O(1)

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

clike 复制代码
redis> EXISTS mykey
(integer) 0
redis> INCR mykey
(integer) 1
redis> SET mykey "10"
"OK"
redis> INCR mykey
(integer) 11
redis> SET mykey "234293482390480948029348230948"
"OK"
redis> INCR mykey
(error) value is not an integer or out of range
redis> SET mykey 'not a number'
"OK"
redis> INCR mykey
(error) value is not an integer or out of range

INCRBY

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

语法:

clike 复制代码
INCRBY key decrement

时间复杂度:O(1)

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

示例:

clike 复制代码
redis> EXISTS mykey
(integer) 0
redis> INCRBY mykey 3
(integer) 3
redis> SET mykey "10"
"OK"
redis> INCRBY mykey 3
(integer) 13
redis> INCRBY mykey "not a number"
(error) ERR value is not an integer or out of range
redis> SET mykey "234293482390480948029348230948"
"OK"
redis> INCRBY mykey 3
(error) value is not an integer or out of range
redis> SET mykey 'not a number'
"OK"
redis> INCRBY mykey 3
(error) value is not an integer or out of range

DECR

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

语法:

clike 复制代码
DECR key

时间复杂度:O(1)

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

示例:

clike 复制代码
redis> EXISTS mykey
(integer) 0
redis> DECR mykey
(integer) -1
redis> SET mykey "10"
"OK"
redis> DECR mykey
(integer) 9
redis> SET mykey "234293482390480948029348230948"
"OK"
redis> DECR mykey
(error) value is not an integer or out of range
redis> SET mykey 'not a number'
"OK"
redis> DECR mykey
(error) value is not an integer or out of range

DECYBY

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

语法:

clike 复制代码
DECRBY key decrement

时间复杂度:O(1)

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

示例:

clike 复制代码
redis> EXISTS mykey
(integer) 0
redis> DECRBY mykey 3
(integer) -3
redis> SET mykey "10"
"OK"
redis> DECRBY mykey 3
(integer) 7
redis> DECRBY mykey "not a number"
(error) ERR value is not an integer or out of range
redis> SET mykey "234293482390480948029348230948"
"OK"
redis> DECRBY mykey 3
(error) value is not an integer or out of range
redis> SET mykey 'not a number'
"OK"
redis> DECRBY mykey 3
(error) value is not an integer or out of range

INCRBYFLOAT

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

语法:

clike 复制代码
INCRBYFLOAT key increment

时间复杂度:O(1)

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

示例:

clike 复制代码
redis> SET mykey 10.50
"OK"
redis> INCRBYFLOAT mykey 0.1
"10.6"
redis> INCRBYFLOAT mykey -5
"5.6"
redis> SET mykey 5.0e3
"OK"
redis> INCRBYFLOAT mykey 2.0e2
"5200"

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

3.其他命令

APPEND

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

语法:

clike 复制代码
APPEND KEY VALUE

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

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

示例:

clike 复制代码
redis> EXISTS mykey
(integer) 0
redis> APPEND mykey "Hello"
(integer) 5
redis> GET mykey
"Hello"
redis> APPEND mykey " World"
(integer) 11
redis> GET mykey
"Hello World"

GETRANGE

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

语法:

clike 复制代码
GETRANGE key start end

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

返回值:string 类型的子串

示例:

clike 复制代码
redis> SET mykey "This is a string"
"OK"
redis> GETRANGE mykey 0 3
"This"
redis> GETRANGE mykey -3 -1
"ing"
redis> GETRANGE mykey 0 -1
"This is a string"
redis> GETRANGE mykey 10 100
"string"

SETRANGE

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

语法:

clike 复制代码
SETRANGE key offset value

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

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

示例:

clike 复制代码
redis> SET key1 "Hello World"
"OK"
redis> SETRANGE key1 6 "Redis"
(integer) 11
redis> GET key1
"Hello Redis"

STRLEN

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

语法:

clike 复制代码
STRLEN key

时间复杂度:O(1)

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

示例:

clike 复制代码
redis> SET mykey "Hello world"
"OK"
redis> STRLEN mykey
(integer) 11
redis> STRLEN nonexisting
(integer) 0

4. 内部编码

字符串类型的内部编码有 3 种:

• int:8 个字节的长整型。

• embstr:小于等于 39 个字节的字符串。

• raw:大于 39 个字节的字符串。

Redis 会根据当前值的类型和⻓度动态决定使⽤哪种内部编码实现。

整型类型示例如下:

clike 复制代码
127.0.0.1:6379> set key 6379
OK
127.0.0.1:6379> object encoding key
"int"

短字符串示例如下:

clike 复制代码
// ⼩于等于 39 个字节的字符串
127.0.0.1:6379> set key "hello"
OK
127.0.0.1:6379> object encoding key
"embstr"

长字符串示例如下:

clike 复制代码
// ⼤于 39 个字节的字符串
127.0.0.1:6379> set key "one string greater than 39 bytes ........"
OK
127.0.0.1:6379> object encoding key
"raw"

5. 典型使用场景

缓存(Cache)功能

下图 是比较典型的缓存使⽤场景,其中 Redis 作为缓冲层,MySQL 作为存储层,绝大部分请求的数据都是从 Redis 中获取。由于 Redis 具有⽀撑⾼并发的特性,所以缓存通常能起到加速读写和 降低后端压力的作用

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

下面我们通过伪代码大概模拟业务数据访问教程:

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

clike 复制代码
UserInfo getUserInfo(long uid) {
 ...
}

2)首先从 Redis 获取⽤⼾信息,我们假设⽤⼾信息保存在 "user:info:" 对应的键中:

clike 复制代码
// 根据 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 中获取对应的信息,随后写 ⼊缓存并返回

clike 复制代码
// 如果缓存未命中(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 的访问数。

与 MySQL 等关系型数据库不同的是,Redis 没有表、字段这种命名空间,⽽且也没有对键名 有强制要求(除了不能使⽤⼀些特殊字符)。但设计合理的键名,有利于防⽌键冲突和项⽬ 的可维护性,⽐较推荐的⽅式是使⽤ "业务名:对象名:唯⼀标识:属性" 作为键名。例如MySQL 的数据库名为 vs,⽤⼾表名为 user_info,那么对应的键可以使⽤"vs:user_info:6379"、"vs:user_info:6379:name" 来表⽰,如果当前 Redis 只会被⼀个业务使⽤,可以省略业务名 "vs:"。如果键名过程,则可以使⽤团队内部都认同的缩写替代,例如"user:6379:friends:messages:5217" 可以被 "u:6379🇫🇷m:5217" 代替。毕竟键名过⻓,还 是会导致 Redis 的性能明显下降的。

计数(Counter)功能

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

图 2-11 记录视频播放次数

clike 复制代码
// 在 Redis 中统计某视频的播放次数
long incrVideoCounter(long vid) {
 key = "video:" + vid;
 long count = Redis 执⾏命令:incr key
 return counter;
}

实际中要开发⼀个成熟、稳定的真实计数系统,要⾯临的挑战远不⽌如此简单:防作弊、按 照不同维度计数、避免单点问题、数据持久化到底层数据源等。

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

图 2-12 Session 分散存储

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

Redis 集中管理 Session

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

短信验证码

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

clike 复制代码
 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;
        }
    }

以上介绍了使用 Redis 的字符串数据类型可以使用的几个场景,但其适用场景远不止于此,开发人员可以结合字符串类型的特点以及提供的命令,充分发挥自己的想象力,在自己的业务中去找到合适的场景去使用 Redis 的字符串类型

三、Hash哈希

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

字符串和哈希类型对比

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

1. 命令

HSET

设置 hash 中指定的字段(field)的值(value)。

语法:

clike 复制代码
 HSET key field value [field value ...]

时间复杂度:插⼊⼀组 field 为 O(1), 插⼊ N 组 field 为 O(N)

返回值:添加的字段的个数。

示例:

clike 复制代码
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HGET myhash field1
"Hello

HGET

获取 hash 中指定字段的值。

语法:

clike 复制代码
HGET key field

时间复杂度:O(1)

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

示例:

clike 复制代码
redis> HSET myhash field1 "foo"
(integer) 1
redis> HGET myhash field1
"foo"
redis> HGET myhash field2
(nil)

HEXISTS

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

语法:

clike 复制代码
 HEXISTS key field

时间复杂度:O(1)

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

示例:

clike 复制代码
redis> HSET myhash field1 "foo"
(integer) 1
redis> HEXISTS myhash field1
(integer) 1
redis> HEXISTS myhash field2
(integer) 0

HDEL

删除 hash 中指定的字段。

语法:

clike 复制代码
HDEL key field [field ...]

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

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

示例:

clike 复制代码
redis> HSET myhash field1 "foo"
(integer) 1
redis> HDEL myhash field1
(integer) 1
redis> HDEL myhash field2
(integer) 0

HKEYS

获取 hash 中的所有字段

语法:

clike 复制代码
HKEYS key

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

返回值:字段列表。

示例:

clike 复制代码
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HKEYS myhash
1) "field1"
2) "field2"

HVALS

获取 hash 中的所有的值。

语法:

clike 复制代码
HVALS key

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

返回值:所有的值。

示例:

clike 复制代码
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HVALS myhash
1) "Hello"
2) "World"

HGETALL

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

语法:

clike 复制代码
 HGETALL key

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

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

示例:

clike 复制代码
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HGETALL myhash
1) "field1"
2) "Hello"
3) "field2"
4) "World"

HMGET

⼀次获取 hash 中多个字段的值。

语法:

clike 复制代码
HMGET key field [field ...]

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

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

示例:

clike 复制代码
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HMGET myhash field1 field2 nofield
1) "Hello"
2) "World"
3) (nil)

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

HLEN

获取 hash 中的所有字段的个数。

语法:

clike 复制代码
HLEN key

时间复杂度:O(1)

返回值:字段个数。

示例:

clike 复制代码
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HLEN myhash
(integer) 2

HSETNX

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

语法:

clike 复制代码
HSETNX key field value

时间复杂度:O(1)

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

示例:

clike 复制代码
redis> HSETNX myhash field "Hello"
(integer) 1
redis> HSETNX myhash field "World"
(integer) 0
redis> HGET myhash field
"Hello"

HINCRBY

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

语法:

clike 复制代码
HINCRBY key field increment

时间复杂度:O(1)

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

示例:

clike 复制代码
redis> HSET myhash field 5
(integer) 1
redis> HINCRBY myhash field 1
(integer) 6
redis> HINCRBY myhash field -1
(integer) 5
redis> HINCRBY myhash field -10
(integer) -5

HINCRBYFLOAT

HINCRBY 的浮点数版本。

语法:

clike 复制代码
HINCRBYFLOAT key field increment

时间复杂度:O(1)

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

示例:

clike 复制代码
redis> HSET mykey field 10.50
(integer) 1
redis> HINCRBYFLOAT mykey field 0.1
"10.6"
redis> HINCRBYFLOAT mykey field -5
"5.6"
redis> HSET mykey field 5.0e3
(integer) 0
redis> HINCRBYFLOAT mykey field 2.0e2
"5200"

2.内部编码

哈希的内部编码有两种:

• 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

clike 复制代码
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:

clike 复制代码
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:

clike 复制代码
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"

3. 使用场景

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


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

clike 复制代码
    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

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

关系型数据库稀疏性

4. 缓存方式对比

截至目前为止,我们已经能够用三种方法缓存用户信息,下⾯给出三种方案的实现方法和优缺点分析

  1. 原⽣字符串类型⸺使用字符串类型,每个属性⼀个键:
clike 复制代码
set user:1:name James
set user:1:age 23
set user:1:city Beijing

优点:实现简单,针对个别属性变更也很灵活。

缺点:占用过多的键,内存占用量较⼤,同时⽤⼾信息在 Redis 中比较分散,缺少内聚性,所以这种方案基本没有实用性

  1. 序列化字符串类型,例如 JSON 格式
clike 复制代码
set user:1 

优点:针对总是以整体作为操作的信息比较合适,编程也简单。同时,如果序列化⽅案选择合适,内存的使用效率很高。

缺点:本身序列化和反序列需要⼀定开销,同时如果总是操作个别属性则非常不灵活。

  1. 哈希类型
clike 复制代码
hmset user:1 name James age 23 city Beijing

优点:简单、直观、灵活。尤其是针对信息的局部变更或者获取操作。

缺点:需要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,可能会造成内存的较大消耗。

结语

本文主要介绍了Redis中的全局命令、内部编码、单线程架构,以及String数据结构类型的具体使用和hash的具体使用。

以上就是本文全部内容,感谢各位能够看到最后,如有问题,欢迎各位大佬在评论区指正,希望大家可以有所收获!创作不易,希望大家多多支持!

最后,大家再见!祝好!我们下期见!

相关推荐
RestCloud1 分钟前
如何通过ETLCloud实现跨系统数据同步?
数据库·数据仓库·mysql·etl·数据处理·数据同步·集成平台
你是狒狒吗2 分钟前
TM中,return new TransactionManagerImpl(raf, fc);为什么返回是new了一个新的实例
java·开发语言·数据库
懒羊羊大王呀5 分钟前
Ubuntu20.04中 Redis 的安装和配置
linux·redis
iceslime8 分钟前
旅行商问题(TSP)的 C++ 动态规划解法教学攻略
数据结构·c++·算法·算法设计与分析
Channing Lewis1 小时前
sql server如何创建表导入excel的数据
数据库·oracle·excel
秃头摸鱼侠1 小时前
MySQL安装与配置
数据库·mysql·adb
UGOTNOSHOT1 小时前
每日八股文6.3
数据库·sql
行云流水行云流水2 小时前
数据库、数据仓库、数据中台、数据湖相关概念
数据库·数据仓库
John Song2 小时前
Redis 集群批量删除key报错 CROSSSLOT Keys in request don‘t hash to the same slot
数据库·redis·哈希算法
IvanCodes2 小时前
七、Sqoop Job:简化与自动化数据迁移任务及免密执行
大数据·数据库·hadoop·sqoop