Redis6.0版的新特性
redis在6.0版本之后更新了一些重要的新特性
前言
Redis 6 主要的变化有:SSL、ACLs、RESP3、客户端缓存、线程I/O、Redis benchmark 中的集群支持和改进的Redis cli 集群支持,以及Redis集群代理。
1、增加了多线程Thread I/O
6.0之前的redis基本上是一个单线程的,但并不是指只有一个线程,比如说执行unlink操作删除大key的时候(unlink和del命令一样都是用来删除key,但是unlink是异步的,适合删除大的key),会有单独的线程完成,不然会阻塞主线程,还有慢的IO操作的时候,也会使用单独的线程完成,还有比如持久化的时候,也是会有单独的线程来实现.
6.0之后增加了多线程的实现,多线程使用在io的操作上,工作线程还是只有一个单线程,还是串行实现的,多的io线程用于读read或者写write,
多线程不会同时执行读写操作。所有多出的线程要不是全部用于读,要不全部都是用于写。
6.0多线程的配置默认情况下是关闭的,需要通过配置开启
#开启多线程后,还需要设置线程数,否则是不生效的。同样修改redis.conf配置文件
io-threads 4 #默认开启4个线程,其中包含1个main线程和3个写线程,默认情况下不开启读线程
io-threads-do-reads no#默认情况下,开启了多线程之后是用来写操作的,
如果此设置为yes则表示新开启的线程用于读操作
关于线程数的设置,官方有一个建议:4核的机器建议设置为2或3个线程,8核的建议设置为6个线程,线程数一定要小于机器核数。还需要注意的是,线程数并不是越大越好,官方认为超过了8个基本就没什么意义了。
说明2:如果开启多线程,至少要4核的机器,且Redis实例已经占用相当大的CPU耗时的时候才建议采用,否则使用多线程没有意义。所以估计80%的公司开发人员看看就好。
-
- Redis6.0之前的版本真的是单线程吗?
Redis在处理客户端的请求时,包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。但如果严格来讲从Redis4.0之后并不是单线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 key 的删除等等。
-
- Redis6.0之前为什么一直不使用多线程?
官方曾做过类似问题的回复:使用Redis时,几乎不存在CPU成为瓶颈的情况, Redis主要受限于内存和网络。例如在一个普通的Linux系统上,Redis通过使用pipelining每秒可以处理100万个请求,所以如果应用程序主要使用O(N)或O(log(N))的命令,它几乎不会占用太多CPU。
使用了单线程后,可维护性高。多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。Redis通过AE事件模型以及IO多路复用等技术,处理性能非常高,因此没有必要使用多线程。单线程机制使得 Redis 内部实现的复杂度大大降低,Hash 的惰性 Rehash、Lpush 等等 “线程不安全” 的命令都可以无锁进行。
-
- Redis6.0为什么要引入多线程呢?
Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,对于小数据包,Redis服务器可以处理80,000到100,000 QPS,这也是Redis处理的极限了,对于80%的公司来说,单线程的Redis已经足够使用了。
但随着越来越复杂的业务场景,有些公司动不动就上亿的交易量,因此需要更大的QPS。常见的解决方案是在分布式架构中对数据进行分区并采用多个服务器,但该方案有非常大的缺点,例如要管理的Redis服务器太多,维护代价大;某些适用于单个Redis服务器的命令不适用于数据分区;数据分区无法解决热点读/写问题;数据偏斜,重新分配和放大/缩小变得更加复杂等等。
从Redis自身角度来说,因为读写网络的read/write系统调用占用了Redis执行期间大部分CPU时间,瓶颈主要在于网络的 IO 消耗, 优化主要有两个方向:
• 提高网络 IO 性能,典型的实现比如使用 DPDK 来替代内核网络栈的方式
• 使用多线程充分利用多核,典型的实现比如 Memcached。
协议栈优化的这种方式跟 Redis 关系不大,支持多线程是一种最有效最便捷的操作方式。所以总结起来,redis支持多线程主要就是两个原因:
• 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核
• 多线程任务可以分摊 Redis 同步 IO 读写负荷
-
- Redis6.0多线程的实现机制?
流程简述如下:
1、主线程负责接收建立连接请求,获取 socket 放入全局等待读处理队列
2、主线程处理完读事件之后,通过 RR(Round Robin) 将这些连接分配给这些 IO 线程
3、主线程阻塞等待 IO 线程读取 socket 完毕
4、主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行
5、主线程阻塞等待 IO 线程将数据回写 socket 完毕
6、解除绑定,清空等待队列
-
- 开启多线程后,是否会存在线程并发安全问题?
从上面的实现机制可以看出,Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。所以我们不需要去考虑控制 key、lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。
2、客户端缓存Client Side Cache
如果本地没有实现JVM缓存,那么在大并发的情况下对redis服务器也是一种考验,所以redis提出一种客户端缓存方案
主要实现过程如下图:
实现过程是:客户端 1从服务器获取某个key的value,然后服务器缓存下当前key的,并返回value给客户端1。客户端1拿到value之后,将key-value缓存在本地。如果另一个客户端2修改了key的value为value2,则服务端检查之后发现此key已经被缓存到客户端1,则会给客户端1发送一个失效key的value的通知,客户端1收到通知后,失效缓存在本地的key-value
3、ACL细粒度安全控制acces control list
可以根据命令和key来控制访问连接
在redis6之前,只能通过密码来控制,还有通过rename来调整高危命令flushdb,keys*,shutdown等命令的权限。
redis6之后,提供了更细粒度的权限控制
1、接入权限:用户名和密码
2、控制可以执行的命令
3、控制可以操作的key
#查看权限列表
127.0.0.1:6379> acl list
1) "user default on nopass ~* &* +@all"
# default表示用户名,on表示是否启用,nopass表示没有密码,~*表示可操作性的key, +@all表示可执行的命令
#增加用户权限
127.0.0.1:6379> acl setuser lilei
OK
127.0.0.1:6379> acl list
1) "user default on nopass ~* &* +@all"
2) "user lilei off &* -@all"
#查看当前操作用户
127.0.0.1:6379> acl whoami
"default"
#设置一个密码是123456 只能够使用get的命令的用户,
#并且在key前一定要加上`cached:`这个限制
127.0.0.1:6379> acl setuser lilei1 on >123456 ~cached:* +get
OK
#检查是否设置成功
127.0.0.1:6379> acl list
1) "user default on nopass ~* &* +@all"
2) "user lilei off &* -@all"
3) "user lilei1 on #8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92 ~cached:* &* -@all +get"
#切换用户操作
127.0.0.1:6379> auth lilei1 123456
OK
#没有权限的key,无法操作
127.0.0.1:6379> get a
(error) NOPERM this user has no permissions to access one of the keys used as arguments
#有权限的key可以正常操作
127.0.0.1:6379> get cached:a
(nil)
4、增加SSL模块
通过增加设置,在传输的时候使用SSL协议,确保传输过程的安全性
当SSL模块开启的时候,不能使用多线程
5、增加RESP3新的通信协议
增加RESP3同行协议,优化服务端和客户端之间的通信
#启用RESP3协议
127.0.0.1:6379> hello 3
1# "server" => "redis"
2# "version" => "6.2.5"
3# "proto" => (integer) 3
4# "id" => (integer) 4
5# "mode" => "standalone"
6# "role" => "master"
7# "modules" => (empty array)
#获取所有hash显示结构的变化(可读性更高一点)
127.0.0.1:6379> hgetall hash1
1# "a" => "1"
2# "b" => "2"
3# "c" => "3"
#未开启RESP3协议时展示的结构
127.0.0.1:6379> hgetall hash1
1) "a"
2) "1"
3) "b"
4) "2"
5) "c"
6) "3"