在一些网络服务的系统中,Redis 的性能,多是比 MySQL 等硬盘数据库的性能更重要的课题。好比微博,把热点微博[1],最新的用户关系,都存储在 Redis 中,大量的查询击中 Redis,而不走 MySQL。node
那么,针对 Redis 服务,咱们能作哪些性能优化呢?或者说,应该避免哪些性能浪费呢?linux
在讨论优化以前,咱们须要知道,Redis 服务自己就有一些特性,好比单线程运行。除非修改 Redis 的源代码,否则这些特性,就是咱们思考性能优化的基本面。程序员
那么,有哪些 Redis 基本特性须要咱们考虑呢?Redis 的项目介绍中归纳了它特性:面试
Redis is an in-memory database that persists on disk. The data model is key-value, but many different kind of values are supported.redis
首先,Redis 使用操做系统提供的虚拟内存来存储数据。并且,这个操做系统通常就是指 Unix。Windows 上也能运行 Redis,可是须要特殊处理。若是你的操做系统使用交换空间,那么 Redis 的数据可能会被实际保存在硬盘上。关注公众号:程序员白楠楠,获取2020最新面试题算法
其次,Redis 支持持久化,能够把数据保存在硬盘上。不少时候,咱们也确实有必要进行持久化来实现备份,数据恢复等需求。但持久化不会凭空发生,它也会占用一部分资源。mongodb
第三,Redis 是用 key-value 的方式来读写的,而 value 中又能够是不少不一样种类的数据;更进一步,一个数据类型的底层还有被存储为不一样的结构。不一样的存储结构决定了数据增删改查的复杂度以及性能开销。数据库
最后,在上面的介绍中没有提到的是,Redis 大多数时候是单线程运行[2]的(single-threaded),即同一时间只占用一个 CPU,只能有一个指令在运行,并行读写是不存在的。不少操做带来的延迟问题,均可以在这里找到答案。编程
关于最后这个特性,为何 Redis 是单线程的,却能有很好的性能(根据 Amdahl’s Law,优化耗时占比大的过程,才更有意义),两句话归纳是:Redis 利用了多路 I/O 复用机制[3],处理客户端请求时,不会阻塞主线程;Redis 单纯执行(大多数指令)一个指令不到 1 微秒[4],如此,单核 CPU 一秒就能处理 1 百万个指令(大概对应着几十万个请求吧),用不着实现多线程(网络才是瓶颈[5])。设计模式
Redis 的官方博客在几个地方都说,性能瓶颈更多是网络[6],那么咱们如何优化网络上的延时呢?
首先,若是大家使用单机部署(应用服务和 Redis 在同一台机器上)的话,使用 Unix 进程间通信来请求 Redis 服务,速度比 localhost 局域网(学名 loopback)更快。官方文档[7]是这么说的,想想,理论上也应该是这样的。
但不少公司的业务规模不是单机部署能支撑的,因此仍是得用 TCP。
Redis 客户端和服务器的通信通常使用 TCP 长连接。若是客户端发送请求后须要等待 Redis 返回结果再发送下一个指令,客户端和 Redis 的多个请求就构成下面的关系:
(备注:若是不是你要发送的 key 特别长,一个 TCP 包彻底能放下 Redis 指令,因此只画了一个 push 包)
这样这两次请求中,客户端都须要经历一段网络传输时间。
但若是有可能,彻底可使用 multi-key 类的指令来合并请求,好比两个 GET key 能够用 MGET key1 key2 合并。这样在实际通信中,请求数也减小了,延时天然获得好转。
若是不能用 multi-key 指令来合并,好比一个 SET,一个 GET 没法合并。怎么办?
Redis 中有至少这样两个方法能合并多个指令到一个 request 中,一个是MULTI/EXEC,一个是 script。前者原本是构建 Redis 事务的方法,但确实能够合并多个指令为一个 request,它到通信过程以下。至于 script,最好利用缓存脚本的 sha1 hash key 来调起脚本,这样通信量更小。
这样确实更能减小网络传输时间,不是么?但如此以来,就必需要求这个 transaction / script 中涉及的 key 在同一个 node 上,因此要酌情考虑。
若是上面的方法咱们都考虑过了,仍是没有办法合并多个请求,咱们还能够考虑合并多个 responses。好比把 2 个回复信息合并:
这样,理论上能够省去 1 次回复所用的网络传输时间。这就是 pipeline 作的事情。举个 ruby 客户端使用 pipeline 的例子:
require 'redis' @redis = Redis.new() @redis.pipelined do @redis.get 'key1' @redis.set 'key2' 'some value' end # => [1, 2]
听说,有些语言的客户端,甚至默认就使用 pipeline 来优化延时问题,好比 node_redis。
另外,不是任意多个回复信息均可以放进一个 TCP 包中,若是请求数太多,回复的数据很长(好比 get 一个长字符串),TCP 仍是会分包传输,但使用 pipeline,依然能够减小传输次数。
pipeline 和上面的其余方法都不同的是,它不具备原子性。因此在 cluster 状态下的集群上,实现 pipeline 比那些原子性的方法更有可能。
小结一下:
使用 unix 进程间通讯,若是单机部署
使用 multi-key 指令合并多个指令,减小请求数,若是有可能的话
使用 transaction、script 合并 requests 以及 responses
在大数据量的状况下,有些操做的执行时间会相对长,好比 KEYS *,LRANGE mylist 0 -1,以及其余算法复杂度为 O(n) 的指令。由于 Redis 只用一个线程来作数据查询,若是这些指令耗时很长,就会阻塞 Redis,形成大量延时。
尽管官方文档中说 KEYS * 的查询挺快的,(在普通笔记本上)扫描 1 百万个 key,只需 40 毫秒(参见:https://redis.io/commands/keys),但几十 ms 对于一个性能要求很高的系统来讲,已经不短了,更况且若是有几亿个 key(一台机器彻底可能存几亿个 key,好比一个 key 100字节,1 亿个 key 只有 10GB),时间更长。
因此,尽可能不要在生产环境的代码使用这些执行很慢的指令,这一点 Redis 的做者在博客[8]中也提到了。另外,运维同窗查询 Redis 的时候也尽可能不要用。甚至,Redis Essential 这本书建议利用 rename-command KEYS '' 来禁止使用这个耗时的指令。
除了这些耗时的指令,Redis 中 transaction,script,由于能够合并多个 commands 为一个具备原子性的执行过程,因此也可能占用 Redis 很长时间,须要注意。
若是你想找出生产环境使用的「慢指令」,那么能够利用 SLOWLOG GET count 来查看最近的 count 个执行时间很长的指令。至于多长算长,能够经过在 redis.conf 中设置 slowlog-log-slower-than 来定义。
除此以外,在不少地方都没有提到的一个可能的慢指令是 DEL,但 redis.conf 文件的注释[9]中却是说了。长话短说就是 DEL 一个大的 object 时候,回收相应的内存可能会须要很长时间(甚至几秒),因此,建议用 DEL 的异步版本:UNLINK。后者会启动一个新的 thread 来删除目标 key,而不阻塞原来的线程。
更进一步,当一个 key 过时以后,Redis 通常也须要同步的把它删除。其中一种删除 keys 的方式是,每秒 10 次的检查一次有设置过时时间的 keys,这些 keys 存储在一个全局的 struct 中,能够用 server.db->expires 访问。检查的方式是:
从中随机取出 20 个 keys
把过时的删掉。
这里对于性能的影响是,若是真的有不少的 keys 在同一时间过时,那么 Redis 真的会一直循环执行删除,占用主线程。
对此,Redis 做者的建议[10]是警戒 EXPIREAT 这个指令,由于它更容易产生 keys 同时过时的现象。我还见到过一些建议是给 keys 的过时时间设置一个随机波动量。最后,redis.conf 中也给出了一个方法,把 keys 的过时删除操做变为异步的,即,在 redis.conf 中设置 lazyfree-lazy-expire yes。
一种数据类型(好比 string,list)进行增删改查的效率是由其底层的存储结构决定的。
咱们在使用一种数据类型时,能够适当关注一下它底层的存储结构及其算法,避免使用复杂度过高的方法。举两个例子:
ZADD 的时间复杂度是 O(log(N)),这比其余数据类型增长一个新元素的操做更复杂,因此要当心使用。
除了时间性能上的考虑,有时候咱们还须要节省存储空间。好比上面提到的 ziplist 结构,就比 hashtable 结构节省存储空间(Redis Essentials 的做者分别在 hashtable 和 ziplist 结构的 Hash 中插入 500 个 fields,每一个 field 和 value 都是一个 15 位左右的字符串,结果是 hashtable 结构使用的空间是 ziplist 的 4 倍。)。但节省空间的数据结构,其算法的复杂度可能很高。因此,这里就须要在具体问题面前作出权衡。欢迎关注公众号:朱小厮的博客,回复:1024,能够领取redis专属资料。
如何作出更好的权衡?我以为得深挖 Redis 的存储结构才能让本身安心。这方面的内容咱们下次再说。
以上这三点都是编程层面的考虑,写程序时应该注意啊。下面这几点,也会影响 Redis 的性能,但解决起来,就不仅是靠代码层面的调整了,还须要架构和运维上的考虑。
Redis 运行的外部环境,也就是操做系统和硬件显然也会影响 Redis 的性能。在官方文档中,就给出了一些例子:
CPU:Intel 多种 CPU 都比 AMD 皓龙系列好
虚拟化:实体机比虚拟机好,主要是由于部分虚拟机上,硬盘不是本地硬盘,监控软件致使 fork 指令的速度慢(持久化时会用到 fork),尤为是用 Xen 来作虚拟化时。
内存管理:在 linux 操做系统中,为了让 translation lookaside buffer,即 TLB,可以管理更多内存空间(TLB 只能缓存有限个 page),操做系统把一些 memory page 变得更大,好比 2MB 或者 1GB,而不是一般的 4096 字节,这些大的内存页叫作 huge pages。同时,为了方便程序员使用这些大的内存 page,操做系统中实现了一个 transparent huge pages(THP)机制,使得大内存页对他们来讲是透明的,能够像使用正常的内存 page 同样使用他们。但这种机制并非数据库所须要的,多是由于 THP 会把内存空间变得紧凑而连续吧,就像mongodb 的文档[11]中明确说的,数据库须要的是稀疏的内存空间,因此请禁掉 THP 功能。Redis 也不例外,但 Redis 官方博客上给出的理由是:使用大内存 page 会使 bgsave 时,fork 的速度变慢;若是 fork 以后,这些内存 page 在原进程中被修改了,他们就须要被复制(即 copy on write),这样的复制会消耗大量的内存(毕竟,人家是 huge pages,复制一份消耗成本很大)。因此,请禁止掉操做系统中的 transparent huge pages 功能。
Redis 的一项重要功能就是持久化,也就是把数据复制到硬盘上。基于持久化,才有了 Redis 的数据恢复等功能。
但维护这个持久化的功能,也是有性能开销的。
首先说,RDB 全量持久化。
这种持久化方式把 Redis 中的全量数据打包成 rdb 文件放在硬盘上。可是执行 RDB 持久化过程的是原进程 fork 出来一个子进程,而 fork 这个系统调用是须要时间的,根据Redis Lab 6 年前作的实验[12],在一台新型的 AWS EC2 m1.small^13 上,fork 一个内存占用 1GB 的 Redis 进程,须要 700+ 毫秒,而这段时间,redis 是没法处理请求的。
虽然如今的机器应该都会比那个时候好,可是 fork 的开销也应该考虑吧。为此,要使用合理的 RDB 持久化的时间间隔,不要太频繁。
接下来,咱们看另一种持久化方式:AOF 增量持久化。
这种持久化方式会把你发到 redis server 的指令以文本的形式保存下来(格式遵循 redis protocol),这个过程当中,会调用两个系统调用,一个是 write(2),同步完成,一个是 fsync(2),异步完成。
这两部均可能是延时问题的缘由:
write 可能会由于输出的 buffer 满了,或者 kernal 正在把 buffer 中的数据同步到硬盘,就被阻塞了。
其中,write 的阻塞貌似只能接受,由于没有更好的方法把数据写到一个文件中了。但对于 fsync,Redis 容许三种配置,选用哪一种取决于你对备份及时性和性能的平衡:
always:当把 appendfsync 设置为 always,fsync 会和客户端的指令同步执行,所以最可能形成延时问题,但备份及时性最好。
everysec:每秒钟异步执行一次 fsync,此时 redis 的性能表现会更好,可是 fsync 依然可能阻塞 write,算是一个折中选择。
以上,咱们都是基于单台,或者单个 Redis 服务进行优化。下面,咱们考虑当网站的规模变大时,利用分布式架构来保障 Redis 性能的问题。
首先说,哪些状况下不得不(或者最好)使用分布式架构:
数据量很大,单台服务器内存不可能装得下,好比 1 个 T 这种量级
须要服务高可用
解决这些问题能够采用数据分片或者主从分离,或者二者都用(即,在分片用的 cluster 节点上,也设置主从结构)。
这样的架构,能够为性能提高加入新的切入点:
把慢速的指令发到某些从库中执行
把持久化功能放在一个不多使用的从库上
其中前两条都是根据 Redis 单线程的特性,用其余进程(甚至机器)作性能补充的方法。
固然,使用分布式架构,也可能对性能有影响,好比请求须要被转发,数据须要被不断复制分发。(待查)
其实还有不少东西也影响 Redis 的性能,好比 active rehashing(keys 主表的再哈希,每秒 10 次,关掉它能够提高一点点性能),可是这篇博客已经写的很长了。并且,更重要不是收集已经被别人提出的问题,而后记忆解决方案;而是掌握 Redis 的基本原理,以不变应万变的方式决绝新出现的问题。
小编总结了2020面试题,这份面试题的包含的模块分为19个模块,分别是: Java 基础、容器、多线程、反射、对象拷贝、Java Web 、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM 。
关注公众号:程序员白楠楠,获取上述资料。