首页 > 技术文章 > 缓存实战小项目

hello-liu 2021-12-16 01:30 原文

高性能缓存项目

目标:从0开始迭代,手把手一步步设计并实现

概述

缓存的用处:缓存在实际生产中是非常重要的工具,有了缓存之后,我们可以避免重复计算,提高吞吐量

虽然缓存乍一看很简单,不就是一个Map吗?最初级的缓存确实可以用一个Map来实现,不过一个功能晚辈、性能强劲的缓存,需要考虑的店就非常多了,我们从最简单的HashMap入手,一步步提高我们缓存的性能。

最初始版本

从最简单的案例开始

package imooccache;

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

/**
 * 最简单的形式:HashMap
 */
public class IMoocCache {

		//v1.0
  //  private static  HashMap<String,Integer> cache = new HashMap<>();
		//v2.0  加入final后,该变量只能被被赋值一次
     private static final  HashMap<String,Integer> cache = new HashMap<>();

 //   public static Integer computer(String userId) throws InterruptedException {
 public synchronized Integer computer(String userId) throws InterruptedException {
        Integer result = cache.get(userId);
        //先检查HshMap里面有没有保存过之前的计算结果
        if(result==null){

                result = doCompute(userId);


            cache.put(userId,result);
        }

        return result;

    }


    private static Integer doCompute(String userId) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);

        return new Integer(userId);
    }


    public static void main(String[] args) throws InterruptedException {

        IMoocCache iMoocCache = new IMoocCache();
        System.out.println("开始计算了");
        Integer result = computer("13");

        System.out.println("第一次计算结果"+result);
      result =  IMoocCache.computer("13");

        System.out.println("第二次计算结果"+result);
    }
}

程序第二次计算结果将会远远快于第一次,因为程序将第一次计算结果存在了HashMap中,计算时先查询HashMap

但是V1.0版本的代码存在问题:程序中使用了hashmap,这是线程不安全的,这时候我们很容易想到利用synchronize关键字来修饰hashmap,以保证其线程安全性。但是使用synchronize关键字的V2.0同样会带来问题,如上所示,整个synchronize锁的范围过大,业务代码和非业务代码全部锁住了。这会导致程序运行效率低下,代码的复用性差,这时候考虑给hashmap加final关键字,并且使用装饰者模式把代码侵入的问题解决。

解决代码复用性差问题:

使用装饰者模式重构代码

这里我们假设ExpensiveFunction类是耗时计算的实现类,实现了Computable接口,但是其本身不具备缓存功能,也不需要考虑缓存的事情。

为了将业务代码compute方法解耦,写一个接口

package imooccache.computable;

/**
 * 有一个计算函数computer,用来代表耗时计算,每个计算器都要事先这个接口,这样就可以无情如实现缓存功能
 */

public interface Computable<A,V> {
    V compute(A arg) throws Exception; 

}

其实现类为:

package imooccache.computable;

public class ExpensiveFunction implements Computable<String, Integer> {


    @Override
    public Integer compute(String arg) throws Exception {
        Thread.sleep(5000);
        return Integer.valueOf(arg);
    }
}

具体代码如下:

package imooccache;

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

import java.util.HashMap;
import java.util.Map;

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没有被计算
            result = c.compute(arg);

            //计算了结果后就需要将它放入缓存中

            cache.put(arg,result);
        }

        return result;
    }


    public static void main(String[] args) {


        ImoocCache2<String, Integer> expensiveComputer = new ImoocCache2<>(
                new ExpensiveFunction());
        try {
            Integer result = expensiveComputer.compute("666");
            System.out.println("第一次计算结果"+result);
            result = expensiveComputer.compute("666");
            System.out.println("第二次计算结果"+result);
        } catch (Exception e) {
            e.printStackTrace();
        }





    }
}

##### 使用ConcurrentHashMap

​ 但是目前程序仍然存在问题:那就是性能差,不能并行计算,一开始尝试把synchronize的范围缩小,但是此时是写安全的,但是读仍然不安全,因此将hashmap换成concurrenthashmap

private final Map<A,V> cache = new HashMap<>();

改为

private final Map<A,V> cache = new ConcurrentHashMap<>();

但是ConcurrentHashMap也会带来新的问题,它会导致在计算完成前,另一个要求计算 相同值得请求到来的时候,会导致计算两边,这和缓存想避免多次计算的初衷恰恰相反,这是不可接受的。这个时候就要引入Future和Callable来避免重复计算的问题,修改后的代码如下:

package imooccache.computable;

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

/**
 * 利用future避免重复计算
 * @param <A>
 * @param <V>
 */

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


    private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();

    private final Computable<A,V> c;

    public ImoocCache7(Computable<A, V> c) {
        this.c = c;
    }

    @Override
    public V compute(A arg) throws Exception {
        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为空,所以需要对f赋值
            f = ft;
            cache.put(arg,ft);
            System.out.println("从futureTask调用了计算函数");
            ft.run();

        }

        return f.get();

    }


    public static void main(String[] args) {


        ImoocCache7<String, Integer> expensiveComputer = new ImoocCache7<>(
                new ExpensiveFunction());
        try {
            Integer result = expensiveComputer.compute("666");
            System.out.println("第一次计算结果"+result);
            result = expensiveComputer.compute("666");
            System.out.println("第二次计算结果"+result);
        } catch (Exception e) {
            e.printStackTrace();
        }




    }
}

