从0到1打造高性能缓存

1.第一版:最简单的缓存,用HashMap实现

   先检测HashMap里面 有没有 保存过之前的计算结果,若缓存中找不到,那么需要计算一下结果,并保存到HashMap 中【 该方法是若没有在map中找到,便模拟 从数据库中找,或 查 Es, 这里不关心具体的计算逻辑,只是进行休眠】
package imooccache;

import java.util.HashMap;
import java.util.concurrent.TimeUnit;

/**
 *    最简单的缓存形式 : HashMap
 *    先检测HashMap里面 有没有 保存过之前的计算结果,若缓存中找不到,那么需要计算一下结果,并保存到HashMap 中
 */
public class ImoocCache1 {

         private final HashMap<String,Integer> cash =new HashMap<>();

         public Integer ccomputer(String userId) throws InterruptedException {
                   Integer result =cash.get(userId);
                   //先检测HashMap里面 有没有 保存过之前的计算结果
                   if (result==null){
                         //若缓存中找不到,那么需要计算一下结果,并保存到HashMap 中
                       result=doCompute(userId);
                       cash.put(userId,result);
                   }
                   return  result;
         }

    /**
     *                该方法是若没有在map中找到,便模拟 从数据库中找,或 查 Es,
     *                 这里不关心具体的计算逻辑,只是进行休眠
     * @param userId
     * @return
     * @throws InterruptedException
     */
         private Integer doCompute(String userId) throws InterruptedException{
             //进行休眠
              TimeUnit.SECONDS.sleep(5);
              return new Integer(userId);
         }


    public static void main(String[] args) throws InterruptedException {
        ImoocCache1 cache1=new ImoocCache1();
        System.out.println("开始计算了");
        Integer result =cache1.ccomputer("13");
        System.out.println("第一次计算结果:"+result);
         result =cache1.ccomputer("13");
        System.out.println("第二次计算结果:"+result);
    }
}

 

2.暴露出来的性能和复用性问题:

在多线程的情况下是并发不安全的,jdk1.8的HashMap在多线程的情况下也会出现死循环的问题,但是1.8是在链表转换树或者对树进行操作的时候会出现线程安全的问题。

1.可以使用:synchronized 

这样就解决了多线程安全问题,但是随之带来两个不能忽视的问题:

1.性能差: 用了synchronized  之后,多个线程就不能同时访问了,使性能急剧下降,但是使用缓存就是看中性能优势

2.代码复用性差

         体现:

            doCompute是业务方法【可能会查数据库】,缓存在业务的方法类中写了一个hashmap, 这很不合适,缓存的内容不应该侵入到业务代码中,应该做到业务的分离,

其次在实际开发中会有多个service: 有的是服务于订单的,有的是服务于用户的,若都想用缓存,在每一个service中都创建缓存代码,侵入性极强。

若缓存逻辑有变化,在每一个创建过缓存的类都进行修改,也不科学

2.给hashmap 进行 final 修饰:

        给hashmap 添加上 final :让他的引用不改变,【map的内容还是可以改变的】

     

 

3.用装饰者模式解耦:

          使用装饰者模式进行代码重构,仅仅关系解耦,先不关心性能

装饰者模式参考:https://www.cnblogs.com/mingmingcome/p/9798248.html

本版本使用的是简化版的装饰者模式:即丢弃抽象装饰类,让具体装饰类 实现 抽象构件角色

1. 抽象构件角色:

*    有一个计算函数 computer , 用来代表耗时计算 ,每一个计算器都要实现这个接口,这样就可以无侵入实现缓存功能
/**
 *    有一个计算函数 computer , 用来代表耗时计算 ,每一个计算器都要实现这个接口,这样就可以无侵入实现缓存功能
 */
public interface Computable <A,V>{

    V compute(A arg) throws Exception ;
}

2.具体构件角色:

