手写可持久化的纯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}
好,其他功能还没有详细测试,后续有变动再分享给大家,感谢!