Redis总结
1. 基本概念
1.1 架构
1.1.1 数据库
基于内存的NoSql,默认有16个db,采用select 0/15的方式切换db
1.1.2 单线程
1)定义
Redis4中执行命令的模块采用单线程的方式处理客户端请求,采用IO多路复用的方式(类似于epoll)来监听多个套接字,最终将被激活的socket对应事件交由文件事件分配器分派给对应事件处理器来执行,如下图所示
图片摘自
2)优劣
- 优
- 易维护
- 不需要考虑多线程并发的问题
- 性能瓶颈在网络IO和内存,不在CPU
- 劣
- 采用单线程处理消息队列会降低IO效率,redis6中引入多线程解决了此问题
1.2 数据结构
k-v键值对的方式存储
k固定为字符串,采用字典(hashtable)的形式存储,每个key对应两张hashtable,为的是适应渐进式hash分批进行hash的特性
val可以为五种形式
- string
- list 链表
- hash hashmap
- set hashset
- zset treeset 采用分数进行排序
2.常见问题
2.1 内存过期与淘汰
2.1.1 内存过期机制
可以为key设置过期时间,这样在时间到时,就会从缓存中删除此数据;过期字典中记录各个key的过期时间
2.2.2 内存淘汰机制
- 设置了过期时间的keys
- lru volatile-lru
- random volatile-random
- ttl 即将过期 volatile-ttl
- 所有keys
- lru allkeys-lru
- random allkeys-random
除了以上还有不过期 no-eviction
2.2 持久化
2.2.1 rdb
大规模数据恢复+弱一致性
默认的方式,通过在内存中备份redis的数据库快照(这样短时间内,内存中有两份一样的数据),并定期将快照写入磁盘的方式,写入的周期如下
- save 900 1 #超过900s且有至少1个key变动,则触发保存---长期少量
- save 300 10 #超过300s且有至少10个key变动,则触发保存---中期中量
- save 60 10000 #超过60s且有至少10000个key变动,则触发保存---短期大量
2.2.2 aof
rdb辅助,强一致性
可选方式,作为rdb的补充,通过对redis中存储的数据分析,逆向分析出存储这些数据最少需要执行的命令,将其写入aof文件中,写入时只允许在尾部追加,如果aof写满,则创建新文件先写入其中,之后采用重命名的方式覆盖原文件
同步aof文件到磁盘可选取的周期如下
-
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
-
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
-
appendfsync no #让操作系统决定何时进行同步
或者通过手动触发的方式
-
save
通过save会立即阻塞所有操作并进行rdb文件的持久化
-
bgsave
通过bgsave可以在后台异步的完成rdb文件持久化,可以通过lastsave获取最近一次的保存时间
-
flushall
即使是清空所有数据库,也会触发一次rdb文件的持久化,只不过为空
2.3 事务
2.3.1 MULTI
打开命令缓存队列,之后输入的命令均会放入队列中,之后通过EXEC命令一次性执行队列中的命令,如果
- 有语法错误的命令,则整个队列都撤销
- 出现了命令与操作数不匹配(对string做INCR),则只撤销出现此类问题的命令
2.3.2 WATCH
对某个字段打开监控(上乐观锁),通过此方式,可以达到事务全部回滚的特性,使用时,首先客户端AWATCH K1,K1即为要监视的对象,之后使用MULTI打开队列,如果此时其他客户端对K1进行修改,那么如果队列中
-
涉及到关于K1的操作,整个队列中的命令都被撤销
-
否则,队列不被撤销
2.4 扩容
2.4.1 主从(垂直扩容-灾备/读写分离)
1)定义
通过在redis-2上调用salveof redis-1的IP 端口,即可实现手动设置主从关系(保持到下一次重启),这样主机会将rdb文件拷贝到从机,从而实现与主机内容同步。主从方式不会"真正"增加可以存储的数据,只是提高了主机的可靠性且可以将读压力转移到从机上
2)实现
设置主从关系时,一个机器只能有一个主机,一个主机可以有多个从机,可以设计多级slave,来分担同步压力
2.4.2 集群(水平扩容)
多个redis存储不同的数据,这样可以实现容量提升,提高有效性
2.5 IO多路复用
主要是同步/异步+阻塞/非阻塞IO对比
假设client和server建立了socket,在client发出read调用时
如果是
-
阻塞
client会一直阻塞到read系统调用返回,也即server返回了数据并且client将数据从内核空间拷贝到了用户空间,这也是传统的BIO采用的方式,这时需要每个socket都对应一个线程
-
非阻塞
client发出read调用后不会阻塞
由于不会阻塞,此时有两种解决方式
-
轮询
client周期性的发起read请求,检查是否有server传来的数据已经准备好
-
IO多路复用
-
select
建立一个数组包含所有要监听的socket,构建为一个待查队列,当用户进程调用
select()时会一直阻塞,直到其中一个socket就绪,同时返回活跃的socket数量,但是不知道具体哪些socket,用户进程需要遍历所有的socket才可以找到哪些socket活跃 -
poll
类似于select,采用链表方式构建待查队列
-
epoll
将需要监听的socket和其要执行的事件(如accept)与一个回调函数一并注册到epoll中,构成一个待查队列,其中的事件都还未得到响应;当待查队列上的某个事件激活时,会调用回调函数将此socket和对应事件插入到就绪队列中,那么用户进程每次在调用
epoll_wait(类似于select())时,只遍历就绪队列即可,这样省去了检查整个待查队列的开销 -
NIO-selector
NIO中的selector即Reactor模式中的多路选择(解复用)器,用户进程需要将socket(channel)和对应的事件注册到selector上,之后selector所在线程调用
select(),其会阻塞到注册的事件中至少有一个被激活,当select()返回之后调用selectedKeys()用于返回所有的就绪事件对应的key,之后用户进程只需要遍历返回的就绪时间队列,判断对应的事件类型,执行对应的方法即可(如read返回,则开始读数据)
-
-
当read系统调用返回后,如果
-
同步
那么client会一直阻塞等待接收server传来的数据,直到没有数据要传递
-
异步
那么client会为此事件保留一个callback,不会发生阻塞,当server传来的数据抵达client时,调用callback通知client来开始读取传来的数据
redis中,采用类似于selector的方式,构建了多路复用器,也即前面图中的文件事件分派器,从而避免阻塞等待请求系统调用响应
2.6 失效
2.6.1 缓存穿透
要访问的数据不在redis中时,就会到mysql中去获取,若有人恶意捏造不存在的数据请求,则会为mysql带来巨大压力,因而需要对传来的请求进行数据校验,保证其合法性
至于参数校验可以参考JavaGuide-布隆过滤器
2.6.2 缓存雪崩
redis中的数据大面积失效,这时大量的请求导向mysql,为mysql带来巨大压力,这时可以采用
-
提高可靠性
主从复制,或者集群多点备份数据
-
降流
采用消息队列缓存请求,避免冲击mysql
2.7 与memcached对比
主要有如下几点
- memcached的value只支持string
- memcached不能持久化