手写可持久化的纯JDK缓存

          近期笔者搞了一个极简springboot项目,不依赖mysql,redis,数据库用H2,Cache用Caffeine,其他缓存就自己写了一个可持久化的工具。开源的代码如下:

        https://github.com/EricLoveMia/simpleBoot

        本篇文章主要讲述可持久化的缓存工具。

        在没有持久化之前,从网上炒了一个工具:

    

public class Cache {

    private final static Map map;

    /** 定时器线程池,用于清除过期的缓存 */
    private static ThreadFactory threadFactory;


    //定时器线程池,用于清除过期缓存
    private final static ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

    public synchronized static void put(String key,Object data,boolean writeToFile){
        Cache.put(key,data,0,writeToFile);
    }

    public synchronized static void put(String key, Object data, long expire,boolean writeToFile) {
        // 清除原键值对
        // Cache.remove(key);

        if(expire > 0){
            Future future = executor.schedule(new Runnable() {
                @Override
                public void run() {
                    synchronized (key){
                        map.remove(key);
                    }
                }
            },expire,TimeUnit.MILLISECONDS);
            map.put(key,new Entity(data,future));
           
        }else{
            map.put(key,new Entity(data,null));
            
        }

    }

    public synchronized static Object get(String key) {
        Entity entity = map.get(key);
        return entity == null?null:entity.getValue();
    }

    /**
     * 读取缓存
     *
     * @param key 键
     *            * @param clazz 值类型
     * @return
     */
    public synchronized static  T get(String key, Class clazz) {

        return clazz.cast(Cache.get(key));
    }


    /**
     * 清除缓存
     *
     * @param key
     * @return
     */
    public synchronized static Object remove(String key) {
        //清除原缓存数据
        Entity entity = map.remove(key);
        if (entity == null){ return null ;}
        //清除原键值对定时器
        Future future = entity.getFuture();
        if (future != null) { future.cancel(true);}
        return entity.getValue();
    }

    /**
     * 查询当前缓存的键值对数量
     *
     * @return
     */
    public synchronized static int size() {
        return map.size();
    }

    /**
     * 缓存实体类
     */
    private static class Entity {
        //键值对的value
        private Object value;
        //定时器Future
        private Future future;

        public Entity(Object value, Future future) {
            this.value = value;
            this.future = future;
        }

        /**
         * 获取值
         *
         * @return
         */
        public Object getValue() {
            return value;
        }

        /**
         * 获取Future对象
         *
         * @return
         */
        public Future getFuture() {
            return future;
        }
    }



}

     上述代码,一旦断电重启,数据全部丢失。此时想到,可以将缓存的数据写入文件中,当重启的时候再从文件中读取即可。那么就要考虑文件的增删改查功能。下面一一介绍

     首先解决文件的写入问题,新增一个FileUtil的工具类

public class FileUtil {

    public static final String basePath = "cache/";

    private static final String line = "\t\n";

    private static final String[] preArray = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0"};

    static Map pathMaps = new ConcurrentHashMap<>(256);

    static Map indexMap = new ConcurrentHashMap<>(102400);

    这里 basePath表示文件的主路径,line表示行分隔符,因为数据是key value的形式一行行存入文件的。preArray是为了把数据按照key 的开头字母分开,这样可以通过分散文件的方式减少单一文件的存储压力,同时也能加快读取查找的速度。

  pathMaps 这个map主要记录了不同的前缀名对应的文件是哪个,同时记录最大行数,用于indexMap的使用;

 indexMap 这个map是索引map,为了加快查找的速度,将key和文件、行数作为索引存储起来,这样查找某个key的时候,先去索引查找是在哪个文件的哪一行,极大的提高查询的效率。

    下面就是新增的方法

