一、高可用问题

1,缓存穿透

1.1 概述

某key在redis中找不到,频繁访问这个key,缓存中没有就直接访问数据库了,频繁访问就导致数据库宕机了

1.2 解决方案

1.21 布隆过滤器
概述

请求时验证此key是否存在
创建一个二进制数组,都置为0(二进制数组只有0和1)
提供 key.hash() 运算找到对应二进制数组的下标,置为1
新来的key经过hash运算看对应下标的数组元素值是否为1就可以判断存在与否

力扣题目

题目描述:
请按照下列指示实现一个redis布隆过滤器:
① 首先构造n个哈希方法,每个哈希方法字符串到key值映射公式为:key = ( i * key + s) % mod,其中key从0开始,s为字符串中每个字符的ASCII值,i为这是第几个哈希方法(从1开始),mod为每个方法对应的随机数,取值在10000~20000之间。
② 实现add函数,向布隆过滤器中添加一个字符串:使用每个构造的哈希方法得到n个key值,相应key值标记。
③ 实现contains函数,检查给定字符串是否在布隆过滤器中:检查是否每个哈希方法的key值都有标记。
输入字符串数组s1表示先将这些字符串加入到布隆过滤器中,再检验字符串数组s2中的字符串是否在布隆过滤器中。

import java.util.*;

//hash运算可能存在位运算,异或等,参考HashMap的hash运算

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param s1 string字符串ArrayList 
     * @param s2 string字符串ArrayList 
     * @param n int整型 
     * @return int整型ArrayList
     */
    public ArrayList<Integer> BloomFilter (ArrayList<String> s1, ArrayList<String> s2, int n) {
        ArrayList<Integer> list=new ArrayList<>();
        //s1为需要添加的字符串集合
        //s2为需要判断是否存在的字符串集合
        //n为需要计算多少hash值,也就是看误判率判断

        //申请一个足够长的二进制数组,默认值都为0
        int[] boolFilter=new int[10000];

        for (String str:s1){
            add(str,n,boolFilter);
        }
        // s2数组中哪些已存在
        for (String str:s2){
            list.add(contains(str,boolFilter,n));
        }
        return list;
    }

    //mod应为随机数,一个过滤器一个,应在方法内,此处为了省事
    int mod=10001;
    //hash值计算应有<<或&等
    public int hash(int key,int i,String s1){
        for (int j = 0; j < s1.length(); j++) {
            // 计算hash值 key = ( i * key + s) % mod
            key=(i*key + s1.charAt(j)) % mod;
        }
        return key;
    }

    public void add(String s1,int n,int[] boolFilter){
        for (int i = 1; i <= n; i++) {
            int key2=0;
            //计算出的key1下标对应的值变为1
            boolFilter[hash(key2,i,s1)]=1;
        }
    }

    //判断计算出的下标key的值是否为1
    public int contains(String s2,int[] boolFilter,int n){
        for (int i = 1; i <= n; i++) {
            int key2=0;
            if (boolFilter[hash(key2,i,s2)]!=1){
                return 0;
            }
        }
        return 1;
    }
}
加了布隆过滤器还存在的问题

问题来源:不同key经过hash运算的结果可能相同
解决办法:误判率设置的低一点(误判率:每增加一个hash运算可减少一点误判率),不能完全避免,不能设置过低,过低的话,hash运算的次数就多,时间,空间的开销就大。

1.22 日志检测 --这个瞎猜的,不用纠结

业务层每个类中加个定时执行的接口
内容:
检测其他接口的访问时间(这段时间访问的尽可能多几次,算平均)
和预估时间比较,或者取之前接口平均访问时间比较(定时触发的第十次和第九次接口访问时间比较)

1.23 总结

在于是否用到缓存,不存在可以设置下

2,缓存击穿

2.1 概述

存在一个热点数据,用户频繁访问,该热点数据的key有过期时间,该过期到下一次设置服务器扛不住压力导致的宕机(例如:热搜)

2.2 解决方案

2.21 wait…

1)热点key时间到了失效
给key延长时间,或者设置永不过期
2)热点key时间没到,但是请求量太大了,redis响应不了那么多请求,导致一部分请求连接数据库查询结果响应 --瞎猜的,不用纠结
CountDownLatch压测,看看最大能接受多少并发请求
Semphore进行限流

2.22 分布式锁

set key nx px
看门狗延长key有效时间

3,缓存雪崩

3.1 概述

大量热点key失效

3.2 解决方案

预热key

key设置的过期时间适当增长,部分key设置永不过期,一段时间后手动清理

限流,分布式锁,Semphore

二、过期key删除

定时删除:
在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。对内存最友好,对 CPU 时间最不友好。
惰性删除:
放任键过期不管,但是每次获取键时,都检査键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。对 CPU 时间最优化,对内存最不友好。
定期删除:
每隔一段时间,默认 100ms,程序就对数据库进行一次检査,删除里面的过期键。至 于要删除多少过期键,以及要检査多少个数据库,则由算法决定。前两种策略的折中,对 CPU 时间和内存的友好程度较平衡。

三、内存淘汰策略