耗时计算的实现类,实现了 Computable接口,但是本身 不具备缓存功能,
*    也不需要考虑缓存的事情
/**
 *    耗时计算的实现类,实现了 Computable接口,但是本身 不具备缓存功能,
 *    也不需要考虑缓存的事情
 */
public class ExpensiveFunction implements Computable<String ,Integer> {

    @Override
    public Integer compute(String arg) throws Exception {

        Thread.sleep(5000);
        return Integer.valueOf(arg);
    }
}

3.具体装饰类


/**
 *    用装饰者模式,给计算机自动添加缓存功能
 */
public class ImoocCache2<A,V> implements Computable<A,V> {

    private final Map<A,V> cache =new HashMap<>();
    private final Computable<A,V> c;
    public ImoocCache2(Computable<A,V> c){
           this.c=c;
    }
    @Override
    public synchronized V compute(A arg) throws Exception {
        System.out.println("开始进行缓存机制");
        V result =cache.get(arg);
        if (result==null){
            result=c.compute(arg);
            cache.put(arg,result);
        }
        return result;
    }

    public static void main(String[] args) throws Exception {
        ImoocCache2<String,Integer> expensiveComputer  =new ImoocCache2<>(new ExpensiveFunction());
        Integer  result =expensiveComputer.compute("666");
        System.out.println("第一次计算结果:"+result);
        result =expensiveComputer.compute("666");
        System.out.println("第二次计算结果:"+result);

    }
}

1.  以后使用缓存只需要两行代码:

ImoocCache2<String,Integer> expensiveComputer  =new ImoocCache2<>(new ExpensiveFunction());
Integer  result =expensiveComputer.compute("666");

对于不同的功能,只需改变 具体构件类即可,将其放入  缓存类的构造函数中 :

ImoocCache2<String,Integer> expensiveComputer =new ImoocCache2<>(new ExpensiveFunction());

2. 若要修改缓存逻辑,只需要修改缓存类即可,不需要在每一个service类中进行修改

4.用ConcurrentHashMap保证并发安全:

上一版本的使用装饰者进行解耦,但是性能很差,【因为使用synchronized,只能一个一个的来,不适用与多线程】

1. 优化锁性能:减小锁的粒度

将 synchronized 从锁住方法,转到 锁住put操作 ,这样提高了并发效率,但 这并不意味着线程安全,因为HashMap还要考虑到同时读写情况

2. 使用ConcurrentHashMap 优化缓存:

使用 ConcurrentHashMap  即可,不需要加锁了

package imooccache;

import imooccache.computable.Computable;
import imooccache.computable.ExpensiveFunction;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ImoocCache3<A,V> implements Computable<A,V> {

    private final Map<A,V> cache =new ConcurrentHashMap<>();
    private final Computable<A,V> c;
    public ImoocCache3(Computable<A,V> c){
        this.c=c;
    }
    @Override
    public  V compute(A arg) throws Exception {
        System.out.println("开始进行缓存机制");
        V result =cache.get(arg);
        if (result==null){
            result=c.compute(arg);

                cache.put(arg,result);

        }

        return result;
    }

    public static void main(String[] args) throws Exception {
        ImoocCache2<String,Integer> expensiveComputer  =new ImoocCache2<>(new ExpensiveFunction());
        Integer  result =expensiveComputer.compute("666");
        System.out.println("第一次计算结果:"+result);
        result =expensiveComputer.compute("666");
        System.out.println("第二次计算结果:"+result);

    }
}

 

5.用Future解决重复计算问题:

package imooccache;

/**
 *    利用Future解决重复计算问题
 */


import imooccache.computable.Computable;
import imooccache.computable.ExpensiveFunction;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

public class ImoocCache4<A,V> implements Computable<A,V> {

