前言
最近在回顾&学习Redis相关的知识,也是本着分享的态度和知识点的记录在这里写下相关的文章。希望能帮助各位读者学习或者回顾到Redis相关的知识,当然,本人才疏学浅,在学习和写作的过程中难以会出现说文章的内容有出入或错误,也希望读者们能及时指出,我会及时加以修正。谢谢,加油!
那么接下来,就让我们一起来学习Redis相关的知识点吧!
哦?缓存
什么是缓存
什么是缓存呢?缓存就是数据交换的缓冲区(称作:Cache),这里我们可以简单的理解为存储在内存中的一个区域或数据。
缓存的作用
那么缓存究竟有什么作用呢?缓存的作用,主要有两个作用:高性能、高并发。
- 高性能
在不使用缓存的情况下,通常查询所需数据需要从数据库(如:MySQL)中读取,读取结果后我们还可能对结果进行一系列的复杂且耗时的计算,进而得出的我们真正想要的结果。在操作结果不会频繁发生变化的情况下,使用缓存后,查询请求一过来,直接从内存查询获取到最终的结果,免除了重新进行复杂且耗时的计算,性能极大的提高。 - 高并发
缓存走内存,天然支持高并发(单机甚至可支持到上万QPS),而相对,数据库来说,一般并发请求量在2000QPS左右(单机)。
缓存单机承载并发量是MySQL单机的几十倍。(归根到底,还是因为缓存走内存,执行效率高,可以快速查询到我们想要的结果,立马返回结束请求)
使用缓存后常见的缓存问题
- 缓存与数据库双写一致性
- 缓存雪崩
- 缓存穿透
- 缓存击穿
- 缓存并发竞争
(后续会针对这些问题进行分析)
常见的缓存问题分析
缓存雪崩
随机保存key的过期时间、缓存预热、缓存高可用架构、缓存持久化方案的实施
缓存穿透
布隆过滤器、保存空值
缓存击穿
热点Key问题,能不设置过期时间就不设置、key过期的情况下加锁控制并发从数据库中读取,避免高并发情况下由于缓存过期直接将请求打到数据库使得数据库宕机。
缓存与数据库的双写一致性
说起使用缓存,不得不提及这样一个重要的问题,即缓存和数据库的数据一致性问题(保证缓存和数据库保存的数据一致)。
Cache Aside Pattern
介绍
Cache Aside Pattern是一个由国外友人提出的经典的缓存+数据库的读写模式,该模式分为:
- 读实践:
- 先读缓存,如果缓存中读取到值,则返回;
- 若缓存中没有则查询数据库,计算值;
- 将计算结果放入缓存中,返回结果。
- 写实践:
- 先更新数据库;
- 再删除缓存。
进一步分析
针对Cache Aside Pattern的写实践我们来进行讨论如下几个问题:
- 问:为什么不更新缓存而是删除缓存?
- “先更新数据库,再删除缓存”是否会出现数据不一致问题呢?
- 删除缓存失败,缓存内存储旧数据而数据库存储的是新数据,出现数据不一致问题,不满足业务要求。
- 为什么不“先删除缓存,再更新数据库”呢?这种情况是否能保证数据一致呢?
- 更新数据库失败,但缓存内存储的数据同数据库一致,满足业务要求。
- 但是如果出现并发情况,请求A先删除缓存,此时有另外个请求B进行读取,发生缓存未命中,从数据库读取旧数据又保存到缓存中,接着请求A继续更新数据库。那么此时同样会出现缓存同数据库数据不一致问题。
- …
这里的数据一致性问题,归根到底,还是操作的原子性问题(要么一起成功,要么一起失败)。
TODO
总结
针对这个问题,是实际场景中,如果业务上允许缓存与数据库存在短暂或一段时间的数据不一致,那么我们尽可能地不去采用“缓存读写请求串行化的”等方案去解决数据不一致的问题。
缓存读写请求串行化:
- QPS、吞吐量降低
- 读请求长阻塞
- 多服务实例服务部署的请求路由(将请求打到同一台机)
- 热点数据路由,请求倾斜问题
Redis的线程模型
(支持多线程的 Redis 6.0 版本于 2020-05-02 终于发布,所以在这讲的是Redis单线程版的!!!需要注意)
客户端与Redis的通信流程
这里先不说一些比较概念性的内容,而是先对《客户端与Redis的通信流程》结合上图做一个整体的描述说明,阅读期间可能会因为一些专有名词而产生疑惑甚至出现阅读障碍,但是不要方,hold住,先看完有个整体大概的印象,知道大概大概后再结合后续具体的说明会进一步了解。(当然这里阅读之前,你需要对网络Socket编程有些理解,不懂?没事,先去了解下再回来继续看吧。可以了解下Java的Socket编程会很有意思哦,写个Demo,写个简单的“聊天室”,对你来说不会是个难事啦~~)
来吧,让我们来看看这个通信流程是个怎样的肥事。
- 在Redis启动初始化,会将Server Socket的
AE_READABLE
事件与连接应答处理器关联。(我***~~~AE_READABLE
事件、连接应答处理器 咩玩意哦。听我的,先结合图尽可能理解先) - 客户端发起同Redis服务端建立连接时,Server Socket会产生一个
AE_READABLE
事件,IO多路复用程序监听到该事件后,会将该Socket产生的AE_READABLE
事件压入队列中。 - 文件事件分派器会从队列中取出服务端产生的这个
AE_READABLE
事件会交由连接应答处理器处理。(为什么是交给连接应答处理器处理呢?因为在一开始Server Socket的AE_READABLE
事件与连接应答处理器关联了哈!) - 连接应答处理器会创建一个同客户端连接通信的Socket,这里我们假定该Socket为 socket01,并将 socket01的
AE_READABLE
事件与命令请求处理器关联。 - 客户端发送请求,如:set key value,此时与客户端连接的socket01会产生
AE_READABLE
事件,同样地,IO多路复用程序监听到socket01的事件后,会将socket01产生的AE_READABLE
事件压入队列中。 - 文件事件分派器从队列中取出socket01产生的
AE_READABLE
事件会交由命令请求处理器进行处理。(因为在第 4 步中已经将 socket01的AE_READABLE
事件与命令请求处理器关联 了哈) - 命令请求处理器执行客户端的请求命令,如:在内存中实现对key的set操作,设置为新的值,接着将socket01的
AE_WRITABLE
事件与命令回复处理器关联。 - 客户端准备好接收请求结果后,Redis中的socket01会产生一个
AE_WRITABLE
事件,同样会经由IO多路复用程序压入队列中。 - 文件事件分派器从队列中取出socket01产生的
AE_WRITABLE
事件会交由命令回复处理器进行处理。(因为在第 7 步中已经将 socket01的AE_WRITABLE
事件与命令回复处理器关联 了哈) - 命令回复处理器会向socket01输入本次操作的结果,并最后将 socket01的
AE_WRITABLE
事件与命令回复处理器的关联解除,最终完成一次与客户端的通信。
Redis线程模型说明
Redis基于Reactor模型开发了网络事件处理模型,称为文件事件处理器。采用IO多路复用机制,同时监听多个socket,当socket产生对应事件时,会根据socket的事件来(文件事件分派器)选择相应的事件处理器来处理事件。
文件事件处理器,包括:多个socket、IO多路复用程序、文件事件分派器、事件处理器。其中事件处理器,包括:连接应答处理器、命令请求处理器、命令回复处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
为什么Redis使用的是单线程模型但效率还是很高呢?
- 单线程操作,反倒避免了多线程上下文切换的问题&多线程共享资源的竞争问题。
- 基于IO多路复用机制,非阻塞。
Redis的过期策略&淘汰机制
这一节,让我们来了解下Redis的过期策略和内存淘汰机制。了解这些知识点的原因,在于可以帮助我们清楚地知道Redis针对过期的key是如何进行处理的,解答类似“为什么这个key过期了,但还占用着内存”等问题,也可以让我们了解到在Redis可使用的内存满了情况下,它又是如何处理的,解答类似“为什么我一些没有设置过期时间的key也被删除了呢”等问题。
Redis的过期策略
针对过期的key,Redis是如何处理的呢?已过期的key不会立马被删除。在默认情况下,Redis每隔100ms会随机抽取一些设置了过期时间的key,检查其是否过期,如已过期则进行删除。为什么是随机抽取一些呢?因为在定期删除的情况下,如果设置了过期时间的key真的很多的话,Redis需要全部检查这些key是否真的过期,那将是一个CPU负载很高的操作。
从上面我们了解到Redis的定期删除可能会遗漏掉一些真的已经过期了的key,那么这些key又是怎么被删除的呢?
Redis的惰性删除,即在访问某个key的时候,会先校验该key是否已过期,如果已过期则进行删除,并返回空结果。在定期删除的过程中,如果已经过期的key没有被抽取到,则在访问该key的时候将会被删除。
总结,Redis的过期策略:定期删除+惰性删除。
Redis的内存淘汰机制
采用Redis这样的过期策略,仍会出现很多过期的key没有被删除的情况(被定期删除遗漏,又没有及时访问走惰性删除),堆积在内存中,后续又有新的key存到内存中,直到Redis可使用的内存耗尽,那此时Redis又是如何处理的呢?
答案是:走Redis的内存淘汰机制。
Redis会依据某种策略会一些key进行淘汰,腾出内存空间。其中最常用的策略为:allkeys-lru,即当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用(LRU)的 key。(其他几种Redis的内存淘汰机制,我就不在这里做过多说明啦,可参考其他文献)
Redis使用的是一种近似LRU的算法实现的 allkeys-lru 内存淘汰。LRU算法,参考:缓存淘汰算法–LRU算法。
额外分享:值得了解的是,LRU算法在Java中可以使用 LinkedHashMap 快速实现。
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int CACHE_SIZE;
public LRUCache(int cacheSize) {
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > CACHE_SIZE;
}
}
Redis持久化
TODO
持久化方式:
RDB
AOF
持久化方式的对比
Redis主从架构
TODO
Redis集群工作原理等
参考资料
- Java面试——缓存
- 帮你解读什么是Redis缓存穿透和缓存雪崩(包含解决方案)
- 缓存穿透、缓存击穿、缓存雪崩区别和解决方案
- 究竟先操作缓存,还是数据库?
- 缓存,究竟是淘汰,还是修改?
- 彻底搞懂Redis的线程模型
- 彻底弄懂Redis的内存淘汰策略
- 缓存淘汰算法–LRU算法
总结
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)