快!做对这 10 点,让你的 Redis 性能更上一层楼
mhr18 2024-10-31 13:31 20 浏览 0 评论
今天跟大家分享 「提升 Redis 性能的 10 个手段」 ,Redis 作为内存数据库,虽说已经足够快了。但是,做对这 10 点,可以让你的 Redis 性能更上一层楼!
注:本文源码基于 Redis 6.2
01 使用 pipeline
Redis 是基于请求-响应模型的 TCP 服务器。意味着单次请求 RTT(往返时间),取决于当前网络状况 。这会导致单个 Redis 请求可能非常快,比如通过本地环路网卡。可能非常慢,比如处于网络状况不佳的环境。
另一方面,Redis 每次请求-响应,都涉及到 read 和 write 系统调用。甚至会触发多次 epoll_wait 系统调用(Linux 平台)。这导致 Redis 不断在用户态和内核态进行切换。
static int connSocketRead(connection *conn, void *buf, size_t buf_len) {
// read 系统调用
int ret = read(conn->fd, buf, buf_len);
}
static int connSocketWrite(connection *conn, const void *data, size_t data_len) {
// write 系统调用
int ret = write(conn->fd, data, data_len);
}
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
// 事件触发,Linux 下为 epoll_wait 系统调用
numevents = aeApiPoll(eventLoop, tvp);
}
复制代码
那么,如何节省往返时间和系统调用次数呢?批处理是一个好的办法。
为此,Redis 提供了 「pipeline」。pipeline 的原理很简单,将多个命令打包成「一个命令」发送。Redis 收到后,解析成多个命令执行。最终将多个结果打包返回。
「pipeline 可以有效的提升 Redis 性能」。
但是,使用 pipeline 有几点需要你留意
- 「pipeline 不能保证原子性」。在一次 pipeline 命令执行期间,可能会执行其它 client 发起的命令。请记住,pipeline 只是批量处理命令。想要保证原子性,使用 MULTI 或者 Lua 脚本。
- 「单次 pipeline 命令不宜过多」。当使用 pipeline 时,Redis 会将 pipeline 命令的响应结果,暂存在内存 Reply buffer 中,等待所有命令执行完毕后返回。如果 pipeline 命令过多,可能会导致占用较多内存。可以将单个 pipeline 拆分成多个 pipeline。
顺便提一句,笔者曾今遇到过,升级 Redis 实例。新实例上线后,系统响应时间下降了 6ms。可别小瞧这 6ms,正常此系统平均响应时间还不到 3ms。
后来发现,新 Redis 实例和旧 Redis 实例不在同一个机房。新 Redis 机房比旧 Redis 机房多 1.5ms 的 RTT(网络往返时间)。而项目代码里有 4 处 Redis 调用,导致上升了 4 * 1.5ms = 6ms 延迟。
所以这也告诉我们,pipeline 的重要性,如果 4 处 Redis 调用能使用 pipeline 组合,则最大也就上升 1.5ms 的延迟。相对于 Redis 的高性能来说,哪怕 1ms 网络延迟都是关键的。
02 开启 IO 多线程
在「Redis 6」版本以前,Redis 是 「单线程」 读取、解析、执行命令的。Redis 6 开始,引入了 IO 多线程。
IO 线程负责读取命令、解析命令、返回结果。开启后可以有效提升 IO 性能。
我画了一张示意图供你参考
如上图所示,主线程和 IO 线程会共同参与命令的读取、解析以及结果响应。
但执行命令的,为 「主线程」。
IO 线程默认关闭,你可以修改 redis.conf 以下配置开启。
io-threads 4
io-threads-do-reads yes
复制代码
「io-threads」 是 IO 线程数(包含主线程),我建议你根据机器,设置不同值进行压测,取最优值。
03 避免 big key
Redis 执行命令是单线程的,这意味着 Redis 操作「big key」有阻塞的风险。
big key 通常指的是 Redis 存储的 value 过大。包括:
- 单个 value 过大。如 200M 大小的 String。
- 集合元素过多。如 List、Hash、Set、ZSet 中有几百、上千万数据。
举个例子,假设我们有一个 200M 大小的 String key,名称为「foo」。
执行如下命令
127.0.0.1:6379> GET foo
复制代码
当返回结果时,Redis 会分配 200m 的内存,并执行 memcpy 拷贝。
void _addReplyProtoToList(client *c, const char *s, size_t len) {
...
if (len) {
/* Create a new node, make sure it is allocated to at
* least PROTO_REPLY_CHUNK_BYTES */
size_t size = len < PROTO_REPLY_CHUNK_BYTES? PROTO_REPLY_CHUNK_BYTES: len;
// 分配内存(例子中为 200m)
tail = zmalloc(size + sizeof(clientReplyBlock));
/* take over the allocation's internal fragmentation */
tail->size = zmalloc_usable_size(tail) - sizeof(clientReplyBlock);
tail->used = len;
// 内存拷贝
memcpy(tail->buf, s, len);
listAddNodeTail(c->reply, tail);
c->reply_bytes += tail->size;
closeClientOnOutputBufferLimitReached(c, 1);
}
}
复制代码
而 Redis 输出 buf 为 16k
// server.h
#define PROTO_REPLY_CHUNK_BYTES (16*1024) /* 16k output buffer */
typedef struct client {
...
char buf[PROTO_REPLY_CHUNK_BYTES];
} client;
复制代码
这意味着 Redis 无法单次返回响应数据,需要注册「可写事件」,从而触发多次 write 系统调用。
这里有两个耗时点:
- 分配大内存(也可能释放内存,如 DEL 命令)
- 触发多次可写事件(频繁执行系统调用,如 write、epoll_wait)
那么,如何找出 big key 呢?
如果 slow log 出现了简单命令,如 GET、SET、DEL,大概率是出现了 big key。
127.0.0.1:6379> SLOWLOG GET
3) (integer) 201323 // 单位微妙
4) 1) "GET"
2) "foo"
复制代码
其次,可以通过 Redis 分析工具来查找 big key。
$ redis-cli --bigkeys -i 0.1
...
[00.00%] Biggest string found so far '"foo"' with 209715200 bytes
-------- summary -------
Sampled 1 keys in the keyspace!
Total key length in bytes is 3 (avg len 3.00)
Biggest string found '"foo"' has 209715200 bytes
1 strings with 209715200 bytes (100.00% of keys, avg size 209715200.00)
0 lists with 0 items (00.00% of keys, avg size 0.00)
0 hashs with 0 fields (00.00% of keys, avg size 0.00)
0 streams with 0 entries (00.00% of keys, avg size 0.00)
0 sets with 0 members (00.00% of keys, avg size 0.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00)
复制代码
对于 big key,有以下几点建议:
1.业务中尽量避免 big key 出现。当出现 big key 时,你要判断这样设计是否合理,又或者是出现了 bug。
2.将 big key 拆分为多个小 key。
3.使用替代命令。
- 如果 Redis 版本大于 4.0,可使用 UNLINK 命令替代 DEL。Redis 版本大于 6.0,可开启 lazy-free 机制。将释放内存操作,放到后台线程执行。
- LRANGE、HGETALL 等替换为 LSCAN、HSCAN 分次获取。
但我还是建议在业务中避免 big key。
04 避免执行时间复杂度高的命令
我们知道 Redis 是「单线程」执行命令的。执行时间复杂度高的命令,很可能会阻塞其它请求。
复杂度高的命令和元素数量有关。通常有以下两种场景。
- 元素太多,消耗 IO 资源。如 HGETALL、LRANGE,时间复杂度为 O(N)。
- 计算过于复杂,消费 CPU 资源。如 ZUNIONSTORE,时间复杂度为 O(N)+O(M log(M))
Redis 官方手册,标记了命令执行的时间复杂度。建议你在使用不熟悉的命令前,先查看手册,留意时间复杂度。
实际业务中,你应该尽量避免时间复杂度高的命令。如果必须要用,有两点建议
- 保证操作的元素数量,尽可能少。
- 读写分离。复杂命令通常是读请求,可以放到「slave」结点执行。
05 使用惰性删除 Lazy free
key 过期或是使用 DEL 删除命令时,Redis 除了从全局 hash 表移除对象外,还会将对象分配的内存释放。当遇到 big key 时,释放内存会造成主线程阻塞。
为此,Redis 4.0 引入了 UNLINK 命令,将释放对象内存操作放入 bio 后台线程执行。从而有效减少主线程阻塞。
Redis 6.0 更进一步,引入了 Lazy-free 相关配置。当开启配置后,key 过期和 DEL 命令内部,会将「释放对象」操作「异步执行」。
void delCommand(client *c) {
delGenericCommand(c,server.lazyfree_lazy_user_del);
}
void delGenericCommand(client *c, int lazy) {
int numdel = 0, j;
for (j = 1; j < c->argc; j++) {
expireIfNeeded(c->db,c->argv[j]);
// 开启 lazy free 则使用异步删除
int deleted = lazy ? dbAsyncDelete(c->db,c->argv[j]) :
dbSyncDelete(c->db,c->argv[j]);
...
}
}
复制代码
建议至少升级到 Redis 6,并开启 Lazy-free。
06 读写分离
Redis 通过副本,实现「主-从」运行模式,是故障切换的基石,用来提高系统运行可靠性。也支持读写分离,提高读性能。
你可以部署一个主结点,多个从结点。将读命令分散到从结点中,从而减轻主结点压力,提升性能。
07 绑定 CPU
Redis 6.0 开始支持绑定 CPU,可以有效减少线程上下文切换。
CPU 亲和性(CPU Affinity)是一种调度属性,它将一个进程或线程,「绑定」到一个或一组 CPU 上。也称为 CPU 绑定。
设置 CPU 亲和性可以一定程度避免 CPU 上下文切换,提高 CPU L1、L2 Cache 命中率。
早期「SMP」架构下,每个 CPU 通过 BUS 总线共享资源。CPU 绑定意义不大。
而在当前主流的「NUMA」架构下,每个 CPU 有自己的本地内存。访问本地内存有更快的速度。而访问其他 CPU 内存会导致较大的延迟。这时,CPU 绑定对系统运行速度的提升有较大的意义。
现实中的 NUMA 架构比上图更复杂,通常会将 CPU 分组,若干个 CPU 分配一组内存,称为 「node」。
你可以通过 「numactl -H 」 命令来查看 NUMA 硬件信息。
$ numactl -H
available: 2 nodes (0-1)
node 0 cpus: 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38
node 0 size: 32143 MB
node 0 free: 26681 MB
node 1 cpus: 1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39
node 1 size: 32309 MB
node 1 free: 24958 MB
node distances:
node 0 1
0: 10 21
1: 21 10
复制代码
上图中可以得知该机器有 40 个 CPU,分组为 2 个 node。
node distances 是一个二维矩阵,表示 node 之间 「访问距离」,10 为基准值。上述命令中可以得知,node 自身访问,距离是 10。跨 node 访问,如 node 0 访问 node 1 距离为 21。说明该机器「跨 node 访问速度」比「node 自身访问速度」慢 2.1 倍。
其实,早在 2015 年,有人提出 Redis 需要支持设置 CPU 亲和性,而当时的 Redis 还没有支持 IO 多线程,该提议搁置。
而 Redis 6.0 引入 IO 多线程。同时,也支持了设置 CPU 亲和性。
我画了一张 Redis 6.0 线程家族供你参考。
上图可分为 3 个模块
- 主线程和 IO 线程:负责命令读取、解析、结果返回。命令执行由主线程完成。
- bio 线程:负责执行耗时的异步任务,如 close fd。
- 后台进程:fork 子进程来执行耗时的命令。
Redis 支持分别配置上述模块的 CPU 亲和度。你可以在 redis.conf 找到以下配置(该配置需手动开启)。
# IO 线程(包含主线程)绑定到 CPU 0、2、4、6
server_cpulist 0-7:2
# bio 线程绑定到 CPU 1、3
bio_cpulist 1,3
# aof rewrite 后台进程绑定到 CPU 8、9、10、11
aof_rewrite_cpulist 8-11
# bgsave 后台进程绑定到 CPU 1、10、11
bgsave_cpulist 1,10-11
复制代码
我在上述机器,针对 IO 线程和主线程,进行如下测试:
首先,开启 IO 线程配置。
io-threads 4 # 主线程 + 3 个 IO 线程
io-threads-do-reads yes # IO 线程开启读和解析命令功能
复制代码
测试如下三种场景:
- 不开启 CPU 绑定配置。
- 绑定到不同 node。
「server_cpulist 0,1,2,3」
- 绑定到相同 node。
「server_cpulist 0,2,4,6」
通过 redis-benchmark 对 get 命令进行基准测试,每种场景执行 3 次。
$ redis-benchmark -n 5000000 -c 50 -t get --threads 4
复制代码
结果如下:
1.不开启 CPU 绑定配置
throughput summary: 248818.11 requests per second
throughput summary: 248694.36 requests per second
throughput summary: 249004.00 requests per second
复制代码
2.绑定不同 node
throughput summary: 248880.03 requests per second
throughput summary: 248447.20 requests per second
throughput summary: 248818.11 requests per second
复制代码
3.绑定相同 node
throughput summary: 284414.09 requests per second
throughput summary: 284333.25 requests per second
throughput summary: 265252.00 requests per second
复制代码
根据测试结果,绑定到同一个 node,qps 大约提升 15%
使用绑定 CPU,你需要注意以下几点:
- Linux 下,你可以使用 「numactl --hardware」 查看硬件布局,确保支持并开启 NUMA。
- 线程要尽可能分布在 「不同的 CPU,相同的 node」,设置 CPU 亲和度才有效。否则会造成频繁上下文切换和远距离内存访问。
- 你要熟悉 CPU 架构,做好充分的测试。否则可能适得其反,导致 Redis 性能下降。
08 合理配置持久化策略
Redis 支持两种持久化策略,RDB 和 AOF。
RDB 通过 fork 子进程,生成数据快照,二进制格式。
AOF 是增量日志,文本格式,通常较大。会通过 AOF rewrite 重写日志,节省空间。
除了手动执行「BGREWRITEAOF」命令外,以下 4 点也会触发 AOF 重写
- 执行「config set appendonly yes」命令
- AOF 文件大小比例超出阈值,「auto-aof-rewrite-percentage」
- AOF 文件大小绝对值超出阈值,「auto-aof-rewrite-min-size」
- 主从复制完成 RDB 加载
RDB 和 AOF,都是在主线程中触发执行。虽然具体执行,会通过 fork 交给后台子进程。但 fork 操作,会拷贝进程数据结构、页表等,当实例内存较大时,会影响性能。
AOF 支持以下三种策略。
- appendfsync no:由操作系统决定执行 fsync 时机。 对 Linux 来说,通常每 30 秒执行一次 fsync,将缓冲区中的数据刷到磁盘上。如果 Redis qps 过高或写 big key,可能导致 buffer 写满,从而频繁触发 fsync。
- appendfsync everysec: 每秒执行一次 fsync。
- appendfsync always: 每次「写」会调用一次 fsync,性能影响较大。
AOF 和 RDB 都会对磁盘 IO 造成较高的压力。其中,AOF rewrite 会将 Redis hash 表所有数据进行遍历并写磁盘。对性能会产生一定的影响。
线上业务 Redis 通常是高可用的。如果对缓存数据丢失不敏感。考虑关闭 RDB 和 AOF 以提升性能。
如果无法关闭,有以下几点建议:
- RDB 选择业务低峰期做,通常为凌晨。保持单个实例内存不超过 32 G。太大的内存会导致 fork 耗时增加。
- AOF 选择 appendfsync no 或者 appendfsync everysec。
- AOF auto-aof-rewrite-min-size 配置大一些,如 2G。避免频繁触发 rewrite。
- AOF 可以仅在从节点开启,减轻主节点压力。
根据本地测试,不开启 AOF,写性能大约能提升 20% 左右。
09 使用长连接
Redis 是基于 TCP 协议,请求-响应式服务器。使用短连接会导致频繁的创建连接。
短连接有以下几个慢速操作:
- 创建连接时,TCP 会执行三次握手、慢启动等策略。
- Redis 会触发新建/断开连接事件,执行分配/销毁客户端等耗时操作。
- 如果你使用的是 Redis Cluster,新建连接时,客户端会拉取 slots 信息初始化。建立连接速度更慢。
所以,相对于性能快速的 Redis,创建连接是十分慢速的操作。
「建议使用连接池,并合理设置连接池大小」。
但使用长连接时,需要留意一点,要有「自动重连」策略。避免因网络异常,导致连接失效,影响正常业务。
10 关闭 SWAP
SWAP 是内存交换技术。将内存按页,复制到预先设定的磁盘空间上。
内存是快速的,昂贵的。而磁盘是低速的,廉价的。
通常使用 SWAP 越多,系统性能越低。
Redis 是内存数据库,使用 SWAP 会导致性能快速下降。
建议留有足够内存,并关闭 SWAP。
总结
以上就是今天为大家分享的 「提升 Redis 性能的 10 个手段」。
我绘制了思维导图,方便大家记忆。
可以看到,性能优化并不容易,需要我们了解很多底层知识,并做出充分测试。在不同机器、不同系统、不同配置下,Redis 都会有不同的性能表现。
「建议大家根据实际情况,充分测试,合理优化。」
如果你有更好的 Redis 性能优化手段,欢迎在评论区一起交流~
-End-
作者:虎珀
链接:https://juejin.cn/post/7071265791005425678
相关推荐
- Java培训机构,你选对了吗?(java培训机构官网)
-
如今IT行业发展迅速,不仅是大学生,甚至有些在职的员工都想学习java开发,需求量的扩大,薪资必定增长,这也是更多人选择java开发的主要原因。不过对于没有基础的学员来说,java技术不是一两天就能...
- 产品经理MacBook软件清单-20个实用软件
-
三年前开始使用MacBookPro,从此再也不想用Windows电脑了,作为生产工具,MacBook可以说是非常胜任。作为产品经理,值得拥有一台MacBook。MacBook是工作平台,要发挥更大作...
- RAD Studio(Delphi) 本月隆重推出新的版本12.3
-
#在头条记录我的2025#自2024年9月,推出Delphi12.2版本后,本月隆重推出新的版本12.3,RADStudio12.3,包含了Delphi12.3和C++builder12.3最...
- 图解Java垃圾回收机制,写得非常好
-
什么是自动垃圾回收?自动垃圾回收是一种在堆内存中找出哪些对象在被使用,还有哪些对象没被使用,并且将后者删掉的机制。所谓使用中的对象(已引用对象),指的是程序中有指针指向的对象;而未使用中的对象(未引用...
- Centos7 初始化硬盘分区、挂载(针对2T以上)添加磁盘到卷
-
1、通过命令fdisk-l查看硬盘信息:#fdisk-l,发现硬盘为/dev/sdb大小4T。2、如果此硬盘以前有过分区,则先对磁盘格式化。命令:mkfs.文件系统格式-f/dev/sdb...
- 半虚拟化如何提高服务器性能(虚拟化 半虚拟化)
-
半虚拟化是一种重新编译客户机操作系统(OS)将其安装在虚拟机(VM)上的一种虚拟化类型,并在主机操作系统(OS)运行的管理程序上运行。与传统的完全虚拟化相比,半虚拟化可以减少开销,并提高系统性能。虚...
- HashMap底层实现原理以及线程安全实现
-
HashMap底层实现原理数据结构:HashMap的底层实现原理主要依赖于数组+链表+红黑树的结构。1、数组:HashMap最底层是一个数组,称为table,它存放着键值对。2、链...
- long和double类型操作的非原子性探究
-
前言“深入java虚拟机”中提到,int等不大于32位的基本类型的操作都是原子操作,但是某些jvm对long和double类型的操作并不是原子操作,这样就会造成错误数据的出现。其实这里的某些jvm是指...
- 数据库DELETE 语句,还保存原有的磁盘空间
-
MySQL和Oracle的DELETE语句与数据存储MySQL的DELETE操作当你在MySQL中执行DELETE语句时:逻辑删除:数据从表中标记为删除,不再可见于查询结果物理...
- 线程池—ThreadPoolExecutor详解(线程池实战)
-
一、ThreadPoolExecutor简介在juc-executors框架概述的章节中,我们已经简要介绍过ThreadPoolExecutor了,通过Executors工厂,用户可以创建自己需要的执...
- navicat如何使用orcale(详细步骤)
-
前言:看过我昨天文章的同鞋都知道最近接手另一个国企项目,数据库用的是orcale。实话实说,也有快三年没用过orcale数据库了。这期间问题不断,因为orcale日渐消沉,网上资料也是真真假假,难辨虚...
- 你的程序是不是慢吞吞?GraalVM来帮你飞起来性能提升秘籍大公开
-
各位IT圈内外的朋友们,大家好!我是你们的老朋友,头条上的IT技术博主。不知道你们有没有这样的经历:打开一个软件,半天没反应;点开一个网站,图片刷不出来;或者玩个游戏,卡顿得想砸电脑?是不是特别上火?...
- 大数据正当时,理解这几个术语很重要
-
目前,大数据的流行程度远超于我们的想象,无论是在云计算、物联网还是在人工智能领域都离不开大数据的支撑。那么大数据领域里有哪些基本概念或技术术语呢?今天我们就来聊聊那些避不开的大数据技术术语,梳理并...
- 秒懂列式数据库和行式数据库(列式数据库的特点)
-
行式数据库(Row-Based)数据按行存储,常见的行式数据库有Mysql,DB2,Oracle,Sql-server等;列数据库(Column-Based)数据存储方式按列存储,常见的列数据库有Hb...
- AMD发布ROCm 6.4更新:带来了多项底层改进,但仍不支持RDNA 4
-
AMD宣布,对ROCm软件栈进行了更新,推出了新的迭代版本ROCm6.4。这一新版本里,AMD带来了多项底层改进,包括更新改进了ROCm的用户空间库和AMDKFD内核驱动程序之间的兼容性,使其更容易...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (74)
- oracle批量插入数据 (65)
- oracle事务隔离级别 (59)
- oracle 空为0 (51)
- oracle主从同步 (56)
- oracle 乐观锁 (53)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)