herramienta de código abierto fit-cache

I. Introducción

        dirección de github: https://github.com/SongTing0711/fit-cache

        En la actualidad, el uso de caché local, Redis, Es, Ck y otros cachés en la industria está en un estado relativamente casual. Por un lado, es un desperdicio de recursos. Por otro lado, si es un caché local , puede ser la gota que colme el vaso que lleve al oom.

        El ajuste de caché creado por el autor proporciona si la clave es adecuada para el almacenamiento en caché, el tiempo adecuado para el almacenamiento en caché y resuelve problemas como el abuso de caché y la falla simultánea de caché.

2. Arquitectura

1. Posicionamiento de herramientas

        fit-cahe necesita implementar dos funciones: si la clave es adecuada para el almacenamiento en caché y, si es adecuada para el almacenamiento en caché, cuánto tiempo debe almacenarse en caché.

        El autor se refiere al motor de almacenamiento Innodb de Mysql (innodb usa un porcentaje de LRU para el grupo de búfer de mysql para evitar lecturas y escrituras de disco frecuentes) y HotKey de JD.com (JD.com lo usa para almacenar en caché datos de puntos de acceso en el centro comercial de manera oportuna para evitar que redis explote con grandes promociones).       

        El autor cree que la ventana deslizante + la lista LRU se puede utilizar para el cálculo, es decir, la frecuencia y el tiempo de acceso más reciente.

5fd46ff9604e42429bc68dbe57863d44.png

2. Date cuenta 

        Si se realiza, el autor cree que el peso de la frecuencia es mayor que el acceso más reciente, por lo que si es adecuado para el almacenamiento en caché, primero debe juzgar si la ventana deslizante correspondiente a la clave cumple con la regla de calor establecida, si no, entonces mire la lista de LRU, pero la longitud de la LRU debe ser limitada, de lo contrario, no es muy probable que se vuelva a revisar en el corto plazo.

3ed866f94e37406d97ddddd7dd836184.png

        El tiempo adecuado para el almacenamiento en caché debe ponderarse por frecuencia y tiempo de acceso reciente, y se realiza un cálculo ponderado. El tiempo de acceso reciente también debe decaer exponencialmente, porque cuanto más largo es el tiempo de acceso, obviamente menor es la prioridad. 

c0902d460dbf47ad87bd36c8a7dfef93.png

3. Embalaje 

        No hay más de dos formas de empaquetar herramientas de código abierto. Una es empaquetarlas en un Cliente independiente y ponerlas en maven como referencia del servicio. Esto es para la latitud de un solo servicio. La ventaja es que es fácil de introducir. al usarlo, y no necesita ser como muchas herramientas de middleware.Dividir el cliente y el servidor genera enormes costos de introducción.

        El otro es encapsularlo en un modo cliente-servidor, de modo que se pueda calcular la latitud del clúster, para juzgar si es adecuado para el almacenamiento en caché en su conjunto y el tiempo de almacenamiento en caché correspondiente.

        El autor actualmente encapsula el primer tipo y el segundo tipo todavía está en el empaque.

3. Código fuente

        Aquí está el código central de fit-cache

1, núcleo        

        La API central expuesta por FitCacheStore: si es adecuada para el almacenamiento en caché y adecuada para el tiempo de almacenamiento en caché

        El valor de la clave adecuada para el almacenamiento en caché se puede almacenar en conjunto, y la capa inferior del autor es cafeína

         DispatcherConfig es una cola de bloqueo, donde los eventos de acceso clave se almacenan y luego los subprocesos asincrónicos los eliminan para calcular la frecuencia y el acceso reciente.

public class FitCacheStore {

    /**
     * 判断是否适合缓存
     */
    public static boolean isFitCache(String key) {
        try {
            // 先看滑动窗口的热度,判断适不适合缓存
            boolean fit = CaffeineCacheHolder.getFitCache().getIfPresent(key) != null;
            if (!fit) {
                fit = CaffeineCacheHolder.getLruCache().get(key) != null;
            }
            DispatcherConfig.QUEUE.put(key);
            return fit;
        } catch (Exception e) {
            return false;
        }
    }


    public static int fitCacheTime(String key) {
        try {
            SlidingWindow window = (SlidingWindow) CaffeineCacheHolder.getWindowCache().getIfPresent(key);
            long lastTime = (long) CaffeineCacheHolder.getLruCache().get(key);
            if (window == null && lastTime == 0) {
                return 0;
            }
            if (window == null && lastTime != 0) {
                return FitCacheTime.calculateStorageTime(0, lastTime);
            }
            if (window != null && lastTime == 0) {
                return FitCacheTime.calculateStorageTime(window.getCount(), 0);
            }
            int res = FitCacheTime.calculateStorageTime(window.getCount(), lastTime);
            DispatcherConfig.QUEUE.put(key);
            return res;
        } catch (Exception e) {
            return 0;
        }
    }

