防止Redis缓存击穿:模仿go singleflight实现

目录

什么是缓存击穿

Go singleflight防止缓存击穿

Java代码简单实现


 

 

什么是缓存击穿

指一个Key非常热,在不停的扛着并发,并发集中对这一个点进行访问,当这个Key在Redis失效的瞬间,持续的并发请求就穿破缓存去数据库查找,对数据库造成极大压力。

缓存击穿

Go singleflight防止缓存击穿

Go singleflight常用来防止缓存击穿,简单来说就是一个key只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存中获取数据即可。

详见Go singleflight

Java代码简单实现

有以下需要注意的功能点

  1. 同一个key多个线程只允许一个线程更新,其他线程阻塞等待
  2. 同一个key,多个线程共享一个排他的写锁,缓存就绪后允许多线程并发读
  3. 当一个线程更新完Redis,其他线程能并行读取Redis里面的缓存,提高吞吐量
  4. 本地缓存一段时间key的访问与更新记录,定期清理

下面看代码。核心使用了Semaphore来限流,通过ConcurrentHashMap为key绑定一个Semaphore令牌。

Semaphore初始阶段令牌数为1,多线程请求同一个key,只有一个线程能拿到令牌,其他线程阻塞。

通过AtomicLong记录线程总数,后续用来申请令牌数,实现并发读取。

当单线程更新好缓存后,通过Semaphore.release(线程访问总数-1)申请令牌,其他阻塞的线程拿到足够令牌并发读取Redis。

定期清理ConcurrentHashMap避免内存吃紧。

public class TokenEntity {

    //令牌,一开始只允许一个线程拿到令牌,实现多线程更新串行化
    public Semaphore semaphore = new Semaphore(1);
    //记录线程访问次数
    public AtomicLong count = new AtomicLong(0);
    //记录当前时间,方面后续缓存清理
    public Long time = System.currentTimeMillis();
}



public class SingleFlight {

    // 定期清理访问记录
    private static ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(
        1);

    //模拟远程cache
    private static final Map<Integer, String> userCache = new ConcurrentHashMap<>();
    //请求访问记录
    private static Map<Integer, Long> requestRecordMap = new ConcurrentHashMap<>();
    //key对应的令牌,初始只允许一个线程拿到令牌
    private static Map<Integer, TokenEntity> semaphoreMap = new ConcurrentHashMap<>();
    //线程池
    private static final ExecutorService executorService = Executors.newFixedThreadPool(20);

    public static String getValue(int id) {
        //读取缓存
        String result = userCache.get(id);
        if (result == null) {
            //访问记录不为null表示已经有其他线程更新过了,直接从缓存读取
            if (requestRecordMap.get(id) != null) {
                return userCache.get(id);
            } else {
                //只允许一个线程访问,为当前请求id绑定一个令牌
                TokenEntity tokenEntity = semaphoreMap.computeIfAbsent(id, k -> new TokenEntity());
                //同一个id记录访问数量,方便后续增大令牌
                tokenEntity.count.incrementAndGet();
                //获取一个令牌
                try {
                    tokenEntity.semaphore.acquire();
                    //拿到令牌后,再次检查缓存是否被更新过,被更新过直接读取缓存
                    if (requestRecordMap.get(id) != null) {
                        //释放自己的令牌
                        tokenEntity.semaphore.release();
                        //System.out.println("直接缓存读取 = " + Thread.currentThread().getName());
                        return userCache.get(id);
                    }
                    result = Thread.currentThread().getName();
                    //缓存更新一次
                    System.out.println("只有一个线程更新缓存");
                    userCache.put(id, result);
                    //记录访问
                    requestRecordMap.put(id, System.currentTimeMillis());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //释放并增大令牌,允许剩下的线程并发拿到令牌
                    tokenEntity.semaphore.release(tokenEntity.count.intValue() - 1);
                }
            }
        } else {
            System.out.println("直接缓存读取 2");
        }
        return result;
    }

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 20; i++) {
            final int j = i;
            executorService.submit(() -> {
                String result = getValue(9);
                //System.out.println(result + " " + j);
            });
        }

        for (int i = 0; i < 20; i++) {
            final int j = i;
            executorService.submit(() -> {
                String result = getValue(9);
                // System.out.println(result + " " + j);
            });
        }
        executorService.shutdown();
        //清理本地缓存,本地缓存清理周期要小于远程缓存过期时间
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            System.out.println("定时清理");
            clearRequestRecordMap();
        }, 0, 10, TimeUnit.SECONDS);
    }

    private static void clearRequestRecordMap() {
        Iterator<Entry<Integer, Long>>
            iterator = requestRecordMap.entrySet().iterator();
        long current = System.currentTimeMillis();
        // Iterate over the HashMap
        while (iterator.hasNext()) {
            Map.Entry<Integer, Long> entry = iterator.next();
            // 大于十秒删除缓存
            if (current - entry.getValue() > 10000) {
                iterator.remove();
                semaphoreMap.remove(entry.getKey());
                System.out.println("清理缓存");
            }
        }
    }
}

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值