当 redis 的内存空间(maxmemory 参数配置)已经用满时,redis 将根据配置的驱逐策略(maxmemory-policy 参数配置),进行相应的淘汰动作。

noeviction:默认策略,不淘汰任何 key,直接返回错误。
allkeys-lru:在所有的 key 中,使用 LRU 算法淘汰部分 key。
allkeys-lfu:在所有的 key 中,使用 LFU 算法淘汰部分 key,该算法于 Redis 4.0 新增。
allkeys-random:在所有的 key 中,随机淘汰部分 key。
volatile-lru:在设置了过期时间的 key 中,使用 LRU 算法淘汰部分 key。
volatile-lfu:在设置了过期时间的 key 中,使用 LFU 算法淘汰部分 key,该算法于 Redis 4.0 新增。
volatile-random:在设置了过期时间的 key 中,随机淘汰部分 key。
volatile-ttl:在设置了过期时间的 key 中,挑选 TTL(time to live,剩余时间)短的 key 淘汰。

1,LRU --最长时间未使用的

用LinkedHashMap

2,LFU --最不经常使用

题目:
请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。
实现 LFUCache 类:
LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象
int get(int key) - 如果键 key 存在于缓存中,则获取键的值,否则返回 -1 。
void put(int key, int value) - 如果键 key 已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量 capacity 时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最近最久未使用 的键。
为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。
当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 get 或 put 操作,使用计数器的值将会递增。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

/**
 * 触发LRU,一次清理多少?
 * 固定数量key,还是频率低于多少的
 * 方案1:触发一次LFU清理一个key?感觉不太现实,应该其他清理策略
 * HashMap存储key,value,useCount
 * key达到上限,触发LRU
 * 第一遍,遍历HashMap的节点,取出useCount最小值
 * 第二遍,遍历HashMap的节点,删除useCount=useCount最小值的key
 * HashMap的节点 --由数组,链表,红黑树组成
 * 数组+链表:数组就是一个一个地址值连续的节点连接起来,链表就是地址值不连续的节点
 * 红黑树:涉及前中后序遍历,此处不考虑
 */
class LFUCache {
	//采用的策略:Hash(多存了时间戳+key使用次数)  --题目看漏了一点,后来懒得换了
    ConcurrentHashMap<Integer,Node> map=new ConcurrentHashMap<>();

    int capacity;

    public LFUCache(int capacity) {
        this.capacity=capacity;
    }

    public int get(int key) {
        try {
            Thread.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (map.containsKey(key)){
            Node val=map.get(key);
            int count = val.useCount + 1;
            val.setUseCount(count);
            val.setTime(System.currentTimeMillis());
            return val.value;
        }
        return -1;
    }

    public void put(int key, int value) {
        try {
            Thread.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //创建个Node对象
        Node val=new Node();
        val.setValue(value);
        //put的时候要看容量
        if (map.size()<capacity){
            //如果put的key存在,更新value值,useCount++
            putV(key,val);
        }else {
            deleteKey();
            if(capacity==0){
            }else{
                putV(key,val);
            }
            
            //触发清理策略 --清理key
            
        }
    }

    private void putV(int key,Node val) {
        if (map.containsKey(key)){
            int count = val.useCount + 1;
            val.setUseCount(count);
            val.setTime(System.currentTimeMillis());
            map.put(key,val);
        }else {
            int count=1;
            val.setUseCount(count);
            val.setTime(System.currentTimeMillis());
            map.put(key,val);
        }
    }

    private void deleteKey() {
        //迭代器找出最少使用次数
        int minCount = Integer.MAX_VALUE;
        Set<Integer> keys = map.keySet();
        Iterator<Integer> iterator = keys.iterator();
        while (iterator.hasNext()){
            int key=iterator.next();
            Node val = map.get(key);
            minCount=Math.min(minCount,val.useCount);
        }
        //迭代器删除该useCount
        Map<Long,Integer> deleteKey=new ConcurrentHashMap<>();
        List<Long> longs=new ArrayList<>();
        Iterator<Integer> it = keys.iterator();
        while (it.hasNext()){
            int key=it.next();
            Node val = map.get(key);
            if (val.useCount==minCount){
                //添加key与时间戳
                deleteKey.put(val.time,key);
                longs.add(val.time);
            }
        }
        if (longs.size()>1){
            long minTime=longs.get(0);
            for (int i = 0; i < longs.size(); i++) {
                minTime=Math.min(minTime,longs.get(i));
            }
            map.remove(deleteKey.get(minTime));
        }else if(longs.size()==1) {
            map.remove(deleteKey.get(longs.get(0)));
        }
    }

    //如果Node当key,这样会导致数组过长,建议useCount和value一起创建实体类,此处图个迭代方便
    static class Node{
        private int value;
        private int useCount;
        private long time;

        public long getTime() {
            return time;
        }

        public void setTime(long time) {
            this.time = time;
        }

        public int getValue() {
            return value;
        }

        public void setValue(int value) {
            this.value = value;
        }

        public int getUseCount() {
            return useCount;
        }

        public void setUseCount(int useCount) {
            this.useCount = useCount;
        }
    }

}