- Redis是什么?
redis是一个键值对数据库,基于内存进行操作,也可以进行持久化 - Redis过期key是怎么样清理的?
(1)惰性清除
在访问key时,如果发现key已经过期,那么会将key删除 。
(2)定时清理
Redis配置项hz定义了serverCron任务的执行周期, 默认每次清理时间为25ms,每次清理会依次遍历所有DB,从db随机取出20个key,如果过期就删除,如果其中有5个key过期,那么就继续对这个db进行清理,否则开始清理下一个db。
(3)内存不够时清理
当执行写入命令时,如果发现内存不够,那么就会按照配置的淘汰策略清理内存,淘汰策略一般有6种,Redis4.0版本后又增加了2种,主要由分为三类
一共8种
第一类 不处理,等报错(默认的配置)
- noeviction,发现内存不够时,不删除key,执行写入命令时直接返回错误信息。(Redis默认的配置就是noeviction)
第二类 从所有结果集中的key中挑选,进行淘汰
- allkeys-random 就是从所有的key中随机挑选key,进行淘汰
- allkeys-lru 就是从所有的key中挑选最近使用时间距离现在最远的key,进行淘汰
- allkeys-lfu 就是从所有的key中挑选使用频率最低的key,进行淘汰。(这是Redis 4.0版本后新增的策略)
第三类 从设置了过期时间的key中挑选,进行淘汰这种就是从设置了expires过期时间的结果集中选出一部分key淘汰,挑选的算法有:
volatile-random 从设置了过期时间的结果集中随机挑选key删除。
volatile-lru 从设置了过期时间的结果集中挑选上次使用时间距离现在最久的key开始删除
volatile-ttl 从设置了过期时间的结果集中挑选可存活时间最短的key开始删除(也就是从哪些快要过期的key中先删除)
volatile-lfu 从过期时间的结果集中选择使用频率最低的key开始删除(这是Redis 4.0版本后新增的策略)
- LRU算法实现
- LFU算法实现
- Redis为什么是单线程的?
Redis是基于内存的操作,CPU不会成为瓶颈所在,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。 - 采用单线程的优势?
1.Redis项目的代码会更加清晰,处理逻辑会更加简单。
2.不用考虑多个线程修改数据的情况,修改数据时不用加锁,解锁,也不会出现死锁的问题,导致性能消耗。
3.不存在多进程或者多线程导致的切换而造成的一些性能消耗。 - 劣势?
无法利用多CPU的优势
不过可以通过在机器上启动多个Redis实例来利用资源。 - Redis的性能为什么这么高?(Redis单机可以到到10W的QPS(每秒处理请求数))
1.完全基于内存,数据全部存储在内存中,读取时没有磁盘IO,所以速度非常快。
2.Redis采用单线程的模型,没有上下文切换的开销,也没有竞态条件,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。
3.Redis项目中使用的数据结构都是专门设计的,例如SDS(简单动态字符串)是对C语言中的字符串频繁修改时,会频繁地进行内存分配,十分消耗性能,而SDS会使用空间预分配和惰性空间释放来避免这些问题的出现。 空间预分配技术: 对SDS进行修改时,如果修改后SDS实际使用长度为len,
当len<1M时,分配的空间会是2*len+1,也就是会预留len长度的未使用空间,其中1存储空字符
当len>1M时,分配的空间会是len+1+1M,也就是会预留1M长度的未使用空间,其中1存储空字符
4.采用多路复用IO模型,可以同时监测多个流的IO事件能力,在空闲时,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态唤醒,轮询那些真正发出了事件的流,并只依次顺序的处理就绪的流。可以让单个线程高效的处理多个连接请求(尽量减少网络 I/O 的时间消耗)。 - 多路复用模型是什么?
I/O复用指的是多个I/O连接复用一个进程。 最初级的I/O复用,就是一个进程对应多个连接,每次从头至尾进行遍历,判断是否有I/O事件需要处理,有的话就进行处理,缺点是效率比较低,如果一直没有事件进来,会导致CPU空转。 升级版的I/O复用模型 当没有I/O事件时,进程处于阻塞状态,当有I/O事件时,就会有一个代理去唤醒进程,去进行轮询,来处理I/O事件。(这里的代理也就是select和poll,select只能观察1024个连接,poll可以观察无限个连接) epoll是对select和poll的升级版,解决了很多问题,是线程安全的,而且可以通知进程是哪个Socket连接有I/O事件,提高了查找效率。 epoll和select/poll最大区别是 (1)epoll内部使用了mmap共享了用户和内核的部分空间,避免了数据的来回拷贝 (2)epoll基于事件驱动,epoll_wait只返回发生的事件避免了像select和poll对事件的整个轮寻操作。 - Linux中IO模型一共有哪些?
IO模型主要由阻塞式I/O模型,非阻塞式I/O模型,I/O复用模型,信息驱动式I/O模型,异步I/O模型。
- 阻塞式I/O模型
用户态进程调用recvfrom系统调用来接受数据,如果当前内核中数据没有准备好,那么会一直等待,不会进行其他操作,直到内核中的数据准备好,将数据拷贝到用户空间内存,然后recvfrom返回成功的信号,此时用户态进行才解除阻塞的状态,处理收到的数据。- 非阻塞时I/O模型
在非阻塞式I/O模型中,当进程等待内核的数据,而当该数据未到达的时候,进程会不断询问内核,直到内核准备好数据。
用户态进程调用recvfrom接收数据,当前并没有数据报文产生,此时recvfrom返回EWOULDBLOCK,用户态进程会一直调用recvfrom询问内核,待内核准备好数据的时候,之后用户态进程不再询问内核,待数据从内核复制到用户空间,recvfrom成功返回,用户态进程开始处理数据。- I/O多路复用模型
I/O复用指的是多个I/O连接复用一个进程。 最初级的I/O复用,就是一个进程对应多个连接,每次从头至尾进行遍历,判断是否有I/O事件需要处理,有的话就进行处理,缺点是效率比较低,如果一直没有事件进来,会导致CPU空转。升级版的I/O复用模型当没有I/O事件时,进程处于阻塞状态,当有I/O事件时,就会有一个代理去唤醒进程,去进行轮询,来处理I/O事件。(这里的代理也就是select和poll,select只能观察1024个连接,poll可以观察无限个连接)epoll是对select和poll的升级版,解决了很多问题,是线程安全的,而且可以通知进程是哪个Socket连接有I/O事件,提高了查找效率。epoll和select/poll最大区别是 (1)epoll内部使用了mmap共享了用户和内核的部分空间,避免了数据的来回拷贝 (2)epoll基于事件驱动,epoll_wait只返回发生的事件避免了像select和poll对事件的整个轮寻操作。- 信息驱动式I/O模型
是非阻塞的,当需要等待数据时,用户态进程会给内核发送一个信号,告知自己需要的数据,然后就去执行其他任务了,内核准备好数据后,会给用户态发送一个信号,用户态进程收到之后,会立马调用recvfrom,等待数据从从内核空间复制到用户空间,待完成之后recvfrom返回成功指示,用户态进程才处理数据。- 异步I/O模型
与信息驱动式I/O模型区别在于,是在数据从内核态拷贝到用户空间之后,内核才通知用户态进程来处理数据。在复制数据到用户空间这个时间段内,用户态进程也是不阻塞的。
- 阻塞和非阻塞
阻塞和非阻塞通常用来形容多线程间的相互影响 - 并行和并发
并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。 - 同步异步
同步和异步通常用来形容一次方法调用。
同步方法:调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。
异步调用:指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者 - 如何解决Redis缓存穿透问题?
Redis 缓存穿透指的是攻击者故意大量请求一些Redis缓存中不存在key的数据,导致请 求打到数据库上,导致数据库压力过大。
解决方案如下:
1.做好参数校验,无效的请求直接返回,只能避免一部分情况,攻击者总是可以找到一些没有覆盖的情况。2.对缓存中找不到的key,需要去数据库查找的key,缓存到Redis中,但是可能会导致Redis中缓存大量无效的key,可以设置一个很短的过期时间,例如1分钟。
3.可以使用布隆过滤器,将所有可能的存在的数据通过去hash值的方式存入到一个足够大的bitmap中去,处理请求时,通过在bitmap中查找,可以将不存在的数据拦截掉。
- 如何解决Redis缓存雪崩问题?
缓存雪崩主要指的是短时间内大量key失效,导致所有请求全部转向数据库,导致数据库压力过大。
解决方案:
1.在给缓存设置失效时间时加一个随机值,避免集体失效。2.双缓存机制,缓存A的失效时间为20分钟,缓存B没有失效时间,从缓存A读取数据,缓存A中没有时,去缓存B中读取数据,并且启动一个异步线程来更新缓存A。
- 如何解缓存与数据库的数据一致性问题?
首先需要明白会导致缓存与数据库的数据不一致的几个诱因: 多个写请求的执行顺序不同导致脏数据。 更新时正好有读请求,读请求取到旧数据然后更新上。或者数据库是读写分离的,在主库更新完之后,需要一定的时间,从库才能更新。
各种方法的优缺点
1.先更新数据库,后更新缓存
1.两个写请求,写请求A,写请求B,A先更新数据库,B后更新数据库,但是可能B会先更新缓存,A后更新缓存,这样就会导致缓存里面的是旧数据。
2.更新缓存时失败也有可能导致缓存是旧数据。2.先删除缓存,在更新数据库
1.删除缓存后,更新数据库之前假如正好有读请求,读请求把旧数据设置到缓存了。3.先更新数据库,再删除缓存
1.更新后删除缓存时,网络不好,删除失败也有可能导致缓存是旧数据。
正确的方案
1.写请求串行化
将写请求更新之前先获取分布式锁,获得之后才能更新,这样实现写请求的串行化,但是会导致效率变低。
2.先更新数据库,异步删除缓存,删除失败后重试
先更新数据库,异步删除缓存,删除缓存失败时,继续异步重试,或者将操作放到消息队列中,再进行删除操作。(如果数据库是读写分离的,那么删除缓存时需要延迟删除,否则可能会在删除缓存时,从库还没有收到更新后的数据,其他读请求就去从库读到旧数据然后设置到缓存中。)
3.业务项目更新数据库,其他项目订阅binlog更新
业务项目直接更新数据库,然后其他项目订阅binlog,接收到更新数据库操作的消息后,更新缓存,更新缓存失败时,新建异步线程去重试或者将操作发到消息队列,然后后续进行处理。但是这种方案更新mysql后还是有一定延迟,缓冲中才是新值。