Skip to content

API基础

Redis 的命令有上百个,如果纯靠死记硬背比较困难,但是如果理解 Redis 的一些机制,会发现这些命令有很强的通用性。
Redis 不是万金油,有些数据结构和命令必须在特定场景下使用,一旦使用不当可能对 Redis 本身或者应用本身造成致命伤害

全局命令

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

查看所有键:

1
keys *
1
2
127.0.0.1:6379> keys *
(empty list or set)
1
2
3
4
5
6
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> set java jedis
OK
127.0.0.1:6379> set python redis-py
OK
1
2
3
4
127.0.0.1:6379> keys *
1) "python"
2) "java"
3) "hello"

键总数

dbsize

1
2
127.0.0.1:6379> rpush mylist a b c d e f g
(integer) 7

dbsize 命令会返回当前数据库中键的总数。
例如当前数据库有 4 个键,分别是 hello、java、python、mylist,所以 dbsize 的结果是 4

1
2
127.0.0.1:6379> dbsize
(integer) 4

检查键是否存在

exists key

如果键存在则返回 1,不存在则返回 0

1
2
3
4
127.0.0.1:6379> exists java
(integer) 1
127.0.0.1:6379> exists not_exist_key
(integer) 0

删除键

del key [key ...]

del 是一个通用命令,无论值是什么数据结构类型,del 命令都可以将其删除,例如下面将字符串类型的键 java 和列表类型的键 mylist 分别删除

1
2
3
4
5
6
127.0.0.1:6379> exists java
(integer) 1
127.0.0.1:6379> del java
(integer) 1
127.0.0.1:6379> exists java
(integer) 0
1
2
3
4
5
6
127.0.0.1:6379> exists mylist
(integer) 1
127.0.0.1:6379> del mylist
(integer) 1
127.0.0.1:6379> exists mylist
(integer) 0

返回结果为成功删除键的个数,假设删除一个不存在的键,就会返回 0:

1
2
127.0.0.1:6379> del not_exist_key
(integer) 0

同时 del 命令可以支持删除多个键

1
2
3
4
5
6
7
8
127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> set b 2
OK
127.0.0.1:6379> set c 3
OK
127.0.0.1:6379> del a b c
(integer) 3

键过期

expire key seconds

Redis 支持对键添加过期时间,当超过过期时间后,会自动删除键,例如为键 hello 设置了 10 秒过期时间:

1
2
3
4
5
6
7
8
127.0.0.1:6379> expire hello 10
(integer) 1
127.0.0.1:6379> ttl hello
(integer) 2
127.0.0.1:6379> ttl hello
(integer) -2
127.0.0.1:6379> get hello
(nil)

ttl 命令会返回键的剩余过期时间,它有 3 种返回值:

  • 大于等于 0 的整数: 键剩余的过期时间
  • -1: 键没设置过期时间
  • -2: 键不存在

键的数据结构类型

type key

例如键 hello 是字符串类型,返回结果为 string。
键 mylist 是列表类型,返回结果为 list

1
2
3
4
5
6
7
8
127.0.0.1:6379> set a b
OK
127.0.0.1:6379> type a
string
127.0.0.1:6379> rpush mylist a b c d e f g
(integer) 7
127.0.0.1:6379> type mylist
list

如果键不存在,则返回 none:

1
2
127.0.0.1:6379> type not_exist_key
none

数据结构和内部编码

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

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

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

1
2
3
4
5
6
7
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> object encoding hello
"embstr"

127.0.0.1:6379> object encoding mylist
"quicklist"

Redis 这样设计有两个好处: 第一,可以改进内部编码,而对外的数据结构和命令都没有影响,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令。
第二,多种内部编码实现可以在不同场景下发挥各自的优势,例如 ziplist 比较节省内存,但是在列表元素比较多的情况下,性能会有所下降,这时候 Redis 会根据配置选项将列表类型的内部实现转换为 linkedlist

单线程架构

Redis 使用了单线程架构和 I/O 多路复用模型来实现高性能的内存数据库服务。

引出单线程模型

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

客户端 1 设置一个字符串键值对:

1
127.0.0.1:6379> set hello world

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

1
127.0.0.1:6379> incr counter

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

1
127.0.0.1:6379> incr counter

Redis 客户端与服务器的模型可以简化成下图,每次客户端调用都经历了发送命令、执行命令、返回结果三个过程

其中第 2 步是重点要讨论的,因为 Redis 是单线程来处理命令的,所以一条命令从客户端达到服务端不会立刻被执行,所有命令都会进入一个队列中,然后逐个被执行。
所以上面 3 个客户端命令的执行顺序是不确定的,但是可以确定不会有两条命令被同时执行。
所以两条 incr 命令无论怎么执行最终结果都是 2,不会产生并发问题,这就是 Redis 单线程的基本模型。
但是像发送命令、返回结果、命令排队肯定不像描述的这么简单,Redis 使用了 I/O 多路复用技术来解决 I/O 的问题。

为什么单线程还能这么快

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

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

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

第三,单线程避免了线程切换和竞态产生的消耗。

既然采用单线程就能达到如此高的性能,那么也不失为一种不错的选择,因为单线程能带来几个好处:
第一,单线程可以简化数据结构和算法的实现。如果对高级编程语言熟悉的读者应该了解并发数据结构实现不但困难而且开发测试比较麻烦。
第二,单线程避免了线程切换和竞态产生的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。

但是单线程会有一个问题: 对于每个命令的执行时间是有要求的。
如果某个命令执行过长,会造成其他命令的阻塞,对于 Redis 这种高性能的服务来说是致命的,所以 Redis 是面向快速执行场景的数据库。

单线程机制很容易被初学者忽视,但 Redis 单线程机制是开发和运维人员使用和理解 Redis 的核心之一