Redis-高并发缓存问题及解决方案

Redis缓存问题解决方案

1.缓存穿透

1)什么是缓存穿透

缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层。缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。

2)造成缓存穿透的原因

1.自身业务出现问题或者数据有问题。

2.黑客攻击,制造大量不存在的key 利用压测工具等进行攻击

3)解决方案

1.缓存空对象

String get(String key){
    //先从缓存中拿数据
    String cacheValue = cache.get(key);
    // 缓存为空
    if (StringUtils.isBlank(cacheValue)) {
        //从db中拿
        String storageValue = storage.get(key);
        cache.set(key, storageValue);
        //设置一个过期时间,否则缓存中有大量的空对象
        if (storageValue == null) { 
            cache.expire(key, 60 * 5); 12
        }
        return storageValue;
        
    }else{
        //缓存中可以获取之间返回
        return cacheValue;
    }
}

2.布隆过滤器

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

image-20220502151214930

布隆过滤器就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得比较均匀。

向布隆过滤器中添加 key 时,会使用多个 hash 函数对 key 进行 hash 算得一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为1就完成了 add 操作。

向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都为 1,只要有一个位为 0,那么说明布隆过滤器中这个key 不存在。如果都是 1,这并不能说明这个 key 就一定存在,只是极有可能存在,因为这些位被置为1可能是因为其它的 key 存在所致。如果这个位数组比较稀疏,这个概率就会很大,如果这个位数组比较拥挤,这个概率就会降低。 这种方法适用于数据命中不高、 数据相对固定、 实时性低(通常是数据集较大) 的应用场景, 代码维护较为 复杂, 但是缓存空间占用很少

可以用redisson实现布隆过滤器,引入依赖:

<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("123"));//false
        System.out.println(bloomFilter.contains("baiqi"));//false
        System.out.println(bloomFilter.contains("qianyue"));//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)){
        // 从db中获取
        String storageValue = storage.get(key);
        cache.set(key, storageValue);
        // 如果存储数据为空, 需要设置一个过期时间(300秒)
        if (storageValue == null){
            cache.expire(key, 60 * 5);
        }
        return storageValue;
    }else{
        return cacheValue;
    }
    
}

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

2.缓存击穿

1)什么是缓存击穿

由于大批量缓存在同一时间失效可能导致大量请求同时穿透缓存直达数据库,可能会造成数据库瞬间压力过大甚至挂掉。

2)解决方案

在批量增加缓存时最好将这一批数据的缓存过期时间设置为一个时间段内的不同时间。

2.缓存雪崩

1)什么是缓存雪崩

缓存雪崩指的是缓存层支撑不住或宕掉后,流量会像奔逃的野牛一样,打向后端存储层。 由于缓存层承载着大量请求, 有效地保护了存储层,但是如果缓存层由于某些原因不能提供服务(比如超大并发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下降),于是大量请求都会打到存储层,存储层的调用量会暴增.造成存储层也会级联宕机的情况。

2)解决方案

1.保证缓存层服务高可用性,比如使用哨兵模式和集群

2.依赖隔离组件为后端限流熔断并降级。比如使用Sentinel或Hystrix限流降级组件。比如服务降级,我们可以针对不同的数据采取不同的处理方式。当业务应用访问的是非核心数据(例如电商商 品属性,用户信息等)时,暂时停止从缓存中查询这些数据,而是直接返回预定义的默认降级信息、空值或是 错误提示信息;当业务应用访问的是核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取.

4.热点缓存key重建优化

开发人员使用“缓存+过期时间”的策略既可以加速数据读写, 又保证数据的定期更新, 这种模式基本能够满足绝大部分需求。 但是有两个问题如果同时出现,可能就会对应用造成致命的危害:

  • 当前key是一个热点key并发量非常大。 (以板蓝根为例,疫情之前不是热点数据,就没有放在缓存中,但是听说可以抗新冠,火了起来,此时这些请求打在数据库上可能把数据库直接搞宕机)
  • 重建缓存不能在短时间完成, 可能是一个复杂计算, 例如复杂的SQL、 多次IO、 多个依赖等。

在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃.

解决方案:

解决这个问题主要就是要避免大量线程同时重建缓存。我们可以利用互斥锁来解决,此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从

缓存获取数据即可。

代码示例:

