Simple File Based LRU Cache

A simple LRU cache which saves the information in IO as well. It uses a Concurrent HashMap and a Deque to manage the cache.

Cache Interface [Binding Contract]

package edu.immune.cache;

import java.io.FileNotFoundException;
import java.io.IOException;

/**
 * binding contract for creating a key/value cache
 * @author lalit mehra
 *
 * @param  generic type key
 * @param  generic type value
 */
public interface KVStore {
	/**
	 * Get value from the cache based on the supplied key
	 * @param key key to retrieve a value from the cache
	 * @return
	 * @throws IOException
	 */
	public V get(K key) throws IOException;

	/**
	 * Put key value pair to the cache
	 * @param key key to store a value in the cache
	 * @param value value of the supplied key
	 * @throws IOException
	 */
	public void put(K key, V value) throws IOException;

	/**
	 * Delete an entry from the cache
	 * @param key key to delete a value from the cache
	 * @throws IOException
	 */
	public void delete(K key) throws IOException;

	/**
	 * Clear the cache
	 * @throws IOException
	 * @throws FileNotFoundException
	 */
	public void clear() throws FileNotFoundException, IOException;

	/** Get current size of the cache
	 * @return long size of the cache
	 */
	public long size();
}

Actual Implementation

package edu.immune.cache.core;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Deque;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import com.fasterxml.jackson.databind.ObjectMapper;

import edu.immune.cache.KVStore;

/**
 * A LRU based Key Value pair that stores the value in Json Format.

 * Value is converted to json format from object notation when pushed to the
 * cache.

 * Similarly, value is converted back to Object from json when pulled out from
 * the cache.
 *
 * @author lalit mehra
 *
 * @param 
 *            Key
 * @param 
 *            Value
 */
public class JsonKVStore implements KVStore {

	private final ObjectMapper mapper;
	private Map map;
	private final ReadWriteLock lock;
	private final Class type;
	private Deque queue;
	private final int maxSize;
	private File file;
	private File tmpFile;
	private final String pairDelimiter;

	public JsonKVStore(Class clazz) {
		mapper = new ObjectMapper();
		map = new ConcurrentHashMap();
		lock = new ReentrantReadWriteLock();
		type = clazz;
		queue = new ConcurrentLinkedDeque();
		maxSize = 100;
		file = new File("/tmp/cache");
		tmpFile = new File("/tmp/tmpcache");
		pairDelimiter = ":";
	}

	/* (non-Javadoc)
	 * @see edu.immune.cache.KVStore#get(java.lang.Object)
	 */
	@Override
	public V get(K key) throws IOException {
		V value = null;
		try {
			lock.readLock().lock();
			if (map.containsKey(key)) {
				value = mapper.readValue(map.get(key), type);
				queue.remove(key);
				queue.push(key);
			} else {
				// check if file contains the value and return it
				try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
					if (reader.ready()) {
						String[] kvp;
						String pair, pKey, pValue;
						while ((pair = reader.readLine()) != null) {
							kvp = pair.split(":");
							pKey = kvp[0];
							pValue = kvp[1];
							if (pKey.equals(key)) {
								this.put(key, mapper.readValue(pValue, type));
								break;
							}
						}
					}
				}
			}
		} finally {
			lock.readLock().unlock();
		}
		return value;
	}

	/* (non-Javadoc)
	 * @see edu.immune.cache.KVStore#put(java.lang.Object, java.lang.Object)
	 */
	@Override
	public void put(K key, V value) throws IOException {
		try {
			lock.writeLock().lock();
			if (map.containsKey(key)) {
				String json = mapper.writeValueAsString(value);
				queue.remove(key);
				map.put(key, json);
				queue.push(key);
			} else {
				if (map.size() >= maxSize) {
					K qKey = queue.pop();
					map.remove(qKey);
					queue.add(key);
				} else {
					queue.add(key);
				}
				String json = mapper.writeValueAsString(value);
				// write key value pair to the file, delimited by :
				try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
					writer.append(key + pairDelimiter + json);
				}
				map.put(key, json);
			}
		} finally {
			lock.writeLock().unlock();
		}
	}

	/*
	 * @see edu.immune.cache.KVStore#delete(java.lang.Object)
	 * Data is not removed from the underlying file. 

	 * A separate scheduled task should be run from time to time to remove unwanted entries from the file.
	 */
	@Override
	public void delete(K key) throws IOException {
		try {
			lock.writeLock().lock();
			map.remove(key);
			queue.remove(key);
		} finally {
			lock.writeLock().unlock();
		}
	}

	/* (non-Javadoc)
	 * @see edu.immune.cache.KVStore#clear()
	 */
	@Override
	public void clear() throws FileNotFoundException, IOException {
		try {
			lock.writeLock().lock();
			Map tempMap = map;
			map = new ConcurrentHashMap();
			Deque tempDequeue = queue;
			queue = new ConcurrentLinkedDeque();
			tempMap.clear();
			tempMap = null;
			tempDequeue.clear();
			tempDequeue = null;

			// clear the file data
			clearFile();
		} finally {
			lock.writeLock().unlock();
		}

	}

	/* (non-Javadoc)
	 * @see edu.immune.cache.KVStore#size()
	 */
	@Override
	public long size() {
		int size = 0;
		try {
			lock.readLock().lock();
			size = map.size();
		} finally {
			lock.readLock().unlock();
		}
		return size;
	}

	/**
	 * Check if the files exist, if they do then remove the existing ones and create new ones replacing them
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
	private void clearFile() throws FileNotFoundException, IOException {
		try (BufferedReader reader = new BufferedReader(new FileReader(file));
				BufferedWriter writer = new BufferedWriter(new FileWriter(tmpFile))) {
			if (file.exists()) {
				file.delete();
				file.createNewFile();
			}
			if (tmpFile.exists()) {
				file.delete();
				file.createNewFile();
			}
		}
	}

}

Github: https://github.com/mehralalit/darkseid/tree/cache/cache

Leave a comment

Create a free website or blog at WordPress.com.

Up ↑