    private final Map<A, Future<V>> cache =new ConcurrentHashMap<>();
    private final Computable<A,V> c;
    public ImoocCache4(Computable<A,V> c){
        this.c=c;
    }
    @Override
    public  V compute(A arg) throws Exception {
        System.out.println("开始进行缓存机制");
        Future<V> f=cache.get(arg);
        //重复计算都是因为没有从cache找到
        if (f==null){
            //计算任务
            Callable<V> callable=new Callable<V>() {
                @Override
                public V call() throws Exception {
                    return c.compute(arg);
                }
            };
            FutureTask<V> futureTask=new FutureTask<V>(callable);
            //注意:此时 futureTask 还没有开始计算,只是将null赋值给f
            f=futureTask;
            //cache.put 放在 futureTask.run()执行之前的好处:由于ConcurrentHashMap的可见性
            //保障了,一旦cache有值,其余线程就能立刻看见有值,拿到的f不为空,就不会进入 if (f==null) ,而是调用f.get()
            //但是在未计算完成前,会阻塞在f.get(),直到成功返回值,此时第一个线程执行 futureTask.run(),执行完后,futureTask就有值了;
            //多个线程都会从 f.get()拿到值
            cache.put(arg,futureTask);
            futureTask.run();
            System.out.println("从FutureTask调用了计算函数");
        }
        return f.get();
    }

    public static void main(String[] args) throws Exception {
        ImoocCache4<String, Integer> expensiveComputer = new ImoocCache4<>(
                new ExpensiveFunction());
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Integer result = expensiveComputer.compute("666");
                    System.out.println("第一次的计算结果:" + result);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Integer result = expensiveComputer.compute("666");
                    System.out.println("第三次的计算结果:" + result);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Integer result = expensiveComputer.compute("667");
                    System.out.println("第二次的计算结果:" + result);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();


    }
}

6.原子组合操作填补漏洞:

使用了 ConcurrentHashMap.putIfAbsent()

package imooccache;

/**
 *    利用Future解决重复计算问题
 */


import imooccache.computable.Computable;
import imooccache.computable.ExpensiveFunction;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

public class ImoocCache4<A,V> implements Computable<A,V> {

    private final Map<A, Future<V>> cache =new ConcurrentHashMap<>();
    private final Computable<A,V> c;
    public ImoocCache4(Computable<A,V> c){
        this.c=c;
    }
    @Override
    public  V compute(A arg) throws Exception {
        System.out.println("开始进行缓存机制");
        Future<V> f=cache.get(arg);
        //重复计算都是因为没有从cache找到
        if (f==null){
            //计算任务
            Callable<V> callable=new Callable<V>() {
                @Override
                public V call() throws Exception {
                    return c.compute(arg);
                }
            };
            FutureTask<V> futureTask=new FutureTask<V>(callable);
            //注意:此时 futureTask 还没有开始计算,只是将null赋值给f
            f=futureTask;
            //putIfAbsent 返回的是本次操作之前的value【对于第一个线程来说:f=null】
             // 对于第二个线程来说,发现map中已经有值了,并且 接收上一次操作放入的值futureTask
            f=cache.putIfAbsent(arg,futureTask);
            if (f==null){
                   f=futureTask;
                   futureTask.run();
                   System.out.println("从FutureTask调用了计算函数");
            }
        }
        return f.get();
    }

    public static void main(String[] args) throws Exception {
        ImoocCache4<String, Integer> expensiveComputer = new ImoocCache4<>(
                new ExpensiveFunction());
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Integer result = expensiveComputer.compute("666");
                    System.out.println("第一次的计算结果:" + result);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Integer result = expensiveComputer.compute("666");
                    System.out.println("第三次的计算结果:" + result);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Integer result = expensiveComputer.compute("667");
                    System.out.println("第二次的计算结果:" + result);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();


    }
}

7.计算错误如何处理?:

+

模拟异常:

有大约50%的机率出错

/**
 * 描述:     耗时计算的实现类,有概率计算失败
 */
public class ComputeMayThrowException implements Compute<String, Integer>{

    @Override
    public Integer compute(String arg) throws Exception {
        // 模拟实际业务的计算耗时
        double random = Math.random();
        if (random > 0.5) {
            throw new IOException("读取文件错误: " + random);
        }
        Thread.sleep(3000);
        return arg.hashCode();
    }
}