String get(String key) {
    // 从Redis中获取数据
    String value = redis.get(key);
    // 如果value为空, 则开始重构缓存
    if (value == null){
        // 只允许一个线程重建缓存, 使用nx, 并设置过期时间ex
        String mutexKey = "mutext:key:" + key;
        if (redis.set(mutexKey, "1", "ex 180", "nx")){
            // 从数据源获取数据
            value = db.get(key);
            // 回写Redis, 并设置过期时间
            redis.setex(key, timeout, value);
            // 删除key_mutex
            redis.delete(mutexKey);
        }else{
            Thread.sleep(50);
            get(key);
        }
    }
    return value;
}

5. 缓存和数据库双写不一致

在大并发下,同时操作数据库与缓存会存在数据不一致性问题

1、双写不一致问题

线程1写完数据库,还未更新缓存,线程2又写数据库,更新缓存,最好线程1又更新了缓存,此时造成数据库和缓存中的数据我不一致的。

image-20220502155151761

2.读写并发不一致

1.线程1 写进数据库然后删除缓存

2.线程3查询缓存是空的,从数据库中拿到数据但是还未放入缓存

3.线程2又进来了写数据库并删除了缓存

4.线程3继续更新缓存 此时缓存和数据库中的数据还是不一致的。

image-20220502155327954

解决方案:

1、对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。

2、就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期时间依然可以解决大部分业务对于缓存的要求

3、如果不能容忍缓存数据不一致,可以通过加读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相 当于无锁

4、也可以用阿里开源的canal通过监听数据库的binlog日志及时的去修改缓存,但是引入了新的中间件,增加

了系统的复杂度。

6.Redis对过期key的三种清除策略

1)被动删除

当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key

2)主动删除

由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰一批已过期的key

3)内存淘汰

主动清理策略在Redis 4.0 之前一共实现了 6 种内存淘汰策略,在 4.0 之后,又增加了 2 种策 略,总共8种:1

a)针对设置了过期时间的key做处理:

  1. volatile-ttl:在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删 除,越早过期的越先被删除。

  2. volatile-random:就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。

  3. volatile-lru:会使用 LRU 算法筛选设置了过期时间的键值对删除。

  4. volatile-lfu:会使用 LFU 算法筛选设置了过期时间的键值对删除。

b) 针对所有的key做处理:

  1. allkeys-random:从所有键值对中随机选择并删除数据。
  2. allkeys-lru:使用 LRU 算法在所有数据中进行筛选删除。
  3. allkeys-lfu:使用 LFU 算法在所有数据中进行筛选删除。

c) 不处理:

  1. noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息"(error) OOM command not allowed when used memory",此时Redis只响应读操作。

LRU 算法(Least Recently Used,最近最少使用)

淘汰很久没被访问过的数据,以最近一次访问时间作为参考。

LFU 算法(Least Frequently Used,最不经常使用)

淘汰最近一段时间被访问次数最少的数据,以次数作为参考。

如何选择主动清理策略?

当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作(一个不常用的key被突然访问了一下,此时访问时间比一些热点key要晚,导致热点key被清理)会导致LRU命中率急剧下降,缓存污染情况比较严重。这时使用LFU可能更好点。

根据自身业务类型,配置好maxmemory-policy(默认是noeviction),推荐使用volatile-lru。如 果不设置最大内存,当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap),会让 Redis 的性能急剧下降。当Redis运行在主从模式时,只有主结点才会执行过期删除策略,然后把删除操作”del ,key”同步到从结点删除数据

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
高并发场景下,Redis可以应对多个问题,如缓存击穿、缓存雪崩、缓存穿透等。缓存击穿是指在某个热点数据失效后,大量的请求同时访问数据库,导致数据库压力剧增,此时可以采用互斥锁或者设置短暂的空值来解决。缓存雪崩是指缓存中的大量数据同时失效,导致大量请求直接访问数据库,造成数据库压力过大,可以采用设置随机过期时间、添加热点数据和使用集群等方式来避免。而缓存穿透是指恶意请求访问一个不存在于缓存和数据库中的数据,此时可以采用布隆过滤器等方法来判断请求是否合法,并缓存不存在的值。此外,在高并发场景下,多个用户同时进行写操作,往往会导致数据更新的不一致性,可以采用事务或者乐观锁等方式来解决。因此,Redis高并发场景下可以通过以上方法来应对各种问题,确保数据的安全性和一致性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Redis高并发场景解决方案](https://blog.csdn.net/weixin_61901664/article/details/126057493)[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^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [如何应对redis缓存雪崩,redis缓存穿透,redis缓存击穿,redis应对高并发解决方案](https://download.csdn.net/download/lj_70596/13656954)[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^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值