用原子操作消除重复计算

​ 上面的代码这个时候仍然会存在重复的可能,比如两个线程在时间上十分接近(可以看成同时)的运行相同的操作,那么仍然会创建两个任务去计算相同的值。这时候我们可以使用原子操作putIfAbsent来避免。

将上面代码中的put改成putIfAbsent

package imooccache;

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

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

/**
 * 利用future避免重复计算
 * @param <A>
 * @param <V>
 */

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


    private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();

    private final Computable<A,V> c;

    public ImoocCache8(Computable<A, V> c) {
        this.c = c;
    }

    @Override
    public V compute(A arg) throws Exception {
        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 = ft;
    /****/      f =  cache.putIfAbsent(arg,ft);

          if(f==null){
              f = ft;
              System.out.println("从futureTask调用了计算函数");
              ft.run();
          }


        }

        return f.get();

    }


    public static void main(String[] args) {


        ImoocCache8<String, Integer> expensiveComputer = new ImoocCache8<>(
                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();
    }
}

异常处理

对于一个高性能的缓存,我们还需要考虑针对各种异常的处理方法。

我们假设会出现异常

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

改为

ImoocCache10<String, Integer> expensiveComputer = new ImoocCache10<>(new MayFail());

MayFail方法的具体代码如下:

package imooccache.computable;

import java.io.IOException;
import java.util.concurrent.ThreadPoolExecutor;

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

    @Override
    public Integer compute(String arg) throws Exception {
        double random = Math.random();
        if (random > 0.5) {
            throw new IOException("读取文件出错");
        }
        Thread.sleep(3000);
        return Integer.valueOf(arg);
    }
}

将compute方法进行改造

 @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);
            }
        }
    }
缓存过期

通常情况下,缓存不是数据库,因此,我们需要对缓存设置过期时间。所以我们给缓存添加过期功能

在这里我们写一个compute的重载方法,参数中包含超时时间

   public V compute(A arg, long expire) throws ExecutionException, InterruptedException {
        if (expire>0) {
            executor.schedule(new Runnable() {
                @Override
                public void run() {
                    expire(arg);
                }
            }, expire, TimeUnit.MILLISECONDS);
        }
        return compute(arg);
    }

其中的expire方法的具体代码如下:

 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);
        }
    }

整体的代码如下:

package imooccache;

import imooccache.computable.Computable;
import imooccache.computable.MayFail;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 描述:     出于安全性考虑,缓存需要设置有效期,到期自动失效,否则如果缓存一直不失效,那么带来缓存不一致等问题
 */
public class ImoocCache10<A, V> implements Computable<A, V> {

    private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();

    private final Computable<A, V> c;

    public ImoocCache10(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);
            }
        }
    }

    public V computeRandomExpire(A arg) throws ExecutionException, InterruptedException {
        long randomExpire = (long) (Math.random() * 10000);
        return compute(arg, randomExpire);
    }

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

    public V compute(A arg, long expire) throws ExecutionException, InterruptedException {
        if (expire>0) {
            executor.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 static void main(String[] args) throws Exception {
        ImoocCache10<String, Integer> expensiveComputer = new ImoocCache10<>(
                new MayFail());
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Integer result = expensiveComputer.compute("666",5000L);
                    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();


        Thread.sleep(6000L);
        Integer result = expensiveComputer.compute("666");
        System.out.println("主线程的计算结果:" + result);
    }
}

上面代码中,我们还考虑了如果出现大量缓存同时过期,这会导致出现缓存击穿的情况,因此对过期时间进行随机设置。

测试性能
package imooccache;

import imooccache.computable.ExpensiveFunction;

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

public class ImoocCache13 {

    static ImoocCache10<String,Integer> expensiveComputer = new ImoocCache10<>(new ExpensiveFunction());
    public static void main(String[] args) {
    //测试20000个线程执行的效率
        ExecutorService executorService = Executors.newFixedThreadPool(20000);


        for (int i = 0; i < 20000; i++) {
            executorService.submit(()->{
                Integer result = null;


                try {
                   result = expensiveComputer.compute("666");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }

                System.out.println(result);
            });
        }

    }
}

性能测试优化

因为现成的提交时间还是不是完全同步的,因此我们需要利用CountDownLatch来实现线程的统一触发

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;

/**
 * 描述:     TODO
 */
public class ImoocCache12 {

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

    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(100);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
            service.submit(() -> {
                Integer result = null;
                try {
                    System.out.println(Thread.currentThread().getName()+"开始等待");
                    countDownLatch.await();
                    SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatter.get();
                    String time = dateFormat.format(new Date());
                    System.out.println(Thread.currentThread().getName()+"   "+time+"被放行");
                    result = expensiveComputer.compute("666");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
                System.out.println(result);
            });
        }

        Thread.sleep(5000);
        countDownLatch.countDown();
        service.shutdown();
    }
}
class ThreadSafeFormatter {

    public static ThreadLocal<SimpleDateFormat> dateFormatter = new ThreadLocal<SimpleDateFormat>() {

        //每个线程会调用本方法一次,用于初始化
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("mm:ss");
        }

        //首次调用本方法时,会调用initialValue();后面的调用会返回第一次创建的值
        @Override
        public SimpleDateFormat get() {
            return super.get();
        }
    };
}

推荐阅读