    方法一,批量新增map数据(key的首字母相同),主要使用的就是FileWriter类,具体就不详述了

/** 新加数据 */
    public static void writeToFile(Map map,String keyPre) {
        StringBuffer stringBuffer = new StringBuffer();
        try {
            FileWriter fileWriter = new FileWriter(basePath + pathMaps.get(keyPre).path);
            Set> entries = map.entrySet();
            Iterator> iterator = entries.iterator();
            while (iterator.hasNext()) {
                Map.Entry next = iterator.next();
                stringBuffer.append(next.getKey() + ":" + JSONObject.toJSONString(next.getValue())).append(line);
                // TODO
                indexMap.put(next.getKey(), new IndexEntity(next.getKey(), pathMaps.get(keyPre).path, pathMaps.get(keyPre).count.incrementAndGet()));
            }
            fileWriter.write(stringBuffer.toString());
            fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

  方法二、追加map数据(key的首字母相同),与上述方法只有一个不同,就是new FileWriter(...,true); 最后一个参数true表示追加写入

/** 追加数据 */
    public static void addToFile(Map map, String keyPre) {

        StringBuffer stringBuffer = new StringBuffer();
        try {
            FileWriter fileWriter = new FileWriter(basePath + pathMaps.get(keyPre).path,true);
            Set> entries = map.entrySet();
            Iterator> iterator = entries.iterator();
            while (iterator.hasNext()) {
                Map.Entry next = iterator.next();
                stringBuffer.append(next.getKey() + ":" + JSONObject.toJSONString(next.getValue())).append(line);
                indexMap.put(next.getKey(), new IndexEntity(next.getKey(), pathMaps.get(keyPre).path, pathMaps.get(keyPre).count.incrementAndGet()));
            }
            fileWriter.write(stringBuffer.toString());
            fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

 方法三:单个key,value 的写入 这三个方法都在写入文件的同时,写入了indexMap中

public synchronized static void addToFile(String key, Object value) {
        StringBuffer stringBuffer = new StringBuffer();
        try {
            FileWriter fileWriter = new FileWriter(basePath + pathMaps.get(key.substring(0, 1)).path, true);
            stringBuffer.append(key + ":" + JSONObject.toJSONString(value)).append(line);
            // 加入索引
            indexMap.put(key, new IndexEntity(key, pathMaps.get(key.substring(0, 1)).path, pathMaps.get(key.substring(0, 1)).count.incrementAndGet()));

            fileWriter.write(stringBuffer.toString());
            fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

 

下面是查询的方法

方法一:给定文件路径,获取所有数据

public static String readFromFile(String path) {
        StringBuffer stringBuffer = new StringBuffer();
        FileReader fileReader = null;
        try {
            fileReader = new FileReader(basePath + path);
            char[] buf = new char[1024];
            int num;
            while ((num = fileReader.read(buf)) != -1) {
                stringBuffer.append(new String(buf, 0, num));
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fileReader != null) {
                try {
                    fileReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return stringBuffer.toString();
    }

方法二:给定 key值,获取数据,方法是先从index中获得文件名和第几行,然后直接找到对应的行,取出数据

public synchronized static String getByFile(String key) {
        String string = null;
        BufferedReader reader = null;
        IndexEntity indexEntity = indexMap.get(key);
        if (indexEntity != null) {
            String filePre = indexEntity.getFilePre();
            long line = indexEntity.getLine();
            try {
                reader = new BufferedReader(new FileReader(basePath + filePre));
                for (long i = 0; i < line - 1; i++) {
                    reader.readLine();
                }
                string = reader.readLine();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return string;
    }

下面是删除的方法,主要的过程就是把数据取出来,去掉删除的那行,然后重新写入文件,由于这个过程可能较长,需要定时处理删除的内容

    public synchronized static boolean deleteByKey(String key) {
        // 删除键值
        // StringBuffer stringBuffer = new StringBuffer();
        BufferedReader reader = null;
        IndexEntity indexEntity = indexMap.get(key);

        OutputStreamWriter osw;
        List list = new ArrayList<>();
        BufferedWriter writer;
        if (indexEntity != null) {
            try {
                // 拿到文件
                String filePre = indexEntity.getFilePre();
                long line = indexEntity.getLine();
                System.out.println(basePath + filePre);
                reader = new BufferedReader(new FileReader(basePath + filePre));
                String contentLine;
                while((contentLine = reader.readLine()) != null){
                    if(--line == 0) {
                        // 那一行删除
                        // writer.write("--:--\t\n");
                        list.add("--:--" + "\t\n");
                        continue;
                    }else {
                        System.out.println(contentLine);
                        list.add(contentLine + "\t\n");
                        // writer.write(contentLine + "\t\n");
                    }
                }
                

                FileOutputStream fos=new FileOutputStream(new File(basePath + filePre));
                osw= new OutputStreamWriter(fos, "UTF-8");
                writer = new BufferedWriter(osw);
                for( int i=0;i

      为了保证,系统每次重启的时候,都能重新的从文件中获取数据,添加静态代码块,用于初始化。主要是生成indexMap 和 pathMaps

 static {
        for (int i = 0; i < preArray.length; i++) {
            pathMaps.put(preArray[i], new PathEntity(preArray[i] + "-main.log"));
        }
        // 载入所有文件生成index
        File file = new File(basePath);
        File[] tempList = file.listFiles();
        int number;
        BufferedReader reader = null;
        try {
            for (File log : tempList) {
                number = 1;
                if (log.isFile()) {
                    //
                    try {
                        reader = new BufferedReader(new FileReader(basePath + log.getName()));
                        String contentLine;
                        while ((contentLine = reader.readLine()) != null) {
                            // System.out.println(contentLine);
                            String[] split = contentLine.split(":");
                            indexMap.put(split[0], new IndexEntity(split[0], log.getName(), number++));
                            pathMaps.get(split[0].substring(0,1)).getCount().incrementAndGet();
                        }
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        } finally {

            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }

         这时,我们返回原来的缓存Cache代码,修改新增缓存的操作。我们可以看到,增加了写入队列的功能,由于不能频繁的操作文件,我们把新增的请求入队,然后每隔一段时间来消费队列的内容。

public synchronized static void put(String key, Object data, long expire,boolean writeToFile) {
        // 清除原键值对
        // Cache.remove(key);

        if(expire > 0){
            Future future = executor.schedule(new Runnable() {
                @Override
                public void run() {
                    synchronized (key){
                        map.remove(key);
                    }
                }
            },expire,TimeUnit.MILLISECONDS);
            map.put(key,new Entity(data,future));
            // 写入队列,用于刷新到文件中
            if(writeToFile) {
                try {
                    QueueOffer.getOfferQueue().produce(new CacheEntity(key, data, expire));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }else{
            map.put(key,new Entity(data,null));
            // 写入队列,用于刷新到文件中
            if(writeToFile) {
                try {
                    QueueOffer.getOfferQueue().produce(new CacheEntity(key, data, -1));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }

        消费的线程我们写在Runner里面的,实现CommandLineRunner,可以在系统启动后启动一个线程来每隔10秒(这里可以写入配置文件)消费队列中的内容,将其写入文件中。

@Component
@Order(1)
public class CacheInitRunner implements CommandLineRunner {


    @Override
    public void run(String... args) throws Exception {
        System.out.println("load cache..." + Arrays.asList(args));
        new Thread(() -> {
            List list = null;
            while (true) {
                // 每10秒 刷新一次文件
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 获取队列的所有数据
                if (QueueOffer.getOfferQueue().size() > 0) {
                    list = new ArrayList<>();
                    CacheEntity entity = null;
                    for (int i = 0, length = QueueOffer.getOfferQueue().size(); i < length; i++) {
                        try {
                            entity = QueueOffer.getOfferQueue().consume();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        list.add(entity);
                    }

                    // 先分组 然后按照分组写入文件中
                    Map> collect = list.stream().collect(Collectors.groupingBy(CacheEntity::getKey));
                    Set>> entries = collect.entrySet();
                    Iterator>> iterator = entries.iterator();

                    while (iterator.hasNext()) {
                        Map.Entry> next = iterator.next();
                        List value = next.getValue();
                        Map collect1 = value.stream().collect(Collectors.toMap(CacheEntity::getKey, a -> a));
                        FileUtil.addToFile(collect1, next.getKey().substring(0, 1).toLowerCase());
                    }
                } else {
                    System.out.println("暂无缓存数据写入文件");
                }
            }
        }).start();
    }
}

           让我们来写一个测试方法来测试这个过程。

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/cacheWrite")
    public R testCacheWrite(){
        for (int i = 0; i < 1000; i++) {
            Cache.put("a"+i,"data_a"+i,true);
        }
        return R.ok();
    }

    @GetMapping("/getCache/{key}")
    public R testCacheWrite(@PathVariable String key){
        Object o = Cache.get(key);
        return R.ok(o.toString());
    }
}

      运行起我们的项目(项目地址在开头),浏览器输入:http://localhost:8080/test/cacheWrite

     可以看到多了一个文件

手写可持久化的纯JDK缓存_第1张图片

 

         输入  http://localhost:8080/test/getCache/a1

         返回: {"msg":"data_a1","code":0}

 

         好,其他功能还没有详细测试,后续有变动再分享给大家,感谢!

你可能感兴趣的:(JAVA,微服务)