Redis多级缓存架构、缓存设计、布隆过滤器

多级缓存架构

在这里插入图片描述

  • Nginx层:Lua动态渲染模板
    一些静态资源、例如HTML、CSS、JS、图片资源等都可以独立部署在一台服务器上、加载进Redis缓存中。用户请求经过Nginx时、判断是否为静态资源、是则直接从静态资源服务器里面获取、不用经过后端的Web层和Redis集群;
  • Web层缓存
    Web层里面会有一些Ehcache缓存、可以使用HashMap、ConcurrentHashMap、ArrayList等数据结构缓存数据,当请求经过Nginx发送到Web服务器时、判断Web服务器缓存中是否已经缓存请求数据,是就从服务器的缓存中获取并返回、否则去Redis集群中查询
  • Redis集群
    如果前两层缓存都没有查找到数据,则将Web层服务器发送请求到Redis集群里取查询数据,如果查询到,就将数据返回,如果查询不到,返回null.

如果多级缓存都没有查找到数据,那就从MySQL数据库中查询,查到之后将数据放入Redis集群中。

缓存设计

缓存穿透

  • 缓存穿透指查询一个根本不存在的数据,缓存层和存储层都不存在,通常处于容错的考虑,如果从存储层查询不到的数据则不写入缓存层;
  • 缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。
  • 原因有两个
  • 自身业务代码或者数据出现问题
  • 一些恶意攻击

例如:我们的用户ID一般都不可能是负数、如果一直查询ID=-1的数据,肯定查不到,如果同时发送大量请求(上万)会导致存储层压力瞬间变大。

解决方法
  1. 缓存空对象
   Map<String,Object> DB=new HashMap<String,Object>();

    @GetMapping("/getCache")
    public Object get(String key){
        String cache=stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.isEmpty(cache)){
            //从数据库读取数据操作--假设DB为数据库
            String dbVal= (String) DB.get(key);
            stringRedisTemplate.opsForValue().set(key,dbVal);
            if (StringUtils.isEmpty(dbVal)){
                Boolean expire = stringRedisTemplate.expire(key,300,TimeUnit.SECONDS);
            }
            return dbVal;
        }else {
            //...正常业务逻辑
            return cache;
        }
    }
  1. 布隆过滤器拦截
    在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在在进入缓存层、存储层。可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少

  2. 接口层进行进行数据校验
    把一些逻辑上不可能存在的数据进行拦截。如果ID最小为0,那么-1就会带来缓存穿透问题,可以验证ID<0时,拦截掉。

缓存雪崩

由于缓存层承载着大量请求,有效保护了存储层,但是如果缓存层由于某些原因不可用(宕机)或者大量热点缓存key由于超时时间相同,在同一时间段失效,大量请求直接到达存储数据库,数据库承受不住导致系统雪崩。

解决方案
  1. 缓存的过期时间用随机值,尽量让不同的key的过期时间不同。
  2. 依赖隔离组件为使用后端限流熔断并降级。
    比如使用Sentinel或Hystrix限流降级组件。
  3. 提前演练。
    在项目上线前, 演练缓存层宕掉后, 应用以及后端的负载情况以及可能出现的问题, 在4此基础上做一些预案设定。
  4. 设置key永不过期

缓存击穿

指的是缓存中没有,但数据库中有的数据,(一般缓存时间到期),当前key是一个热点key(例如一个秒杀活动),并发量非常大。突然,该热点key失效,导致大量的线程透过缓存层到达数据库层,数据库承受不住,导致崩溃。

解决方法
  1. 分布式锁
    只允许一个线程重建缓存,其他线程等待线程执行完,重新获取缓存数据。当key不存在,获取写锁、当key存在,获取写锁;
  2. 设置key永不过期
    不设置过期时间,不会出现key过期的问题。
  3. 接口限流,熔断与降级
    重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些 服务 不可用时候,进行熔断,失败快速返回机制。

热点key重建

  • 当前key是一个热点key(例如一个热门的娱乐新闻)失效,并发量非常大,有大量的线程来重建缓存,造成后端负载太大,甚至奔溃。
  • 因为重建缓存不能再短时间内完成,可能是一个复杂计算,会有复杂的SQL,多次IO等;
  • 这种情况下,要避免大量线程同时重建缓存
  • 通过互斥锁,只允许一个线程重建缓存,其他线程等待线程执行完,重新获取缓存数据。
    在这里插入图片描述
  @GetMapping("/getProduct")
    public Object getProduct(String key) throws InterruptedException {
        String cache=stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.isEmpty(cache)){
            String mutexKey="mutext:key:"+key;
            Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(mutexKey, "1", 180, TimeUnit.MILLISECONDS);
            if (aBoolean){
                cache= (String) DB.get(key);
                stringRedisTemplate.opsForValue().set(key,cache,300,TimeUnit.SECONDS);
                stringRedisTemplate.delete(mutexKey);
            }else {
                Thread.sleep(50);
                getProduct(key);
            }
        }
        return cache;
    }

缓存与数据库双写不一致

在大并发下,同时操作数据库与缓存会存在数据不一致性问题。
在这里插入图片描述
线程1先写数据库、再准备写入缓存期间,线程也写了数据库、又更新了缓存;
这就造成redis写覆盖;
在这里插入图片描述
线程1写完数据后,删除缓存数据、然后线程3查询这条缓存数据,由于被删除了,查找不到,因此会从数据库中获取,获取到后,在准备更新缓存期间,线程2写了同一条记录,又删除了缓存,最后线程3Web层查出来的数据其实和数据库的不一致,如果把Web层查出来的数据stock更新缓存,那么得到的是脏数据。