2.对于缓存类  捕获异常,对于各种异常进行不同逻辑的处理的

package imooccache;

import imooccache.computable.Computable;
import imooccache.computable.MayFail;

import java.util.Map;
import java.util.Random;
import java.util.concurrent.*;

/**
 *   出于安全性考虑,缓存需要设置有效期,到期自动失效,否则如果缓存一直不失效
 *   ,那么会带来缓存不一致等问题
 */

public class ImoocCache5<A,V> implements Computable<A,V> {

    private final Map<A, Future<V>> cache =new ConcurrentHashMap<>();
    private final Computable<A,V> c;
    public ImoocCache5(Computable<A,V> c){
        this.c=c;
    }
    @Override
    public V compute(A arg) throws InterruptedException, ExecutionException {
        while (true) {
            Future<V> f = cache.get(arg);
            if (f == null) {
                Callable<V> callable = new Callable<V>() {
                    @Override
                    public V call() throws Exception {
                        return c.compute(arg);
                    }
                };
                FutureTask<V> ft = new FutureTask<>(callable);
                f = cache.putIfAbsent(arg, ft);
                if (f == null) {
                    f = ft;
                    System.out.println("从FutureTask调用了计算函数");
                    ft.run();
                }
            }
            try {
                return f.get();
            } catch (CancellationException e) {
                System.out.println("被取消了");
                cache.remove(arg);
                throw e;
            } catch (InterruptedException e) {
                cache.remove(arg);
                throw e;
            } catch (ExecutionException e) {
                System.out.println("计算错误,需要重试");
                cache.remove(arg);
            }
        }
    }


 


}

注意:此时还是有几率发生重复计算:

当计算失败后,重新计算时,需要remove操作:

当前一个线程putIfAbsent放入值后,计算失败,此时remove,然后又有一个线程去putIfAbsent时会发现拿到的future是null,所以又会计算一次。

改进:

在捕获ExecutionException 的异常中使用了 replace方法,重新构建一个计算的FutureTask并尝试替换,理论上并发下只会有一个替换成功,其他线程替换失败则会跳过此步骤。代码如下

import java.util.Objects;
import java.util.concurrent.*;

/**
 * @auth Hahadasheng
 * @since 2020/11/27
 */
public class ComputeHandler implements Compute<String, Integer> {

    /**
     * 使用Future的目的是在并发访问下,如果请求的计算的值正在计算中,那么
     * 对应的请求线程会阻塞直到计算完毕
     */
    private static final ConcurrentHashMap<String, Future<Integer>>
        cache = new ConcurrentHashMap<>();

    /**
     * 使用装饰器模式进行解耦
     */
    //private Compute<String, Integer> compute = new DefaultCompute();
    private Compute<String, Integer> compute = new ComputeMayThrowException();

    @Override
    public Integer compute(String in) throws Exception {
        while (true) {
            Future<Integer> future = cache.get(in);
            if (Objects.isNull(future)) {
                FutureTask<Integer> futureTask = new FutureTask<>(
                        () -> compute.compute(in));
                /* 这里实现避免并发情况下的重复计算问题!
                 * putIfAbsent是一个线程安全,利用CAS原理的设置方法,
                 * 当且仅当设置的key在Map中不存在才会添加进去,并且返回null
                 * 否则设置失败并返回Map中已有的对象
                 * */
                future = cache.putIfAbsent(in, futureTask);
                if (Objects.isNull(future)) {
                    // 说明此时设置成功
                    future = futureTask;
                    System.out.println(Thread.currentThread().getName() + "从FutureTask中调用了计算函数");
                    futureTask.run(); /* 注意:这里没有启动新的线程,只是单纯想用run方法而已 */
                }
            }
            /*
            如果计算过程中出现异常又如何处理
            如果Future在计算过程中被cancel取消,则会抛出CancellationException异常
             */
            try {
                return future.get();
            } catch (CancellationException e) {
                // future.cancel() 取消的情况
                System.out.println(Thread.currentThread().getName() + "计算结果被取消");
                cache.remove(in);
                throw e;
            } catch (InterruptedException e) {
                // thread.interrupt() 中断线程
                System.out.println(Thread.currentThread().getName() + "计算线程被中断");
                cache.remove(in);
                throw e;
            } catch (ExecutionException e) {
                System.out.println(Thread.currentThread().getName() + e.getLocalizedMessage() +  "::计算错误,尝试重试...");
                /* 如果计算错误,需要将错误的future进行移除,
                /  直接调用cache.remove(in)也有重复计算的风险 */
                FutureTask<Integer> futureTaskAgain = new FutureTask<>(
                        () -> compute.compute(in));
                if (cache.replace(in, future, futureTaskAgain)) {
                    System.out.println(Thread.currentThread().getName() + "异常重试计算:从FutureTask中调用了计算函数");
                    futureTaskAgain.run();
                }
            }
        }
    }

 
}

 