    /**
     * 从本地caffeine取值
     */
    public static Object get(String key) {
        return CaffeineCacheHolder.getFitCache().getIfPresent(key);
    }

    /**
     * 设置缓存
     */
    public static boolean set(String key, Object value) {
        Object object = CaffeineCacheHolder.getFitCache().getIfPresent(key);
        Object lru = CaffeineCacheHolder.getLruCache().get(key);
        if (object == null && lru == null) {
            return false;
        }
        CaffeineCacheHolder.getFitCache().put(key, value);
        return true;
    }
//
//    private static ExecutorService threadPoolExecutor = new ThreadPoolExecutor(1,
//            2,
//            5,
//            TimeUnit.SECONDS,
//            new ArrayBlockingQueue<>(100),
//            new ThreadPoolExecutor.DiscardOldestPolicy());
//    public static void main (String[] args) throws InterruptedException {
//        KeyRule rule = new KeyRule("test", true, 2,5);
//        KeyRuleHolder.KEY_RULES.add(rule);
//        IKeyListener iKeyListener = new KeyListener();
//        KeyConsumer keyConsumer = new KeyConsumer();
//        keyConsumer.setKeyListener(iKeyListener);
//
//        threadPoolExecutor.submit(keyConsumer::beginConsume);
//        boolean fit = isFitCache("test");
//        System.out.println("第一次访问test是否适合" + fit);
//        Thread.sleep(1000);
//        fit = isFitCache("test");
//        System.out.println("第2次访问test是否适合" + fit);
//        Thread.sleep(1000);
//        fit = isFitCache("test666");
//        System.out.println("第一次访问test666是否适合" + fit);
//        Thread.sleep(1000);
//        fit = isFitCache("test");
//        System.out.println("第3次访问test是否适合" + fit);
//        Thread.sleep(1000);
//        int time = fitCacheTime("test");
//        System.out.println("第1次访问test适合时间" + time);
//    }

}

2, configuración

        Esto es principalmente para hacer una función de distribución, colocar el subproceso que consume el evento en el grupo de subprocesos para comenzar

@Configuration
public class DispatcherConfig {

    private ExecutorService threadPoolExecutor = new ThreadPoolExecutor(1,
            2,
            5,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100),
            new ThreadPoolExecutor.DiscardOldestPolicy());

    /**
     * 队列
     */
    public static BlockingQueue<String> QUEUE = new LinkedBlockingQueue<>(200);

    @Bean
    public Consumer consumer() {
        List<KeyConsumer> consumerList = new ArrayList<>();
        KeyConsumer keyConsumer = new KeyConsumer();
        consumerList.add(keyConsumer);

        threadPoolExecutor.submit(keyConsumer::beginConsume);
        return new Consumer(consumerList);
    }
}

public class KeyConsumer {

    @Resource
    private IKeyListener iKeyListener;

    public void setKeyListener(IKeyListener iKeyListener) {
        this.iKeyListener = iKeyListener;
    }

