jodd.cache.LRUCache: 小巧的本地缓存, 及其并发bug

本文探讨了LRU(最近最少使用)和LFU(最不经常使用)缓存策略的原理和实际应用。在性能测试中,LRU和LFU的命中率接近,但LRU效率稍高。由于Redis缓存的QPS无法满足需求,作者选择了本地缓存。然而,jodd.cache.LRUCache在多线程环境下出现并发问题,导致内存占用过高。通过加锁解决了并发问题,并保持了性能。测试结果显示,加锁后的LRUCache表现稳定,内存使用正常。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  • LRU (Least recently used) 最近最少使用,如果数据最近被访问过,那么将来被访问的几率也更高。
  • LFU (Least frequently used) 最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。

jodd.cache.LRUCache本身使用很简单,

static LRUCache<String, String> cache = new LRUCache(500000);

直接new 一个LRUCache, 并设置缓存大小即可, 来一条数据get一下, 没有就继续走, 有就返回, 并且每条最后都再put一次.  实测LRUCache 和LFUCache缓存命中率极其接近, 但LRUCache效率稍高(其实效率都很高, 相较于qps 700左右的redisCache).

        为什么不用redisCache, 实测redisCache的qps在700左右, 不能满足线上需求, 而LRUCache/LFUCache 10w缓存qps103w/128w, 100w缓存qps52w/73w.

        为什么不设置缓存过期时间, 实测不管是本地缓存还是redis缓存, 加了过期时间后性能都下降厉害, 如LRUCache 设置缓存10w, 1h过期时间, qps只有3000, 且设置过期时间后命中率相差不大. 

        为什么用jodd.cache.LRUCache, 因为先采用的org.redisson.cache.LRUCacheMap被坑了, 也同样有验证的并发bug. org.redisson.cache.LRUCacheMap 在多线程, 缓存满时, 会导致cpu占用100%! 并且被阻塞. 

        jodd.cache.LRUCache有坑吗, 有, jodd.cache.LRUCache也有严重的并发bug, 在多线程中对LRUCache.put时, LRUCache.size会超过初始化定义的size! 然后内存占用缓慢增加直到100%,  见下面测试代码跑出来的日志和内存信息. 

import jodd.cache.LRUCache;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.util.List;
import java.util.concurrent.*;

public class LruStressTest {

    private final static Logger logger = LoggerFactory.getLogger(LruStressTest.class);

    public static void main(String[] args) throws Exception {
        logger.info("test start...");
        test2();
    }

    private final static Semaphore semaphore = new Semaphore(30);
    private static long total = 0L;
    private static long hitCount = 0L;

    private static LRUCache<String, String> cache = new LRUCache(500000);
    private static ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(10000);

    static void test2() throws Exception {
        LruStressTest lruStressTest = new LruStressTest();

        //start queue thread
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        singleThreadExecutor.submit(() -> {
            try {
                readToQueue();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        //consumer thread
        long startMs = System.currentTimeMillis();
        ExecutorService taskThreadExecutor = Executors.newFixedThreadPool(30);
        for (; ; ) {
            semaphore.acquire();
            total++;
            String text = queue.take();
            taskThreadExecutor.submit(() -> {
                try {
                    String v = lruStressTest.getCacheValue(text);
                    if (StringUtils.isNotBlank(v)) {
                        hitCount++;
                    } else {
                        v = String.valueOf(System.currentTimeMillis());
                        TimeUnit.MILLISECONDS.sleep(1);
                    }
                    lruStressTest.putCacheValue(text, v);
                } catch (Exception e) {
                    e.printStackTrace();
                    throw new IllegalStateException("handle error");
                } finally {
                    semaphore.release();
                }
            });
            if (total % 10000 == 0){
                logger.info("total: {}, hits: {}, queue.size: {} spendMs: {}", total, hitCount, queue.size(), System.currentTimeMillis() - startMs);
                logger.info("cache.size: {}, getHitCount: {}, getMissCount: {}, isFull: {}",
                        cache.size(), cache.getHitCount(), cache.getMissCount(), cache.isFull() );
            }

        }

    }

    public String getCacheValue(String key){
        return cache.get(key);
    }

    //must add synchronized here !
    public void putCacheValue(String key, String value){
        cache.put(key, value);
    }

    //read local text file to queue, mock prd env
    private static void readToQueue() throws Exception {
        File dir = new File("E:\\Projects\\text\\");
        File[] files = dir.listFiles();
        int i = 0;
        for (;;){
            for (File file : files) {
                List<String> lines = FileUtils.readLines(file);
                for (String line : lines) {
                    queue.add(line);
                    i++;
                    if (i>=100){
                        if (queue.size()>0) TimeUnit.MILLISECONDS.sleep(10);
                        i = 0;
                    }
                }
            }
        }
    }


}
14:16:22.105 [main] INFO LruStressTest - total: 23700000, hits: 13556179, queue.size: 0 spendMs: 3248056
14:16:22.105 [main] INFO LruStressTest - cache.size: 9734776, getHitCount: 13045869, getMissCount: 9985785, isFull: true
14:16:29.920 [main] INFO LruStressTest - total: 23710000, hits: 13566116, queue.size: 0 spendMs: 3255871
14:16:29.920 [main] INFO LruStressTest - cache.size: 9734776, getHitCount: 13055325, getMissCount: 9985785, isFull: true

 

        跑出来的日志信息 cache.size: 9734776! 内存一直飙高! 所以 cache.put上一定要加个锁, 实测加锁后正常, 且性能无差异. 

    public synchronized String getCacheValue(String key){
        return cache.get(key);
    }
    
    public synchronized void putCacheValue(String key, String value){
        cache.put(key, value);
    }
16:06:59.832 [main] INFO LruStressTest - total: 43490000, hits: 7916914, queue.size: 0 spendMs: 5100450
16:06:59.832 [main] INFO LruStressTest - cache.size: 500000, getHitCount: 7917744, getMissCount: 35572254, isFull: true
16:07:01.045 [main] INFO LruStressTest - total: 43500000, hits: 7918649, queue.size: 0 spendMs: 5101663
16:07:01.045 [main] INFO LruStressTest - cache.size: 500000, getHitCount: 7919478, getMissCount: 35580522, isFull: true

        加锁后cache.size正常, 内存最高占用2G且后续回收正常了.

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值