8.缓存过期功能和随机性:

出于安全性考虑,缓存需要设置有效期,到期自动失效,否则如果缓存一直不失效 ,那么会带来缓存不一致等问题

使用ScheduledExecutorService  支持定时及周期性任务执行

public final static ScheduledExecutorService executer=
        Executors.newScheduledThreadPool(5);

重载了compute函数

  public  V compute(A arg,long expire) throws ExecutionException, InterruptedException {

        if (expire>0){
               executer.schedule(new Runnable() {
                   @Override
                   public void run() {
                         expire(arg);
                   }
               },expire,TimeUnit.MILLISECONDS);
        }
        //过期缓存被清除后,再次进行查找
        return compute(arg);
    }
清除缓存的具体实现方法
  public synchronized void expire(A key) {
        Future<V> future=cache.get(key);
        if (future !=null){
            if (!future.isDone()){
                System.out.println("Future 任务被取消");
                future.cancel(true);
            }
            System.out.println("过期时间到,缓存被清除");
            cache.remove(key);
        }
    }

    // 缓存过期时间随机

    // 缓存过期时间随机
    public  V computeRandomExpire(A arg) throws ExecutionException, InterruptedException {

                    long randomExpire = (long) (Math.random()*10000);
                    return compute(arg,randomExpire);
    }

9.用线程池测试缓存性能:

package imooccache;

import imooccache.computable.ExpensiveFunction;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ImoocCacheTest {

    static ImoocCache5<String,Integer>expensiveComputer=new ImoocCache5<>(new ExpensiveFunction());
    public static void main(String[] args) {
        ExecutorService service= Executors.newFixedThreadPool(3000);

        long start =System.currentTimeMillis();
        for (int i=0;i<300;i++){
                service.submit(new Runnable() {
                    @Override
                    public void run() {
                             Integer result=null;
                        try {
                            result=expensiveComputer.compute("666");
                            System.out.println(result);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } catch (ExecutionException e) {
                            e.printStackTrace();
                        }
                    }
                });
        }

        service.shutdown();
        while (!service.isTerminated()){
        }
        System.out.println("总耗时:"+(System.currentTimeMillis()-start));
    }
}

 

10.用CountDownLatch实现压测:

上边是随着任务提交到线程池就执行,这样使压力不集中,

想要看到线程统一出发,这样给服务器带来压力

public class ImoocCacheTest {

    static ImoocCache5<String,Integer>expensiveComputer=new ImoocCache5<>(new ExpensiveFunction());
    public static CountDownLatch countDownLatch=new CountDownLatch(1);

    public static void main(String[] args) throws InterruptedException {
        ExecutorService service= Executors.newFixedThreadPool(3000);

        long start =System.currentTimeMillis();
        for (int i=0;i<300;i++){
                service.submit(new Runnable() {
                    @Override
                    public void run() {
                             Integer result=null;
                        try {
                            System.out.println(Thread.currentThread().getName()+"开始等待");
                            countDownLatch.await();
                            System.out.println(Thread.currentThread().getName()+"被放行");
                            result=expensiveComputer.compute("666");
                            System.out.println(result);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } catch (ExecutionException e) {
                            e.printStackTrace();
                        }
                    }
                });
        }
        Thread.sleep(5000);
        countDownLatch.countDown();

        service.shutdown();

    }
}