    public void beginConsume() {
        while (true) {
            try {
                String key = DispatcherConfig.QUEUE.take();
                iKeyListener.newKey(key);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3, escucha

        Consumir eventos, recorrer la ventana deslizante y la lista LRU

@Component
public class KeyListener implements IKeyListener {

    @Override
    public void newKey(String key) {
        SlidingWindow slidingWindow = checkWindow(key);
        // 被访问,进入最近访问列表
        CaffeineCacheHolder.getLruCache().put(key, System.currentTimeMillis());
        //看看达到匹配规则没有
        boolean fit = slidingWindow.addCount(1);

        CaffeineCacheHolder.getWindowCache().put(key, slidingWindow);
        if (fit && CaffeineCacheHolder.getFitCache().getIfPresent(key) == null) {
            //数据变热,适合缓存
            CaffeineCacheHolder.getFitCache().put(key, System.currentTimeMillis());
        }
    }

    /**
     * 生成或返回该key的滑窗
     */
    private SlidingWindow checkWindow(String key) {
        // 取该key的滑窗
        return (SlidingWindow) CaffeineCacheHolder.getWindowCache().get(key, (Function<String, SlidingWindow>) s -> {
            // 是个新key,获取它的规则
            KeyRule keyRule = KeyRuleHolder.findRule(key);
            return new SlidingWindow(keyRule.getInterval(), keyRule.getThreshold());
        });
    }

}

4. Herramientas

        La primera es la lista LRU. Aquí, se utiliza una lista enlazada bidireccional para almacenar el tiempo de acceso de la clave. La capacidad de la lista se puede configurar. El lru de innodb de mysql es especial. Establece una proporción. No debe colocarse en la cabeza cuando se accede por primera vez, pero poner alrededor del 30% si, porque su acceso a los datos es muy grande, no la dimensión clave. Si el escaneo completo de la tabla es fácil de eliminar todo el contenido original de la lista lru.

        El autor no necesita ser como él aquí, porque es clave-dimensional.

public class LruCache {
    class Node {
        String key;
        Object value;
        Node prev;
        Node next;

        public Node(String key, Object value) {
            this.key = key;
            this.value = value;
        }
    }
    private final int capacity;
    private final Map<String, Node> cache;
    private Node head;
    private Node tail;

    public LruCache(int capacity) {
        this.capacity = capacity;
        this.cache = new HashMap<>();
        this.head = new Node("head", 0);
        this.tail = new Node("tail", 0);
        head.next = tail;
        tail.prev = head;
    }

    public Object get(String key) {
        Node node = cache.get(key);
        if (node != null) {
            // l列表里面有就转到头部
            moveToHead(node);
            return node.value;
        }
        return null;
    }

    public synchronized void put(String key, Object value) {
        Node node = cache.get(key);
        if (node != null) {
            node.value = value;
            moveToHead(node);
        } else {
            node = new Node(key, value);
            cache.put(key, node);
            addToHead(node);
            if (cache.size() > capacity) {
                // 超过容量就删除尾部节点
                Node removedNode = removeTail();
                cache.remove(removedNode.key);
            }
        }
    }

    private synchronized void moveToHead(Node node) {
        removeNode(node);
        addToHead(node);
    }

    private synchronized void addToHead(Node node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private synchronized void removeNode(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private synchronized Node removeTail() {
        Node removedNode = tail.prev;
        removeNode(removedNode);
        return removedNode;
    }

}

        Luego está la ventana deslizante, que gestiona la frecuencia.

public class SlidingWindow {
    /**
     * 循环队列,就是装多个窗口用,该数量是windowSize的2倍
     */
    private AtomicLong[] timeSlices;
    /**
     * 队列的总长度
     */
    private int timeSliceSize;
    /**
     * 每个时间片的时长,以毫秒为单位
     */
    private int timeMillisPerSlice;
    /**
     * 共有多少个时间片(即窗口长度)
     */
    private int windowSize;
    /**
     * 在一个完整窗口期内允许通过的最大阈值
     */
    private int threshold;
    /**
     * 该滑窗的起始创建时间,也就是第一个数据
     */
    private long beginTimestamp;
    /**
     * 最后一个数据的时间戳
     */
    private long lastAddTimestamp;


    public SlidingWindow(int duration, int threshold) {
        //超过10分钟的按10分钟
        if (duration > 600) {
            duration = 600;
        }
        //要求5秒内探测出来的,
        if (duration <= 5) {
            this.windowSize = 5;
            this.timeMillisPerSlice = duration * 200;
        } else {
            this.windowSize = 10;
            this.timeMillisPerSlice = duration * 100;
        }
        this.threshold = threshold;
        // 保证存储在至少两个window
        this.timeSliceSize = windowSize * 2;

        reset();
    }

    public SlidingWindow(int timeMillisPerSlice, int windowSize, int threshold) {
        this.timeMillisPerSlice = timeMillisPerSlice;
        this.windowSize = windowSize;
        this.threshold = threshold;
        // 保证存储在至少两个window
        this.timeSliceSize = windowSize * 2;

        reset();
    }

    /**
     * 初始化
     */
    private void reset() {
        beginTimestamp = System.currentTimeMillis();
        //窗口个数
        AtomicLong[] localTimeSlices = new AtomicLong[timeSliceSize];
        for (int i = 0; i < timeSliceSize; i++) {
            localTimeSlices[i] = new AtomicLong(0);
        }
        timeSlices = localTimeSlices;
    }

    /**
     * 计算当前所在的时间片的位置
     */
    private int locationIndex() {
        long now = System.currentTimeMillis();
        //如果当前的key已经超出一整个时间片了,那么就直接初始化就行了,不用去计算了
        if (now - lastAddTimestamp > timeMillisPerSlice * windowSize) {
            reset();
        }

        int index = (int) (((now - beginTimestamp) / timeMillisPerSlice) % timeSliceSize);
        if (index < 0) {
            return 0;
        }
        return index;
    }

    /**
     * 增加count个数量
     */
    public synchronized boolean addCount(int count) {
        //当前自己所在的位置,是哪个小时间窗
        int index = locationIndex();
        clearFromIndex(index);

        int sum = 0;
        // 在当前时间片里继续+1
        sum += timeSlices[index].addAndGet(count);
        for (int i = 1; i < windowSize; i++) {
            sum += timeSlices[(index - i + timeSliceSize) % timeSliceSize].get();
        }

        lastAddTimestamp = System.currentTimeMillis();
        return sum >= threshold;
    }


    public int getCount() {
        int sum = 0;
        //加上前面几个时间片
        for (int i = 1; i < windowSize; i++) {
            sum += timeSlices[i].get();
        }
        return sum;
    }

    private void clearFromIndex(int index) {
        for (int i = 1; i <= windowSize; i++) {
            int j = index + i;
            if (j >= windowSize * 2) {
                j -= windowSize * 2;
            }
            timeSlices[j].set(0);
        }
    }

}

        Luego está el cálculo adecuado para el tiempo de almacenamiento en caché, que se calcula según la frecuencia y el tiempo de acceso reciente

public class FitCacheTime {

    /**
     * 加权递减求和算法,计算数据的评分
     *
     * @param frequency
     * @param lastTime
     * @return
     */
    private static double calculateScore(double frequency, long lastTime) {
        // 根据业务需求和数据的重要性,给访问频率和最近访问时间分配不同的权重
        // 这里可以从配置中心拿
        double frequencyWeight = 0.7;
        double timeWeight = 0.3;
        // 计算访问频率和最近访问时间的值
        double time = (System.currentTimeMillis() - lastTime) / 1000.0;
        // 使用递减函数计算时间权重,越近访问的时间权重越高
        double timeDecay = Math.exp(-time);
        // 加权求和,得到评分
        double score = frequencyWeight * frequency + timeWeight * timeDecay;
        return score;
    }

    /**
     * 计算数据适合被存储的时间
     *
     * @param frequency
     * @param lastTime
     * @return
     */
    public static int calculateStorageTime(double frequency, long lastTime) {
        // 根据评分确定数据适合被存储的时间
        double score = calculateScore(frequency, lastTime);
        int storageTime = (int) Math.ceil(score);
        return storageTime;
    }

}

5. caché

public class CaffeineCacheHolder {
    /**
     * key是appName,value是caffeine
     */
    private static final ConcurrentHashMap<String, Object> CACHE_MAP = new ConcurrentHashMap<>();

    private static final String FIT = "fit";
    private static final String WINDOW = "window";

    private static final String LRU = "lru";
    public static Cache<String, Object> getFitCache() {
        if (CACHE_MAP.get(FIT) == null) {
            // todo 这里要从配置中心拿
            CACHE_MAP.put(FIT, CaffeineBuilder.cache(60, 100, 60));
        }
        return (Cache<String, Object>) CACHE_MAP.get(FIT);
    }

    public static Cache<String, Object> getWindowCache() {
        if (CACHE_MAP.get(WINDOW) == null) {
            // todo 这里要从配置中心拿
            CACHE_MAP.put(WINDOW, CaffeineBuilder.cache(60, 100, 60));
        }
        return (Cache<String, Object>) CACHE_MAP.get(WINDOW);
    }

    public static LruCache getLruCache() {
        if (CACHE_MAP.get(LRU) == null) {
            // todo 这里要从配置中心拿
            CACHE_MAP.put(LRU, new LruCache(1));
        }
        return (LruCache) CACHE_MAP.get(LRU);
    }

}
public class CaffeineBuilder {

    /**
     * 构建所有来的要缓存的key getCache
     */
    public static Cache<String, Object> cache(int minSize, int maxSize, int expireSeconds) {
        return Caffeine.newBuilder()
                .initialCapacity(minSize)
                .maximumSize(maxSize)
                .expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
                .build();
    }

}

Cuatro Resumen

        fit-cache actualmente se da cuenta de si el cliente es adecuado para el almacenamiento en caché, adecuado para el tiempo de almacenamiento en caché, y hay muchas ideas para ampliar las funciones, como la invalidación de caché, y cuando se cambian los datos, los datos antiguos se muestran al usuario, como el secuencia de prioridad de la memoria caché, prioridad baja La memoria caché de nivel invade el espacio de la memoria caché de alta prioridad y así sucesivamente. El autor tiene algunos planes que necesitan ser practicados.

        En la actualidad, el autor todavía está creando y perfeccionando, y los estudiantes interesados ​​pueden unirse.

 

Supongo que te gusta

Origin blog.csdn.net/m0_69270256/article/details/131844517
Recomendado
Clasificación