Redis-数据结构和内部编码
Redis 数据结构
type 命令实际返回的就是当前键的数据结构类型,它们分别是:string(字符串)、list(列 表)、hash(哈希)、set(集合)、zset(有序集合),但这些只是Redis对外的数据结构。
Redis的5种数据类型
实际上Redis针对每种数据结构都有⾃⼰的底层内部编码实现,⽽且是多种实现,这样Redis会 在合适的场景选择合适的内部编码,如表2-1所⽰。
表2-1Redis数据结构和内部编码
可以看到每种数据结构都有⾄少两种以上的内部编码实现,例如list数据结构包含了linkedlist和 ziplist 两种内部编码。同时有些内部编码,例如ziplist,可以作为多种数据结构的内部实现,可以通 过objectencoding命令查询内部编码:
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 这样设计有两个好处:
- 可以改进内部编码,⽽对外的数据结构和命令没有任何影响,这样⼀旦开发出更优秀的内部编码, ⽆需改动外部数据结构和命令,例如Redis3.2提供了quicklist,结合了ziplist和linkedlist两者的优势,为列表类型提供了⼀种更为优秀的内部编码实现,⽽对⽤⼾来说基本⽆感知。
- 多种内部编码实现可以在不同场景下发挥各⾃的优势,例如ziplist⽐较节省内存,但是在列表元素 ⽐较多的情况下,性能会下降,这时候Redis会根据配置选项将列表类型的内部实现转换为 linkedlist,整个过程⽤⼾同样⽆感知。
单线程架构
Redis 使⽤了单线程架构来实现⾼性能的内存数据库服务,⾸先通过多个客⼾端命令调⽤的例 ⼦说明Redis单线程命令处理机制,接着分析Redis单线程模型为什么性能如此之⾼,最终给出为什 么理解单线程模型是使⽤和运维Redis的关键。
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步。我们所谓的Redis是采⽤单线程模型执⾏命令的是指:虽然三个客⼾端看起来是同 时要求Redis去执⾏命令的,但微观⻆度,这些命令还是采⽤线性⽅式去执⾏的,只是原则上命令的 执⾏顺序是不确定的,但⼀定不会有两条命令被同步执⾏,可以想象Redis 内部只有⼀个服务窗⼝,多个客⼾端按照它们达到的先后顺序被排队在窗⼝前,依次接受Redis的服务,所以两条incr命令⽆论执⾏顺序,结果⼀定是2,不会发⽣并发问题,这个就是Redis的单线程 执⾏模型。
2. 为什么单线程还能这么快
通常来讲,单线程处理能⼒要⽐多线程差,例如有10000公⽄货物,每辆⻋的运载能⼒是每次 200 公⽄,那么要50次才能完成;但是如果有50辆⻋,只要安排合理,只需要依次就可以完成任 务。那么为什么Redis使⽤单线程模型会达到每秒万级别的处理能⼒呢?可以将其归结为三点:
- 纯内存访问。Redis将所有数据放在内存中,内存的响应时⻓⼤约为100纳秒,这是Redis达 到每秒万级别访问的重要基础。
- ⾮阻塞IO。Redis使⽤epoll作为I/O多路复⽤技术的实现,再加上Redis⾃⾝的事件处理模型 将epoll中的连接、读写、关闭都转换为事件,不在⽹络I/O上浪费过多的时间,如图2-6所 ⽰。
- 单线程避免了线程切换和竞态产⽣的消耗。单线程可以简化数据结构和算法的实现,让程序模 型更简单;其次单线程避免了在线程竞争同⼀份共享数据时带来的切换和等待消耗。
图2-6Redis使⽤I/O多路复⽤模型
虽然单线程给Redis带来很多好处,但还是有⼀个致命的问题:对于单个命令的执⾏时间都是有 要求的。如果某个命令执⾏过⻓,会导致其他命令全部处于等待队列中,迟迟等不到响应,造成客⼾ 端的阻塞,对于Redis这种⾼性能的服务来说是⾮常严重的,所以Redis是⾯向快速执⾏场景的数据库。