为什么需要多线程?
但是单线程也有单线程的苦恼,比如当我(Redis)需要删除一个很大的数据时,因为是单线程同步操作,这就会导致 Redis 服务卡顿,于是在 Redis 4.0 中就新增了多线程的模块,当然此版本中的多线程主要是为了解决删除数据效率比较低的问题的,他的相关指令有以下三个:
- unlink key
- flushdb async
- flushall async
执行示例如下所示:
> unlink key # 后台删除某个 key > OK # 执行成功 > flushall async # 清空所有数据 > OK # 执行成功
这样我就可以把这些坏人“瞬间”拉黑(删除)了。
所谓的“瞬间”删除其实有些夸张,只是从返回的结果来看是删除成功了,但是这只是把删除工作交给了后台的小弟(子线程)异步来删除数据了。
小贴士:正常情况下使用 del 指令可以很快的删除数据,而当被删除的 key 是一个非常大的对象时,例如时包含了成千上万个元素的 hash 集合时,那么 del 指令就会造成 Redis 主线程卡顿,因此使用惰性删除可以有效的避免 Redis 卡顿的问题。
Redis 6 中的多线程
之前在 Redis 4.0 中你说删除比较慢,骗我开大(多线程)来处理也就罢了,为毛 Redis 6.0 还要多线程嘞?
其实是这样的在 Redis 4.0 版本中虽然引入了多线程,但此版本中的多线程只能用于大数据量的异步删除,然而对于非删除操作的意义并不是很大。
但如果我们使用我们在非删除的环境下使用多线程的话就可以分摊 Redis 同步读写 I/O 的压力,以及充分的利用多核 CPU 的资源了,这样就可以有效的提升 Redis 的 QPS(Query Per Second,每秒查询率)了。
在 Redis 中虽然使用了 I/O 多路复用,并且是基于非阻塞 I/O 进行操作的,但 I/O 的读和写本身是堵塞的,比如当 socket 中有数据时,Redis 会通过调用先将数据从内核态空间拷贝到用户态空间,再交给 Redis 调用,而这个拷贝的过程就是阻塞的,当数据量越大时拷贝所需要的时间就越多,而这些操作都是基于单线程完成的。
I/O 多路复用,简单来说就是通过监测文件的读写事件,再通知线程执行相关操作,保证 Redis 的非阻塞 I/O 能够顺利执行完成的机制。
因此在 Redis 6.0 中新增了多线程的功能来提高 I/O 的读写性能,他的主要实现思路是将主线程的 IO 读写任务拆分给一组独立的线程去执行,这样就可以使多个 socket 的读写可以并行化了,但 Redis 的命令依旧是由主线程串行执行的。
需要注意的是 Redis 6.0 默认是禁用多线程的,可以通过修改 Redis 的配置文件 redis.conf 中的 io-threads-do-reads
等于 true
来开启多线程,完整配置为 io-threads-do-reads true
,除此之外我们还需要设置线程的数量才能正确的开启多线程的功能,同样是修改 Redis 的配置,例如设置 io-threads 4
表示开启 4 个线程。
小贴士:关于线程数的设置,官方的建议是如果为 4 核的 CPU,建议线程数设置为 2 或 3,如果为 8 核 CPU 建议线程数设置为 6,线程数一定要小于机器核数,线程数并不是越大越好。
关于 Redis 的性能,我的父王 antirez(Redis 作者)在 RedisConf 2019 分享时曾提到,Redis 6 引入的多线程 I/O 特性对性能提升至少是一倍以上。国内也有人在阿里云使用 4 个线程的 Redis 版本和单线程的 Redis 进行比较测试,发现测试的结果和 antirez 给出的结论基本吻合,性能基本可以提高一倍。
总结
Redis 虽然依靠自己的:基于内存操作、数据结构简单、多路复用和非阻塞 I/O、避免了不必要的线程上下文切换等特性,在单线程的环境下依然很快;但对于大数据的 key 删除还是卡的飞起,因此在 Redis 4.0 引入了多线程:unlink key/flushall async 等命令,主要用于 Redis 数据的删除,而在 Redis 6.0 中引入了 I/O 多线程的读写,这样就可以更加高效的处理更多的任务了,Redis 只是将 I/O 读写变成了多线程,而命令的执行依旧是由主线程串行执行的,因此在多线程下操作 Redis 不会出现线程安全的问题。
Redis 无论是当初的单线程设计,还是如今与当初设计相背的多线程,目的只有一个:让 Redis 变得越来越快。
所以 Redis 依旧没变,他还是那个曾经的追风少年~