Redis与缓存解读
缓存
在业务开发中,必然会存在需要频繁访问的数据即热点数据,如果通过访问数据库访问这些数据,由于数据存储在磁盘上,在频繁访问下会进行频繁的IO操作,会导致数据库压力过大,响应速度变慢。
那么我们可以在添加一层中间缓存层,将热点数据缓存在内存中,在访问数据时我们不在直接查询数据库,而是先访问缓存,如果数据存在(命中),直接返回即可。如果数据不存在(未命中),再访问数据库。
redis 将数据存储在内存中,因此可以提供接近于内存的访问速度。所以 redis 天然适合作为缓存层。
缓存并不是万能的,实际上缓存更使用于读密集场景,在写密集场景中由于需要保证缓存于数据库的一致性,在修改缓存时还需要修改数据库,反而加重了后端压力。
缓存优缺点
优点:
- 降低后端负载
- 提高读写效率,降低响应时间
缺点:
- 增加数据一致性成本
- 增加代码维护成本
缓存更新策略
为了保证缓存数据有效,我们需要更新缓存。这里主要有六种缓存更新策略:
超时剔除
在 redis 中我们可以设置数据的生存时间(TTL),在超时后,redis会自动删除缓存,在下次查询该数据时,由于缓存不存在,会重新写入缓存,完成更新。
这种方法实现简单,但一致性一般,在缓存未过期之前,对数据库的数据进行增删查改都不会影响缓存,用户查到的数据始终是旧数据。
先删缓存再更新数据库
在对数据库进行更新时,先删除缓存,然后更新数据,在下次查询该数据时,由于缓存不存在,会重新将新数据写入缓存,完成更新。
这种方法也无法保证数据的一致性。假设有两个线程,线程A 与 线程B , 在线程 A 更新缓存时,线程 B 发起查询,可能出现这种情况:
在这种情况下,一直到下次数据更新之前,缓存始终不一致,因此不推荐使用这种方法。
旁路缓存(先更新数据库,再删缓存)
在更新数据时,先更新数据库,再删除缓存,在下次查询该数据时,由于缓存不存在,会重新将新数据写入缓存,完成更新。
这种方法同样无法完全保证数据的一致性,但他是最常用的更新策略。因为它发生问题的概况较小,假设有两个线程,线程A 与 线程B , 在线程 B 更新数据库时,线程 A 发起查询,可能出现这种情况:
同样这种情况会出现数据不一致问题,但这种情况出现概率非常小,出现这种情况需要至少满足四个条件:
- 读操作所读数据缓存失效
- 有个并发的写操作
- 写操作比读操作更快
- 读操作早于写操作进入数据库,晚于写操作更新缓存
这样的条件是十分苛刻的,即使发生也是小概率事件,即使出现也可以通过缓存生存时间兜底。
这种方法最大的问题是删除缓存后的并发问题即缓存击穿问题,在缓存常见问题我们会介绍。
先更新数据库,再更新缓存
在更新数据时,先更新数据库,再更新缓存。
理论上这种方式比先更新数据库再删缓存有着更高的读性能,因为它事先准备好数据。但由于要更新数据库和缓存两块数据,所以它的写性能就比较低,同时他也不能完全保证数据的一致性。
假设有两个线程,线程A 与 线程B , 在线程 A B 同时更新,可能出现这种情况:
读写穿透
客户端只与缓存交互,缓存负责与数据库的交互。
读操作先查询缓存,如果缓存未命中,则缓存从数据库加载数据并写入缓存。写操作是直接写缓存,然后缓存同步更新数据库。这种模式下,缓存和数据库的一致性由缓存中间件维护。
异步缓存写入模式
客户端只与缓存交互,缓存异步地将数据更新到数据库,实现最终一致性。
这种模式适用于写操作频繁的场景,但可能会导致数据一致性问题。
缓存常见问题
使用缓存比较常见的问题有下面三种问题:缓存击穿,缓存雪崩,缓存穿透。
缓存穿透
在我们的业务逻辑中,如果客户端访问的数据不存在于缓存我们会访问数据库,如果数据库存在数据就写入缓存,如果不存在就返回,那么如果客户端不怀好意,频繁发起对不存在数据的请求会发生什么呢?大量请求会直接打入数据库,增大后端压力,实现对服务器的攻击。
解决方案有很多种,最常见的有两种方法:缓存空对象,布隆过滤器。
缓存空对象:当请求的数据在数据库中不存在时,我们将这个“不存在”的结果缓存起来,设置一个较短的过期时间。 这样,相同的请求在缓存失效之前会直接命中缓存,减轻数据库的压力。
布隆过滤器:使用布隆过滤器存储所有可能查询的键,当请求到达时,先通过布隆过滤器判断键是否存在。如果布隆过滤器认为键不存在,则直接返回,不进行数据库查询和缓存操作。
缓存雪崩
缓存雪崩是指在高并发系统中,大量的缓存数据在同一时间过期或被清除,导致大量请求同时涌向数据库,从而对数据库造成巨大压力,甚至可能导致数据库宕机。类似于“雪崩”。
常见的解决方法有以下几种:
设置不同的过期时间: 对于缓存中的每个数据项,设置不同的过期时间,这样可以避免大量数据同时过期。例如,可以为每个数据项的过期时间加上一个随机值。
使用互斥锁: 当缓存数据过期时,如果多个请求同时到达,使用互斥锁确保只有一个请求去查询数据库并更新缓存,其他请求等待或重试。
热点数据永不过期: 对于访问非常频繁的热点数据,可以考虑设置为永不过期,或者设置一个非常长的过期时间。
缓存击穿
在高并发的访问下,当某个热点数据缓存处于过期失效的时间点时,极有可能出现多个线程同时查询该缓存。而查询数据库更新缓存又需要消耗一定时间,在同一时间会有大量并发请求直接访问数据库而导致数据库服务器的CPU或者内存负载过高,服务能力下降甚至宕机。
那么如何解决这个问题呢?有三种解决方案。
- 加锁:在缓存失效后,通过加锁的方式只允许一个线程查询数据和写缓存,其他线程阻塞等待。这个方法会造成部分请求等待。
- 二级缓存:A1为原始缓存,A2为拷贝缓存。A1失效时,可以访问A2,其中A1的缓存失效时间设置为短期(比如5min),A2的缓存失效时间设置为长期(比如1天)。如果缓存value很大,此方案的缓存空间利用率低。
- 双key:思路和方案2类似,不同的是双key分别缓存过期时间(key-time)和缓存数据(key-data),其中(key-time)的缓存失效时间设置为短期(比如5min),(key-data)的缓存失效时间设置为长期(比如1天)。当第一个线程发现 key-time 过期不存在时,则先更新key-time,然后去查询数据库并更新key-data 的值;当其他线程来获取数据时,虽然第一个线程还没有从数据库查询完毕并更新缓存,但发现key-time存在,会直接读取缓存的旧数据返回。和二级缓存的方案对比,该方案的缓存空间利用率高。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
相关文章
RedisDesktopManager远程连接redis的实现
本文主要介绍了RedisDesktopManager远程连接redis的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2022-05-05
最新评论