目录
- 1. Redis的特性
- 2. Redis的使用场景
- 2.1 Redis可以做什么?
- 2.2 Redis不可以做什么
- 3. Redis的安装和启动
- 4. Redis的基本全局命令
- 4.1 GET和SET命令
- 4.2 KEYS指令
- 4.3 EXISTS指令
- 4.4 DEL指令
- 4.5 EXPIRE指令
- 4.6 TTL指令
- 4.7 TYPE指令
- 5. 数据类型和内部编码
- 6. 单线程架构
- 6.1 引出单线程模型
- 6.2 为什么单线程还能这么快
1. Redis的特性
Redis 之所以受到如此多公司的⻘睐,必然有之过人之处,下面是关于 Redis 的 8 个重要特性:
(1)速度快:正常情况下,Redis 执行命令的速度⾮常快,官方给出的数字是读写性能可以达到 10 万 / 秒,当然这也取决于机器的性能,但这里先不讨论机器性能上的差异,只分析⼀下是什么造就了 Redis 如此之快,可以大概归纳为以下四点:
- Redis 的所有数据都是存放在内存中的,下表是谷歌公司 2009 年给出的各层级硬件执行速度,所以把数据放在内存中是 Redis 速度快的最主要原因。
- Redis 是用 C 语言实现的,⼀般来说 C 语言实现的程序 “距离” 操作系统更近,执行速度相对会更快。
- Redis 使用了单线程,预防了多线程可能产生的竞争问题。
- 作者对于 Redis 源代码可以说是精打细磨,曾经有人评价 Redis 是少有的集性能和优雅于一身的开源代码。
下表是谷歌公司给出的各层级硬件执行速度:
层级 | 速度 |
---|---|
L1 cache reference | 0.5 ns |
Branch mispredict | 5 ns |
L2 cache reference | 7 ns |
Mutex lock/unlock | 25 ns |
Main memory reference | 100 ns |
Compress 1 K bytes with Zippy | 3 000 ns |
Send 2 K bytes over 1 Gbps network | 20 000 ns |
Read 1 MB sequentially from Memory | 250 000 ns |
Round trip within same datacenter | 500 000 ns |
Disk seek | 10 000 000 ns |
Read 1 MB sequentially from disk | 20 000 000 ns |
Send packet CA -> Netherlands -> CA | 150 000 000 ns |
(2)基于键值对的数据结构服务器:
- 几乎所有的编程语言都提供了类似字典的功能,例如 C++ 里的 map、Java 里的 map、Python 里的 dict 等,类似于这种组织数据的方式叫做基于键值对的方式,与很多键值对数据库不同的是,Redis 中的值不仅可以是字符串,而且还可以是具体的数据结构,这样不仅能便于在许多应用场景的开发,同时也能提高开发效率。
- Redis 的全称是 REmote Dictionary Server,它主要提供了 5 种数据结构:字符串(string)、哈希(hash)、列表(list)、集合(set)、有序集合(ordered set /zet),同时在字符串的基础之上演变出了位图(Bitmaps)和 HyperLogLog 两种神奇的 ”数据结构“,并且随着 LBS(Location Based Service,基于位置服务)的不断发展,Redis 3.2. 版本种加⼊有关 GEO(地理信息定位)的功能,总之在这些数据结构的帮助下,开发者可以开发出各种 “有意思” 的应用。
(3)丰富的功能。除了 5 种数据结构,Redis 还提供了许多额外的功能:
- 提供了键过期功能,可以用来实现缓存。
- 提供了发布订阅功能,可以用来实现消息系统。
- 支持 Lua 脚本功能,可以利用 Lua 创造出新的 Redis 命令。
- 提供了简单的事务功能,能在⼀定程度上保证事务特性。
- 提供了流水线(Pipeline)功能,这样客户端能将⼀批命令⼀次性传到Redis,减少了网络的开销。
(4)简单稳定:
- Redis 的简单主要表现在三个方面。首先Redis 的源码很少,早期版本的代码只有 2 万行左右,3.0 版本以后由于添加了集群特性,代码增至 5 万行左右,相对于很多 NoSQL 数据库来说代码量相对要少很多,也就意味着普通的开发和运维人员完全可以 “吃透” 它。
- 其次,Redis 使用单线程模型,这样不仅使得 Redis 服务端处理模型变得简单,而且也使得客户端开发变得简单。最后,Redis 不需要依赖于操作系统中的类库(例如 Memcache 需要依赖 libevent 这样的系统类库),Redis 自己实现了事件处理的相关功能。
- 但与简单相对的是 Redis 具备相当的稳定性,在大量使用过程中,很少出现因为 Redis 自身 BUG而导致宕掉的情况。
(5)客户端语言多:
- Redis 提供了简单的 TCP 通信协议,很多编程语言可以很方便地接入到 Redis,并且由于 Redis 受到社区和各大公司的广泛认可,所以支持 Redis 的客户端语言也非常多,几乎涵盖了主流的编程语言,例如 C、C++、Java、PHP、Python、NodeJS 等,后续我们会对 Redis 的客户端使用做详细说
明。
(6)持久化(Persistence):
- 通常看,将数据放在内存中是不安全的,⼀旦发生断电或者机器故障,重要的数据可能就会丢失,因此 Redis 提供了两种持久化方式:RDB 和 AOF,即可以用两种策略将内存的数据保存到硬盘中。这样就保证了数据的可持久性,后续我们将对 Redis 的持久化进行详细说明。
(7)主从复制(Replication):
- Redis 提供了复制功能,实现了多个相同数据的 Redis 副本(Replica)。复制功能是分布式 Redis 的基础。
(8)高可用(High Availability)和分布式(Distributed):
- Redis 提供了高可用实现的 Redis 哨兵(Redis Sentinel),能够保证 Redis 结点的故障发现和故障自动转移。也提供了 Redis 集群(Redis Cluster),是真正的分布式实现,提供了高可用、读写和容量的扩展性。
2. Redis的使用场景
2.1 Redis可以做什么?
要充分理解 Redis 的作用,我们需要对网站的架构有⼀定的基础理解,考虑可能会缺少这方面的背景知识,建议先观看《服务端高并发分布式结构演进之路》这篇博客来对该领域的基础知识做⼀定学习后,再度继续本篇的学习。
(1)缓存(Cache):
- 缓存机制几乎在所有大型网站都有使用,合理地使用缓存不仅可以加速数据的访问速度,而且能够有效地降低后端数据源的压力。
- Redis 提供了键值过期时间设置,并且也提供了灵活控制最大内存和内存溢出后的淘汰策略。可以这么说,⼀个合理的缓存设计能够为⼀个网站的稳定保驾护航。
(2)排行榜系统:
- 排行榜系统几乎存在于所有的网站,例如按照热度排名的排行榜,按照发布时间的排行榜,按照各种复杂维度计算出的排行榜,Redis 提供了列表和有序集合的结构,合理地使用这些数据结构可以很⽅便地构建各种排行榜系统。
(3)计数器应用:
- 计数器在网站中的作用至关重要,例如视频网站有播放数、电商网站有浏览数,为了保证数据的实时性,每⼀次播放和浏览都要做加 1 的操作,如果并发量很大对于传统关系型数据的性能是⼀种挑战。Redis 天然支持计数功能而且计数的性能也非常好,可以说是计数器系统的重要选择。
(4)社交网络:
- 赞 / 踩、粉丝、共同好友 / 喜好、推送、下拉刷新等是社交网站的必备功能,由于社交网站访问量通常比较大,而且传统的关系型数据不太合适保存这种类型的数据,Redis 提供的数据结构可以相对比较容易地实现这些功能。
(5)消息队列系统:
- 消息队列系统可以说是⼀个大型网站的必备基础组件,因为其具有业务解耦、⾮实时业务削峰等特性。Redis 提供了发布订阅功能和阻塞队列的功能,虽然和专业的消息队列比还不够足够强大,但是对于⼀般的消息队列功能基本可以满⾜。
2.2 Redis不可以做什么
- 实际上和任何⼀恶魔你技术⼀样,每个技术都有自己的应用场景和边界,也就是说 Redis 并不是万金油,有很多合适它解决的问题,但是也有很多不合适它解决的问题。我们可以站在数据规模和数据冷热的角度来进行分析。
- 站在数据规模的角度看,数据可以分为大规模数据和小规模数据,我们知道 Redis 的数据是存放在内存中的,虽然现在内存已经足够便宜,但是如果数据量非常大,例如每天有几亿的用户行为数据,使用 Redis 来存储的话,基本上是个无底洞,经济成本相当⾼。
- 站在数据冷热的角度,数据分为热数据和冷数据,热数据通常是指需要频繁操作的数据,反之为冷数据,例如对于视频网站来说,视频基本信息基本上在各个业务线都是经常要操作的数据,而用户的观看记录不⼀定是经常需要访问的数据,这里暂且不讨论两者数据规模的差异,单纯站在数据冷热的角度上看,视频信息属于热数据,用户观看记录属于冷数据。如果将这些冷数据放在 Redis 上,基本上是对于内存的⼀种浪费,但是对于⼀些热数据可以放在 Redis 中加速读写,也可以减轻后端存储的负载,可以说是事半功倍。
- 所以,Redis 并不是万⾦油,相信随着我们对 Redis 的逐步学习,能够清楚 Redis 真正的使用场景。
3. Redis的安装和启动
(1)使用 apt 安装:
Shell
sudo apt install redis -y
(2)支持远程连接:
- 修改 /etc/redis/redis.conf
- 修改 bind 127.0.0.1 为 bind 0.0.0.0
- 修改 protected-mode yes 改为 protected-mode no
Plain Text
# By default, if no "bind" configuration directive is specified, Redis listens
# for connections from all the network interfaces available on the server.
# It is possible to listen to just one or multiple selected interfaces using
# the "bind" configuration directive, followed by one or more IP addresses.
#
# Examples:
#
# bind 192.168.1.100 10.0.0.1
# bind 127.0.0.1 ::1
#
# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the
# internet, binding to all the interfaces is dangerous and will expose the
# instance to everybody on the internet. So by default we uncomment the
# following bind directive, that will force Redis to listen only into
# the IPv4 loopback interface address (this means Redis will be able to
# accept connections only from clients running into the same computer it
# is running).
#
# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
# JUST COMMENT THE FOLLOWING LINE.
#
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# bind 127.0.0.1 # 注释掉这行
bind 0.0.0.0 # 添加这行
protected-mode no # 把 yes 改成 no
(3)Redis的相关服务:
- 启动Redis服务
Plain Text
sudo service redis-server start
- 停止Redis服务
Plain Text
sudo service redis-server stop
- 重启Redis服务
Plain Text
sudo service redis-server restart
(4)持久化文件存储目录:
/var/lib/redis/
Redis 持久化生产的 RDB 和 AOF 文件都默认生成于该目录下。
(5)日志文件目录:
/var/log/redis/
/var/log/redis/ 目录下会保存 Redis 运行期间生产的日志文件,默认按照天进行分割,并且会将⼀定日期的日子文件使用 gzip 格式压缩保存。可以使用任意文本编辑器打开。
(6)Redis 命令行客户端:
- 现在我们已经启动了 Redis 服务,下面将介绍如何使用 redis-cli 连接、操作 Redis 服务。客户端和服务端的交互过程如下图所示。
(7)redis-cli 可以使用两种方式连接 Redis 服务器。
- 第⼀种是交互式方式:通过 redis-cli -h { host } -p { port } 的⽅式连接到 Redis 服务,后续所有的操作都是通过交互式的方式实现,不需要再执行 redis-cli 了,例如:
[root@host ~]# redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> set key hello
OK
127.0.0.1:6379> get key
"hello"
- 第⼆种是命令方式:用redis-cli -h { host } -p { port } { command } 就可以直接得到命令的返回结果,例如:
[root@host ~]# redis-cli -h 127.0.0.1 -p 6379 ping
PONG
[root@host ~]# redis-cli -h 127.0.0.1 -p 6379 set key hello
OK
[root@host ~]# redis-cli -h 127.0.0.1 -p 6379 get key
"hello"
注意:
- 由于我们连接的 Redis 服务位于 127.0.0.1,端口也使用的是默认的 6379
端口,所以可以省略 -h { host } -p { port }。 - redis-cli是学习 Redis 的重要工具,后续的大量章节都是用它来做讲解。
4. Redis的基本全局命令
4.1 GET和SET命令
(1)SET 命令用于向 Redis 存储一个键值对。语法如下:
SET key value
- key:字符串,Redis 键的名称。
- value:字符串,要存储的值。
示例:
SET mykey "Hello, Redis!"
- 这个命令会将字符串 “Hello, Redis!” 存储在键 mykey 中。
(2)SET 命令支持多种选项来影响它的行为:
-
EX seconds⸺使用秒作为单位设置 key 的过期时间。
-
PX milliseconds⸺使用毫秒作为单位设置 key 的过期时间。
-
NX ⸺只在 key 不存在时才进行设置,即如果 key 之前已经存在,设置不执行。
-
XX ⸺只在 key 存在时才进行设置,即如果 key 之前不存在,设置不执行。
-
注意:由于带选项的 SET 命令可以被 SETNX 、 SETEX 、 PSETEX 等命令代替,所以之后的版本中,Redis可能进行合并,并不添加选项。
(3)GET 命令用于从 Redis 中读取一个键的值。语法如下:
GET key
- key:字符串,Redis 键的名称。
示例:
GET mykey
- 如果键 mykey 存在,这个命令会返回 “Hello, Redis!”;如果键不存在,则返回 (nil)。nil就相当于C++当中的nullptr。
(4)注意事项:
- 数据类型:虽然 SET 和 GET 通常用于处理字符串值,但 Redis 支持多种数据类型(如列表、集合、哈希、有序集合等),这些数据类型有各自的操作命令。
- 过期时间:可以使用 EXPIRE 命令为键设置过期时间,这样在指定时间后键会自动删除。
SET mykey "Hello, Redis!"
EXPIRE mykey 60 # 设置 mykey 60 秒后过期
- 持久化:Redis 支持多种持久化机制(如 RDB 和 AOF),用于在服务器重启后恢复数据。
4.2 KEYS指令
(1)Redis的KEYS指令用于查找所有符合给定模式(pattern)的key。以下是KEYS的语法:
KEYS pattern
- pattern:一个字符串,用于匹配Redis中的key。pattern可以使用通配符 * 和 ?,其中 * 表示0或多个字符,?表示单个字符。
- 返回值:符合给定模式的key列表(Array)。如果没有找到匹配的key,则返回一个空列表。
(2)查找所有key:
KEYS *
- 这个命令会返回Redis中所有的key。
(3)使用?进行匹配:
- 假设存在以下key:word、wood,则KEYS wo?d会返回[“word”, “wood”]。
(4)统配样式如下:
- 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。
(5)注意事项:
- 性能影响:在处理大量数据时,KEYS命令可能会影响Redis服务器的性能,因为它会遍历所有key来寻找匹配的项。因此,在生产环境中,应该谨慎使用KEYS命令,尤其是在数据量巨大的情况下。如果需要查找大量匹配的key,可以考虑使用SCAN命令,它提供了更高效的迭代方式。
- 模式匹配:KEYS命令中的pattern是区分大小写的,因此在匹配时需要特别注意。
- 安全性:不要在生产环境中使用KEYS命令来遍历大量的key,因为这可能会导致Redis服务器阻塞或性能下降。如果需要遍历key,可以考虑使用SCAN命令或其他迭代方法。
4.3 EXISTS指令
(1)Redis的EXISTS指令用于检查给定的key是否存在于数据库中。以下是EXISTS的语法:
EXISTS key [key ...]
- key:一个或多个要检查的key。
- 返回值:整数。如果存在至少一个给定的key,则返回1(表示至少有一个key存在)。如果所有给定的key都不存在,则返回0。
(2)检查单个key是否存在:
EXISTS mykey
- 如果mykey存在,则返回1;如果不存在,则返回0。
(3)检查多个key是否存在:
EXISTS key1 key2 key3
- 如果key1、key2和key3中存在至少一个key,则返回1(不会告诉你具体哪些key存在,只告诉你至少有一个存在)。如果所有这三个key都不存在,则返回0。
(4)注意事项:
- EXISTS指令的时间复杂度是O(1),因为它只需要在Redis的key空间中查找给定的key即可。
- 与KEYS指令不同,EXISTS指令不会遍历所有的key来查找匹配的项,因此它不会对Redis服务器的性能产生显著影响。
- 如果需要检查大量的key是否存在,可以考虑使用批处理(即一次性传递多个key给EXISTS指令)或使用其他数据结构(如集合)来组织key,以便更高效地进行检查。
4.4 DEL指令
(1)Redis的DEL指令用于删除已存在的键(key)。以下是DEL的语法:
DEL key [key ...]
- key:一个或多个要删除的键。
- 返回值:整数。返回被成功删除的键的数量。如果某个键不存在,该键会被忽略,并且不会计入返回值中。
(2)删除单个键:
SET mykey "Hello" # 设置一个键值对
DEL mykey # 删除这个键
- 执行上述命令后,mykey会被删除,并且DEL命令返回1。
(3)删除多个键:
SET key1 "Value1"
SET key2 "Value2"
SET key3 "Value3"
DEL key1 key2 key4 # 尝试删除key1、key2和key4(key4不存在)
- 执行上述命令后,key1和key2会被删除,key4由于不存在而被忽略。因此,DEL命令返回2。
(4)注意事项:
- 如果尝试删除一个不存在的键,DEL指令会忽略该键并继续处理其他键。因此,在执行DEL指令时,不需要先检查键是否存在。
- DEL指令的时间复杂度是O(1),因为它只需要在Redis的key空间中查找并删除给定的键即可。但是,如果同时删除大量的键,可能会对Redis服务器的性能产生一定影响。在这种情况下,可以考虑使用SCAN指令结合循环来分批删除键。
- 当使用DEL指令删除一个带有过期时间的键时,该键的过期时间会被立即移除。即使该键的过期时间还未到达,它也会被立即删除。
4.5 EXPIRE指令
(1)Redis的EXPIRE指令用于为存储在Redis数据库中的键(key)设置过期时间。一旦键的过期时间到达,该键及其关联的值将自动被Redis删除(但请注意,删除操作可能是惰性的或定期的,具体取决于Redis的过期策略)。以下是EXPIRE的语法:
EXPIRE key seconds
- key:要设置过期时间的键。
- seconds:过期时间,以秒为单位。
- 返回值:整数。设置成功返回1。当key不存在或者不能为key设置过期时间时(比如在低于2.1.3版本的Redis中尝试更新key的过期时间),返回0。
(2)示例:
SET mykey "Hello" # 设置一个键值对
EXPIRE mykey 60 # 为这个键设置60秒的过期时间
- 执行上述命令后,mykey会在60秒后自动被删除。如果在这60秒内尝试获取mykey的值,会返回其当前的值(“Hello”)。但是,60秒后如果再次尝试获取mykey的值,会返回nil,表示该键已被删除。
(3)注意事项:
- 在设置过期时间时,需要确保过期时间足够长,以允许客户端在过期之前访问数据。但是,过长的过期时间可能会导致不必要的内存占用。
- 如果键已经存在过期时间,再次使用EXPIRE指令为该键设置新的过期时间将覆盖原有的过期时间。
- Redis的过期策略包括定期删除和惰性删除。定期删除是在后台线程中设置一个定期任务,用于扫描和删除过期键。惰性删除是在取出键时才对键进行过期检查,如果发现键已经过期,则删除该键。因此,在某些情况下,即使键的过期时间已经到达,也可能需要等待一段时间才能实际删除该键。
(4)键的过期机制如下图所示:
(5)Redis采用两种策略来删除过期的键:惰性删除和定期删除。
- 惰性删除:当客户端尝试访问一个键时,Redis会检查该键是否已过期。如果已过期,Redis会立即删除该键,并返回nil给客户端。这种策略可以节省CPU资源,但可能会导致内存占用过多,因为过期的键可能不会被立即删除。
- 定期删除:Redis会定期运行一个后台任务,该任务会随机选择一定数量的键进行检查,并删除那些已过期的键。这种策略可以确保即使没有客户端访问,过期的键最终也会被清理掉。但是,为了避免对性能造成太大影响,Redis不会检查所有的键,而是只检查一部分键。因此,有些过期的键可能不会立即被删除,而是在下一次检查时才被处理。
4.6 TTL指令
(1)Redis的TTL(Time To Live)指令用于获取存储在Redis数据库中的键(key)的剩余生存时间。以下是TTL的语法:
TTL key
- key:要获取剩余生存时间的键。
- 返回值:整数。返回键的剩余生存时间,以秒为单位。
- 如果键不存在,返回-2。
- 如果键存在但没有设置剩余生存时间(即永久存在),返回-1。
- 如果键存在且设置了剩余生存时间,则返回该时间的剩余秒数。
(2)示例:
SET mykey "Hello" # 设置一个键值对
EXPIRE mykey 60 # 为这个键设置60秒的过期时间
TTL mykey # 获取这个键的剩余生存时间
- 假设在上述命令执行后,立即执行TTL mykey,可能会返回59(或接近60的某个值,取决于执行命令时的具体时间)。随着时间的推移,TTL mykey的返回值将逐渐减少,直到返回0(表示键即将过期)然后键被自动删除(注意,删除可能是惰性的,具体取决于Redis的过期策略)。
(3)注意事项:
- TTL指令返回的是键的剩余生存时间,而不是键的创建时间或过期时间。
如果键不存在,TTL指令会返回-2,而不是抛出错误或异常。 - 如果键存在但没有设置剩余生存时间(即永久存在),TTL指令会返回-1。
- 在使用TTL指令时,需要注意Redis的过期策略(定期删除和惰性删除),因为即使键的过期时间已经到达,也可能需要等待一段时间才能实际删除该键。
4.7 TYPE指令
(1)Redis的TYPE指令用于返回存储在指定键中的值的数据类型。以下是TYPE的语法:
TYPE key
- key:要查询数据类型的键。
- 返回值:字符串。返回键的数据类型,可能的值包括:
- string:字符串类型,是Redis中最基本的数据类型。
- list:列表类型,可以存储一个有序的字符串序列。
- set:集合类型,可以存储一个无序的字符串集合。
- zset(或sorted set):有序集合类型,每个成员关联一个分数(score),成员按分数排序。
- hash:哈希类型,类似于map或字典,能够存储键值对。
- stream:流数据类型,主要用于存储日志或事件流数据(在Redis 5.0及更高版本中引入)。
- none:表示键不存在。
(2)示例:
SET mystring "Hello" # 设置一个字符串类型的键值对
TYPE mystring # 返回 "string"
LPUSH mylist "Redis" # 设置一个列表类型的键值对
TYPE mylist # 返回 "list"
SADD myset "apple" # 设置一个集合类型的键值对
TYPE myset # 返回 "set"
ZADD myzset 1 "one" # 设置一个有序集合类型的键值对
TYPE myzset # 返回 "zset" 或 "sorted set"
HSET myhash field1 "value1" # 设置一个哈希类型的键值对
TYPE myhash # 返回 "hash"
# 假设Redis版本为5.0或更高,并且启用了stream数据类型
XADD mystream * field "value" # 设置一个流类型的键值对
TYPE mystream # 返回 "stream"
# 查询一个不存在的键
TYPE nonexistent # 返回 "none"
(3)注意事项:
- TYPE指令是一个O(1)操作,因为它只需查看键的元数据,而不需要扫描整个值。这意味着它在性能上非常高效,可以快速返回结果。
- 在使用TYPE指令时,需要确保键名正确且存在于Redis数据库中,否则将返回"none"。
本小结只是抛砖引玉,给出几个通用的命令,为 5 种数据类型的使用做⼀个热身,后续章节将对键管理做⼀个更为详细的介绍。
5. 数据类型和内部编码
(1)type 命令实际返回的就是当前键的数据结构类型,它们分别是:string(字符串)、list(列表)、hash(哈希)、set(集合)、zset(有序集合),但这些只是 Redis 对外的数据类型。
(2)实际上 Redis 针对每种数据结构都有自己的底层内部编码实现,而且是多种实现,这样 Redis 会在合适的场景选择合适的内部编码,如下表所示:
数据类型 | 内部编码 |
---|---|
string | raw、int、embstr |
hash | hashtable、ziplist |
list | linkedlist、ziplist |
set | hashtable、intset |
zset | skiplist、ziplist |
(3)可以看到每种数据结构都有至少两种以上的内部编码实现,例如 list 数据结构包含了 linkedlist 和ziplist 两种内部编码。同时有些内部编码,例如 ziplist,可以作为多种数据结构的内部实现,可以通过 object encoding 命令查询内部编码:
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"
(4)可以看到 hello 对应值的内部编码是 embstr,键 mylist 对应值的内部编码是 ziplist。Redis 这样设计有两个好处:
- 可以改进内部编码,⽽对外的数据结构和命令没有任何影响,这样⼀旦开发出更优秀的内部编码,⽆需改动外部数据结构和命令,例如 Redis 3.2 提供了 quicklist,结合了 ziplist 和 linkedlist 两者的优势,为列表类型提供了⼀种更为优秀的内部编码实现,而对用户来说基本⽆感知。
- 多种内部编码实现可以在不同场景下发挥各自的优势,例如 ziplist 比较节省内存,但是在列表元素比较多的情况下,性能会下降,这时候 Redis 会根据配置选项将列表类型的内部实现转换为linkedlist,整个过程用户同样无感知。
6. 单线程架构
Redis 使用了单线程架构来实现高性能的内存数据库服务,本节首先通过多个客户端命令调用的例子说明 Redis 单线程命令处理机制,接着分析 Redis 单线程模型为什么性能如此之高,最终给出为什么理解单线程模型是使用和运维 Redis 的关键。
6.1 引出单线程模型
(1)现在开启了三个 redis-cli 客户端同时执行命令。
- 客户端 1 设置⼀个字符串键值对:
127.0.0.1:6379> set hello world
- 客户端 2 对 counter 做自增操作:
127.0.0.1:6379> incr counter
- 客户端 3 对 counter 做自增操作:
127.0.0.1:6379> incr counter
(2)我们已经知道从客户端发送的命令经历了:发送命令、执行命令、返回结果三个阶段,其中我们重点关注第 2 步。我们所谓的 Redis 是采用单线程模型执行命令的是指:虽然三个客户端看起来是同时要求 Redis 去执行命令的,但微观角度,这些命令还是采用线性方式去执行的,只是原则上命令的执行顺序是不确定的,但⼀定不会有两条命令被同步执行,如下图所示,可以想象 Redis内部只有⼀个服务窗口,多个客户端按照它们达到的先后顺序被排队在窗口前,依次接受 Redis 的服务,所以两条 incr 命令无论执行顺序,结果⼀定是 2,不会发生并发问题,这个就是 Redis 的单线程执行模型。
- 宏观上同时要求服务的客户端:
- 微观上客户端发送命令的时间有先后次序的:
- Redis 的单线程模型:
6.2 为什么单线程还能这么快
(1)通常来讲,单线程处理能力要比多线程差,例如有 10 000 公斤货物,每辆车的运载能力是每次200 公斤,那么要 50 次才能完成;但是如果有 50 辆车,只要安排合理,只需要依次就可以完成任务。那么为什么 Redis 使用单线程模型会达到每秒万级别的处理能力呢?可以将其归结为三点:
- 纯内存访问。Redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,这是 Redis 达到每秒万级别访问的重要基础。
- 非阻塞 IO。Redis 使用 epoll 作为 I/O 多路复用技术的实现,再加上 Redis 自身的事件处理模型将 epoll 中的连接、读写、关闭都转换为事件,不在网络 I/O 上浪费过多的时间,如下图所示。
- 单线程避免了线程切换和竞态产⽣的消耗。单线程可以简化数据结构和算法的实现,让程序模型更简单;其次多线程避免了在线程竞争同⼀份共享数据时带来的切换和等待消耗。
Redis 使用 I/O 多路复用模型
(2)虽然单线程给 Redis 带来很多好处,但还是有⼀个致命的问题:对于单个命令的执行时间都是有要求的。如果某个命令执行过长,会导致其他命令全部处于等待队列中,迟迟等不到响应,造成客户端的阻塞,对于 Redis 这种高性能的服务来说是非常严重的,所以 Redis 是面向快速执行场景的数据库。