> 文档中心 > 手写可持久化的纯JDK缓存

手写可持久化的纯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<Map.Entry> entries = map.entrySet();     Iterator<Map.Entry> 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<Map.Entry> entries = map.entrySet();     Iterator<Map.Entry> 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<list.size();i++ ){      writer.write(list.get(i));      //writer.newLine();  }  writer.flush();  writer.close();  osw.close();  fos.close();  // 索引删除  indexMap.remove(key);    return true;     } catch (FileNotFoundException e) {  e.printStackTrace();     } catch (IOException e) {  e.printStackTrace();     } finally {  if(reader != null){      try {   reader.close();      } catch (IOException e) {   e.printStackTrace();      }  }     } } return false;    }

      为了保证,系统每次重启的时候,都能重新的从文件中获取数据,添加静态代码块,用于初始化。主要是生成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<String, List> collect = list.stream().collect(Collectors.groupingBy(CacheEntity::getKey));      Set<Map.Entry<String, List>> entries = collect.entrySet();      Iterator<Map.Entry<String, List>> iterator = entries.iterator();      while (iterator.hasNext()) {   Map.Entry<String, List> 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

     可以看到多了一个文件

 

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

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

 

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