• Redis缓存穿透、击穿、雪崩,数据库与缓存一致性


    Redis作为高性能非关系型(NoSQL)的键值对数据库,受到了广大用户的喜爱和使用,大家在项目中都用到了Redis来做数据缓存,但有些问题我们在使用中不得不考虑,其中典型的问题就是:缓存穿透缓存雪崩缓存击穿与关系型数据库的一致性

    一、缓存穿透

    1、概念

    缓存穿透是指查询一个缓存和数据库不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存。

    大致流程如下图所示:

    在一些特定场景 例如:秒杀活动。同一时刻会有大量的请求,都在秒杀同一件商品,这些请求同时去查缓存中没有数据,然后又同时访问数据库。结果悲剧了,数据库可能扛不住压力,直接挂掉。

    也会存在有人恶意请求。一般我们的主键ID都是无符号的自增类型,有些人想要搞垮你的数据库,每次请求都用负数ID,而ID为负数的记录在数据库根本就没有。就会每次都去查询数据库,而每次查询都是空,每次又都不会进行缓存。假如有恶意攻击,就可以利用这个漏洞,对数据库造成压力,甚至压垮数据库。

    3、解决方案

    1) 验证拦截

    接口层进行校验,如鉴定用户权限,对ID之类的字段做基础的校验,如id<=0的字段直接拦截。

    2) 布隆过滤器

    我们可以提前将真实正确的商品id,添加到过滤器当中,每次再进行查询时,先确认要查询的id是否在过滤器当中,如果不在,则说明id为非法id,则不需要进行后续的查询步骤了。

    布隆过滤器是一种比较独特数据结构,有一定的误差。布隆过滤器的特点就是 如果它说不存在那肯定不存在,如果它说存在,那数据有可能实际不存在

    它最大的优点就是性能高,空间占用率及小。

    3) 缓存空对象

    当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源。

    但是这种方法会存在两个问题:

    • 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
    • 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

    二、缓存击穿

    1、概念

    缓存击穿,是指缓存中没有但数据库中有的数据,并且某一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key缓存时间到期,持续的大并发就穿破缓存,直接请求数据库,导致压垮数据库。

    2、解决方案

    1)设置热点数据永远不过期。

    这个方法就比较粗暴,,如果你的热点数据要求实时性比较低,那么可以设置热点数据在热点时段不过期,在访问低峰期过期,比如每天凌晨过期。

    2) 使用分布式锁

    互斥锁可以控制查询数据库的线程访问,但这种方案会导致系统的吞吐量下降,需要根据实际情况使用。

    public static String getData(String key) throws InterruptedException {
    	//从Redis查询数据 
    	String result = getDataByKV(key);
    	//参数校验
    	if (StringUtils.isBlank(result)) {
    		try {
    			//获得锁
    			if (reenLock.tryLock()) {
    				//去数据库查询 
    				result = getDataByDB(key);
    				//校验
    				if (StringUtils.isNotBlank(result)) {
    				//插进缓存 
    				setDataToKV(key, result); 
    				}
    			} else {
    			//睡一会再拿 
    			Thread.sleep(100L); 
    			result = getData(key); 
    			} 
    		} finally {
    		//释放锁 
    		reenLock.unlock(); 
    		} 
    	}
    return result; 
    }
    

    三、缓存雪崩

    1、概念

    缓存雪崩表示在某一时间段缓存集中失效,导致请求全部走数据库,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

    使缓存集中失效的原因:

    1)、redis服务器挂掉了。

    Redis 集群产生了大面积故障;缓存失败,此时仍有大量请求去访问 Redis缓存服务器;在大量 Redis 请求失败后,这些请求将会去访问数据库;

    2、对缓存数据设置了相同的过期时间,导致某时间段内缓存集中失效。

    2、解决方案

    1)实现Redis的高可用

    【事前】搭建Redis 哨兵(Sentinel) 或 Redis 集群(Cluster) 都可以做到高可用;

    【事中】缓存降级(临时支持):当访问次数急剧增加导致服务出现问题时,我们如何确保服务仍然可用。在国内使用比较多的是 Hystrix,它通过熔断、降级、限流三个手段来降低雪崩发生后的损失。只要确保数据库不死,系统总可以响应请求。

    【事后】Redis备份和快速预热:Redis数据备份和恢复、快速缓存预热。

    2)缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。

    (1)采取不同分类商品,缓存不同周期。在同一分类中的商品,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。

    (2)如果缓存数据库是分布式部署,将 热点数据均匀分布在不同的缓存数据库中。

    (3)设置热点数据永远不过期


    四、数据库与缓存一致性

    使用缓存,可以降低耗时,提供系统吞吐性能。但是,使用缓存,会存在数据一致性的问题。

    1、旁路缓存模式

    一般我们使用缓存,都是旁路缓存模式,它的特点就是读的时候插入缓存,写的时候删除缓存

    1)读请求流程如下:

    • 读的时候,先读缓存,缓存命中的话,直接返回数据;
    • 缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应。

    2)写流程如下:

    这里就有两个问题思考:

    1)为什么写请求要做删除库存操作,而不是做插入缓存动作?

    2)为什么是先操作数据库在删除旧的缓存,能对换一下顺序吗?

    2、删除缓存呢,还是更新缓存?

    我们在操作缓存的时候,到底应该删除缓存还是更新缓存呢?我们先来看个例子:

    1. 线程A先发起一个写操作,第一步先更新数据库;
    2. 线程B再发起一个写操作,第二步更新了数据库;
    3. 由于网络等原因,线程B先更新了缓存;
    4. 线程A更新缓存。

    这时候,缓存保存的是A的数据(老数据),数据库保存的是B的数据(新数据),数据不一致了,脏数据出现啦。如果是删除缓存取代更新缓存则不会出现这个脏数据问题。

    3、先操作数据库还是先操作缓存

    双写的情况下,先操作数据库还是先操作缓存?我们再来看一个例子:假设有A、B两个请求,请求A做更新操作,请求B做查询读取操作。

    1. 线程A发起一个写操作,第一步del cache;
    2. 此时线程B发起一个读操作,cache miss;
    3. 线程B继续读DB,读出来一个老数据;
    4. 然后线程B把老数据设置入cache;
    5. 线程A写入DB最新的数据;

    这样就有问题啦,缓存和数据库的数据不一致了。缓存保存的是老数据,数据库保存的是新数据。因此,Cache-Aside缓存模式,选择了先操作数据库而不是先操作缓存。


    声明

    公众号如需转载该篇文章,那麻烦在文章的头部 声明是转至公众号: 后端元宇宙。尊重作者辛苦劳动果实嘛。同时也可以问本人要该文章markdown原稿和原图片。其它情况一律禁止转载哦!

  • 相关阅读:
    Oneops运维系统
    采集日志工具fluentbit输出到kafka
    logstash
    Redux学习笔记
    部分浏览器不支持 replaceAll的解决
    windows10系统文件浏览器不显示图片缩略图
    C#项目版本号自定义位置自动向上增加小工具设计与实现
    父类和子类同名的变量和方法决定于哪个
    JdbcTemplate基本使用 增删该差API
    Tomcat结合Web Maven
  • 原文地址:https://www.cnblogs.com/qdhxhz/p/15721525.html
Copyright © 2020-2023  润新知