解决方案
  • 并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期时间依然可以解决大部分业务对于缓存的要求。
  • 如果不能容忍缓存数据不一致、可以通过读写锁保证并发读写或写写的时候是串行化的,读读操作相当于无锁;
  • 用阿里开源的canal通过监听数据库的binlog日志及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。在这里插入图片描述
    我们针对读多写少的情况加入缓存可以提高性能,如果写多读多又不能容忍数据不一致,那就没必要加缓存了,直接从Web层查询MySQL,不经过Redis,可以保证一致性;
    放入缓存的数据应该是对实时性、一致性要求不高的数据。

布隆过滤器

对于恶意攻击,向服务器请求大量不存在的数据造成的缓存穿透,还可以用布隆过滤器先做一次过滤,对于不存在的数据布隆过滤器一般都能够过滤掉,不让请求再往后端发送。当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。
在这里插入图片描述

  • 布隆过滤器就是一个大型位数组和几个不一样的无偏hash函数。
  • 向布隆过滤器添加key时、会使用多个hash函数进行Hash算出整数值,再对维数组进行取模得到一个位置,每个hash函数会得到一个不同的位置,再将这几个位置置为1,完成Add操作。
  • 向布隆过滤器询问key是否存在时,跟add一样,通过hash算出几个位置,判断这些位置是不是都为1,只要有一个是0,则说明这个key不存在。如果都是1,则该key极有可能存在,因为这些位置为1,可能是其他的key存在所致。
    如果这个数组比较稀疏,这个概率就很大,如果这个位数比较拥挤,这个概率就会降低。
  • 这种方法适用于数据命中不高、 数据相对固定、 实时性低(通常是数据集较大) 的应用场景, 代码维护较为复杂, 但是缓存空间占用很少。
<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.6.5</version>
</dependency>
package com.redisson;

import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedissonBloomFilter {

    public static void main(String[] args) {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        //构造Redisson
        RedissonClient redisson = Redisson.create(config);

        RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
        //初始化布隆过滤器:预计元素为100000000L,误差率为3%,根据这两个参数会计算出底层的bit数组大小
        bloomFilter.tryInit(100000000L,0.03);
        //将zhuge插入到布隆过滤器中
        bloomFilter.add("zhuge");

        //判断下面号码是否在布隆过滤器中
        System.out.println(bloomFilter.contains("guojia"));//false
        System.out.println(bloomFilter.contains("baiqi"));//false
        System.out.println(bloomFilter.contains("zhuge"));//true
    }
}

使用布隆过滤器需要把所有数据提前放入布隆过滤器,并且在增加数据时也要往布隆过滤器里放,布隆过滤器缓存过滤伪代码:

//初始化布隆过滤器
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
//初始化布隆过滤器:预计元素为100000000L,误差率为3%
bloomFilter.tryInit(100000000L,0.03);
        
//把所有数据存入布隆过滤器
void init(){
    for (String key: keys) {
        bloomFilter.put(key);
    }
}

String get(String key) {
    // 从布隆过滤器这一级缓存判断下key是否存在
    Boolean exist = bloomFilter.contains(key);
    if(!exist){
        return "";
    }
    // 从缓存中获取数据
    String cacheValue = cache.get(key);
    // 缓存为空
    if (StringUtils.isBlank(cacheValue)) {
        // 从存储中获取
        String storageValue = storage.get(key);
        cache.set(key, storageValue);
        // 如果存储数据为空, 需要设置一个过期时间(300秒)
        if (storageValue == null) {
            cache.expire(key, 60 * 5);
        }
        return storageValue;
    } else {
        // 缓存非空
        return cacheValue;
    }
}

布隆过滤器不能删除数据,如果要删除得重新初始化数据

  • 7
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
布隆过滤器是一种非常实用的解决缓存穿透、缓存击穿和缓存雪崩问题的工具。对于缓存穿透问题,布隆过滤器可以在缓存中存储空值,避免频繁查询数据库布隆过滤器的原理是通过多次哈希运算将元素映射到一个二进制数组中,如果某个位置的值为1,则表示该元素可能存在;如果为0,则表示该元素一定不存在。通过布隆过滤器,可以快速判断一个请求是否需要查询数据库,从而避免了缓存穿透的问题。\[3\] 对于缓存击穿问题,布隆过滤器可以用于限流和降级策略。通过对热点参数进行限流,可以控制请求的并发量,避免数据库被大量请求压垮。同时,对于无效的请求,可以进行服务降级,直接返回默认值或错误信息,而不是查询数据库。\[2\] 对于缓存雪崩问题,布隆过滤器可以作为一种多级缓存的解决方案之一。除了使用Redis作为缓存外,还可以使用Nginx缓存等其他缓存工具,将请求分散到不同的缓存层,从而减轻数据库的访问压力。同时,可以通过设置缓存的过期时间,避免大量缓存同时过期,导致数据库访问压力过大。\[2\] 总之,布隆过滤器是一种非常实用的工具,可以有效解决Redis缓存雪崩、缓存穿透和缓存击穿问题。通过合理使用布隆过滤器,可以提高系统的性能和稳定性。 #### 引用[.reference_title] - *1* [redis缓存穿透之终极解决方案——布隆过滤器](https://blog.csdn.net/qq_40606397/article/details/114085367)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [redis缓存雪崩、击穿、穿透](https://blog.csdn.net/weixin_45414913/article/details/124901909)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值