Faster, ThreadSafe, TTL supported Cache
I am writing the basic cache technique here to make it faster, Thread Safe, and with cache Expiry Support.
Component used:
- LinkedHashMap
a.) For saving Entry in map to get value in O(1)
b.) To remove oldest entry from Cache map if size exceeds from a limit with accessOrder property as true.It uses Doubly LinkedList which maintains order of accessing the elements in the map and eldest entry is saved in start node so that it can be removed in O(1) time and move the accessed entry to the last node.
c.) We can control size limit by using removeEldestEntry() callback provided by LinkedHashMap.
2. ReentrantReadWriteLock:
a.) It will be easy to take Read lock and Write lock separately so that our read operations may be faster if no thread is busy in writing the entry.
b.) Same thread can re-enter in the block which is acquired by the current thread for some code execution to avoid deadlock if the same thread wants to access particular line of codes.
c.) Multiple Read Operation can be executed at the same time on cache to make cache reading faster.
d.) Obviously it will make our Cache thread safe.
3. ExecutorService:
a.) Whenever our Cache Entry is close to be expiry , we can run another background task to prefetch it and update cache entry proactively.
b.) Suppose we want to update cache entry after every 4 hrs, what will we do , we will prefetch cache val from network after 3.5 hrs threshold and return previous cached value to user and Network call will update our cache entry in background on another thread.
4. Chain Of Responsibility Design Pattern
I am fetching data from Cache with following sequence MemoryCache, DatabaseCache, DiskCache and finally from Network. So there may be some other layer of cache as well in future and we can just add new layer in the chain by giving the new responsibility of fetching data from that new chain.
CacheManager will be Singleton for the Entry point of getting data from the cache.
package com.example.myapplication.cache;
public class CacheManager {
private static CacheManager mInstance;
private MemoryCache memoryCache;
public static int prefetchThreshold = 100;
public static int expiryThreshold = 120;
private CacheManager() {
memoryCache = new MemoryCache();
DBCache dbCache = new DBCache();
DiskCache diskCache = new DiskCache();
NetworkData networkData = new NetworkData();
memoryCache.setNextChain(dbCache);
memoryCache.setNetworkFetcher(networkData);
dbCache.setNextChain(diskCache);
diskCache.setNextChain(networkData);
}
public static CacheManager getInstance(){
if(mInstance == null){
synchronized (CacheManager.class){
if(mInstance == null){
mInstance = new CacheManager();
}
}
}
return mInstance;
}
public CacheValue getEntry(String key){
return memoryCache.getEntry(key);
}
public void putEntry(String key, CacheValue val){
memoryCache.putEntry(key, val);
}
}
CacheValue will represent Cache data and last timestamp when data was inserted in the Cache.
package com.example.myapplication.cache;
import android.graphics.Bitmap;
public class CacheValue {
public Bitmap bm;
public long timeStamp;
CacheValue(Bitmap bm, long timeStamp) {
}
public int getSize() {
return 1;
}
}
CacheDataFetcher will act as interface for multiple Cache Strategy
package com.example.myapplication.cache;
public interface CacheDataFetcher{
void setNextChain(CacheDataFetcher dataFetcher);
CacheValue getEntry(String key);
void deleteEntry(String key);
void updateEntry(String key, CacheValue cv);
void AddEntryIfNotPresent(String key, CacheValue cv);}
MemoryCache will fetch data from Map and if it is not found in map or expired in the map then the responsibility will be transferred to the next chain like Database Cache.
package com.example.myapplication.cache;
import com.example.myapplication.AppExecutors;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class MemoryCache extends LinkedHashMap<String, CacheValue> implements CacheDataFetcher {
private CacheDataFetcher nextChain;
private CacheDataFetcher networkFetcher;
private int maxSize = 2000;
private int currentSize = 0;
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public MemoryCache() {
super(10, 0.75f, true);
}
@Override
public void setNextChain(CacheDataFetcher dataFetcher) {
this.nextChain = dataFetcher;
}
public void setNetworkFetcher(CacheDataFetcher networkFetcher) {
this.networkFetcher = networkFetcher;
}
@Override
protected boolean removeEldestEntry(Map.Entry<String, CacheValue> eldest) {
return currentSize > maxSize;
}
public void putEntry(String key, CacheValue val) {
try {
lock.writeLock().lock();
if (this.containsKey(key)) {
CacheValue value = this.get(key);
if (value != null)
currentSize -= value.getSize();
}
currentSize += val.getSize();
this.put(key, val); //here Linkedhashmap will call removeEldestEntry and removed eldest entry if size exceeds
} catch (Exception ex) {
lock.writeLock().unlock();
} finally {
lock.writeLock().unlock();
}
}
@Override
public CacheValue getEntry(String key) {
try {
lock.readLock().lock();
CacheValue val = this.get(key);
if (val != null && val.bm != null) {
if(System.currentTimeMillis() - val.timeStamp < CacheManager.expiryThreshold) {
checkPrefetchData(val, key);
lock.readLock().unlock();
return val;
}else{
lock.writeLock().lock();
this.remove(key);
nextChain.deleteEntry(key);
lock.writeLock().unlock();
}
}
CacheValue cv2 = nextChain.getEntry(key);
putEntry(key, cv2);
checkPrefetchData(val, key);
lock.readLock().unlock();
return cv2 ;
} catch (Exception ex) {
lock.readLock().unlock();
lock.writeLock().unlock();
} finally {
lock.readLock().unlock();
}
return null;
}
public void checkPrefetchData(CacheValue val, String key){
if (System.currentTimeMillis() - val.timeStamp > CacheManager.prefetchThreshold) {
AppExecutors.getInstance().getIoDispatcherDispatcher().execute(() -> {
CacheValue cv1 = networkFetcher.getEntry(key);
nextChain.updateEntry(key, cv1);
putEntry(key,cv1);
});
}else{
nextChain.AddEntryIfNotPresent(key, cv1); } }
}
DBCache will provide cached data from the database
package com.example.myapplication.cache;
import com.example.myapplication.AppExecutors;
public class DBCache implements CacheDataFetcher{
private CacheDataFetcher nextChain;
@Override
public void setNextChain(CacheDataFetcher dataFetcher) {
this.nextChain = dataFetcher;
}
@Override
public CacheValue getEntry(String key) {
CacheValue val = null;
//val = do some db query if val is null then call next chain
if (val != null && val.bm != null) {
if (System.currentTimeMillis() - val.timeStamp < CacheManager.expiryThreshold) {
return val;
} else { nextchain.deleteEntry(key); AppExecutors.getInstance().getIoDispatcherDispatcher().execute(()->{
deleteFile(key);
});
}
}
return nextChain.getEntry(key);
}
public void deleteFile(String key){
//do file deletion here from DB
} public void deleteEntry(String key){
deleteFile(key);
nextChain.deleteEntry(key); }
public void updateEntry(String key, CacheValue cv){
// update DB with cache value of key
nextChain.updateEntry(key, cv)
} public void AddEntryIfNotPresent(String key, CacheValue cv){
// add entry in DB file here nextChain.AddEntryIfNotPresent(key, cv); }}
DiskCache will be used if data is not found in DBCache
package com.example.myapplication.cache;
import com.example.myapplication.AppExecutors;
import java.io.File;
public class DiskCache implements CacheDataFetcher{
private CacheDataFetcher nextChain;
@Override
public void setNextChain(CacheDataFetcher dataFetcher) {
this.nextChain = dataFetcher;
}
@Override
public CacheValue getEntry(String key) {
File f = new File(key);
if (f.exists()) {
CacheValue val = null;
//val = Do file operation to get the data
if (val != null && val.bm != null) {
if (System.currentTimeMillis() - val.timeStamp < CacheManager.expiryThreshold) {
return val;
} else { nextChain.deleteEntry(key); AppExecutors.getInstance().getIoDispatcherDispatcher().execute(()->{
deleteFile(key, f);
});
}
}
}
return nextChain.getEntry(key);
}
public synchronized void deleteFile(String key, File f){
if(f.exists()) {
//do file deletion here
}
} public void deleteEntry(String key){
deleteFile(key);
nextChain.deleteEntry(key); }
public void updateEntry(String key, CacheValue cv){
// update Diskcache with cache value of key
nextChain.updateEntry(key, cv);
} public void AddEntryIfNotPresent(String key, CacheValue cv){
// add entry in Disk file here nextChain.AddEntryIfNotPresent(key, cv); }}
NetworkData will be used in the last if Entry is not found in any of the cache Strategy.
package com.example.myapplication.cache;
import android.graphics.Bitmap;
public class NetworkData implements CacheDataFetcher{
private CacheDataFetcher nextChain;
@Override
public void setNextChain(CacheDataFetcher dataFetcher) {
this.nextChain = dataFetcher;
}
@Override
public CacheValue getEntry(String key) {
Bitmap bm = null;
// bm = do some network operation and replace null value
return new CacheValue(bm, System.currentTimeMillis());
} public void deleteEntry(String key){
// No need to do anything on network }
public void updateEntry(String key, CacheValue cv){
// no need to do anything on network
} public void AddEntryIfNotPresent(String key, CacheValue cv){
// nothing required here }}
AppExecutors will be used for handling different Thread operation using ThreadPoolExecutor.
package com.example.myapplication;
import android.os.Handler;
import android.os.Looper;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class AppExecutors {
private static AppExecutors mInstance;
private final Executor ioDispatcher;//Network,Disk,DB access
private final Executor cpuDispatcher;//CPU consumtion(Sorting)
private final Executor mainDispatcher;
private AppExecutors(){
cpuDispatcher = Executors.newSingleThreadExecutor();
ioDispatcher = Executors.newFixedThreadPool(4);
mainDispatcher = new MainDispatcher();
}
public static AppExecutors getInstance(){
if(mInstance == null){
synchronized (AppExecutors.class){
if(mInstance == null){
mInstance = new AppExecutors();
}
}
}
return mInstance;
}
// Most Thread time goes into waiting , don't consume much CPU
//no of threads may increase optimally since more thread take memory public Executor getIoDispatcherDispatcher(){
return ioDispatcher;
}// CPU consumption is more , so take 1-2 threads for this part
public Executor getCPUDispatcher(){
return cpuDispatcher;
}
public Executor getMainDispatcher(){
return mainDispatcher;
}
private static class MainDispatcher implements Executor{
private Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
@Override
public void execute(Runnable command) {
mainThreadHandler.post(command);
}
}
}
In this way , we can add more layer or chain in between the flow in future without changing too much in existing code.
If you like this way , Please clap it.
Please feel free to provide your thoughts and comments over it.Thanks.