11.用ThreadLocal确认时间的统一性

使用ThreadLocal ,类型为SimpleDateFormat("mm:ss")主要是查看每一个线程被放行的时间【分:秒】

若时间集中在一两秒内,则测压成功

class ThreadSafeFormatter{
        public static  ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =new ThreadLocal<SimpleDateFormat>(){
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("mm:ss");
            }
        };
}

 

package imooccache;

import imooccache.computable.ExpensiveFunction;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ImoocCacheTest {

    static ImoocCache5<String,Integer>expensiveComputer=new ImoocCache5<>(new ExpensiveFunction());
    public static CountDownLatch countDownLatch=new CountDownLatch(1);

    public static void main(String[] args) throws InterruptedException {
        ExecutorService service= Executors.newFixedThreadPool(3000);

        long start =System.currentTimeMillis();
        for (int i=0;i<300;i++){
            service.submit(new Runnable() {
                @Override
                public void run() {
                    Integer result=null;
                    try {
                        System.out.println(Thread.currentThread().getName()+"开始等待");
                        countDownLatch.await();
                        SimpleDateFormat dateFormat  =ThreadSafeFormatter.dateFormatThreadLocal.get();
                        String time =dateFormat.format(new Date());
                        System.out.println(Thread.currentThread().getName()+"    "+ time+"被放行");
                        result=expensiveComputer.compute("666");
                        System.out.println(result);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (ExecutionException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        Thread.sleep(5000);
        countDownLatch.countDown();
        service.shutdown();

    }
}

class ThreadSafeFormatter{
        public static  ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =new ThreadLocal<SimpleDateFormat>(){
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("mm:ss");
            }
        };
}

 

12.总结:

1.首先实现了一个简单的HashMap缓存,发现有很多缺陷

1.线程不安全:

                  改进1: 使用了synchronized,保证了线程安全,但性能急剧下降【后续解决】

2.

代码复用性差:缓存代码和缓存计算业务代码混在一起,侵入性强

       改进:使用了装饰者模式:进行解耦

3.性能初优化:减小锁的粒度:只在map.put()上加锁,提高了并发效率,但 这并不意味着线程安全,因为HashMap还要考虑到同时读写情况

 
性能进一步优化:使用ConcurrentHashMap,不用人为加锁

4.发现在多线程情况下:同一值可能会计算两次,使用Future 和Callable避免了重复计算

仅仅解决了大部分的重复计算,在两个线程同时计算同一值,同时调用map.get(),返回值都为null时,还是会

创建两个任务去计算同一值,使用了原子操作 map.putIfAbsent()解决了这一问题

5.为了解决:计算中抛出的异常ExecutionException,InterruptedException和CancellationException用

不同的catch语句进行捕获,使用不同的逻辑进行处理。对于后两个异常(一般都是人为的,所以直接抛出异常),

对于计算错误,则使用while(true)保证

计算成功【尝试多次直至成功】

6.但是出现了缓存污染问题,所以计算失败则移除Future,增强了健壮性

7.为了解决缓存过期问题,为每一个结果指定了过期时间,并使用ScheduledThreadPool 进行定期扫描过期元素

8.为了解决在高并发访问,同时过期,造成缓存雪崩,缓存击穿,

则使缓存时间随机化,让缓存更见安全

9.为了观测缓存效果,使用了线程池创建大量线程,使用countDownLatch对所有线程进行统一放行,达到小范围压测的目的,并且使用 ThreadLocal<SimpleDateFormat> 使每一个线程都打印当前时间来验证,压测的准确性

猜你喜欢

转载自blog.csdn.net/weixin_42528855/article/details/114115817