近期笔者搞了一个极简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
可以看到多了一个文件
输入 http://localhost:8080/test/getCache/a1
返回: {"msg":"data_a1","code":0}
好,其他功能还没有详细测试,后续有变动再分享给大家,感谢!