diff --git a/caching/.gitignore b/caching/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/caching/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/caching/etc/caching.png b/caching/etc/caching.png new file mode 100644 index 00000000..6b3b2d05 Binary files /dev/null and b/caching/etc/caching.png differ diff --git a/caching/etc/caching.ucls b/caching/etc/caching.ucls new file mode 100644 index 00000000..815a62ec --- /dev/null +++ b/caching/etc/caching.ucls @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/caching/index.md b/caching/index.md new file mode 100644 index 00000000..f79f13e4 --- /dev/null +++ b/caching/index.md @@ -0,0 +1,24 @@ +--- +layout: pattern +title: Caching +folder: caching +permalink: /patterns/caching/ +categories: Other +tags: + - Java +--- + +**Intent:** To avoid expensive re-acquisition of resources by not releasing +the resources immediately after their use. The resources retain their identity, are kept in some +fast-access storage, and are re-used to avoid having to acquire them again. + +![alt text](./etc/caching.png "Caching") + +**Applicability:** Use the Caching pattern(s) when + +* Repetitious acquisition, initialization, and release of the same resource causes unnecessary performance overhead. + +**Credits** + +* [Write-through, write-around, write-back: Cache explained](http://www.computerweekly.com/feature/Write-through-write-around-write-back-Cache-explained) +* [Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching](https://docs.oracle.com/cd/E15357_01/coh.360/e15723/cache_rtwtwbra.htm#COHDG5177) diff --git a/caching/pom.xml b/caching/pom.xml new file mode 100644 index 00000000..d2284a5f --- /dev/null +++ b/caching/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.7.0 + + caching + + + junit + junit + test + + + org.mongodb + mongodb-driver + 3.0.4 + + + org.mongodb + mongodb-driver-core + 3.0.4 + + + org.mongodb + bson + 3.0.4 + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.19 + + false + + + + + diff --git a/caching/src/main/java/com/iluwatar/caching/App.java b/caching/src/main/java/com/iluwatar/caching/App.java new file mode 100644 index 00000000..c7f55db7 --- /dev/null +++ b/caching/src/main/java/com/iluwatar/caching/App.java @@ -0,0 +1,117 @@ +package com.iluwatar.caching; + +/** + * + * The Caching pattern describes how to avoid expensive re-acquisition of resources by not releasing + * the resources immediately after their use. The resources retain their identity, are kept in some + * fast-access storage, and are re-used to avoid having to acquire them again. There are three main + * caching strategies/techniques in this pattern; each with their own pros and cons. They are: + * write-through which writes data to the cache and DB in a single transaction, + * write-around which writes data immediately into the DB instead of the cache, and + * write-behind which writes data into the cache initially whilst the data is only + * written into the DB when the cache is full. The read-through strategy is also + * included in the mentioned three strategies -- returns data from the cache to the caller if + * it exists else queries from DB and stores it into the cache for future use. These + * strategies determine when the data in the cache should be written back to the backing store (i.e. + * Database) and help keep both data sources synchronized/up-to-date. This pattern can improve + * performance and also helps to maintain consistency between data held in the cache and the data in + * the underlying data store. + *

+ * In this example, the user account ({@link UserAccount}) entity is used as the underlying + * application data. The cache itself is implemented as an internal (Java) data structure. It adopts + * a Least-Recently-Used (LRU) strategy for evicting data from itself when its full. The three + * strategies are individually tested. The testing of the cache is restricted towards saving and + * querying of user accounts from the underlying data store ( {@link DBManager}). The main class ( + * {@link App} is not aware of the underlying mechanics of the application (i.e. save and query) and + * whether the data is coming from the cache or the DB (i.e. separation of concern). The AppManager + * ({@link AppManager}) handles the transaction of data to-and-from the underlying data store + * (depending on the preferred caching policy/strategy). + * + * App --> AppManager --> CacheStore/LRUCache/CachingPolicy --> DBManager + *

+ * + * @see CacheStore + * @See LRUCache + * @see CachingPolicy + * + */ +public class App { + + /** + * Program entry point + * + * @param args command line args + */ + public static void main(String[] args) { + AppManager.initDB(false); // VirtualDB (instead of MongoDB) was used in running the JUnit tests + // and the App class to avoid Maven compilation errors. Set flag to + // true to run the tests with MongoDB (provided that MongoDB is + // installed and socket connection is open). + AppManager.initCacheCapacity(3); + App app = new App(); + app.useReadAndWriteThroughStrategy(); + app.useReadThroughAndWriteAroundStrategy(); + app.useReadThroughAndWriteBehindStrategy(); + } + + /** + * Read-through and write-through + */ + public void useReadAndWriteThroughStrategy() { + System.out.println("# CachingPolicy.THROUGH"); + AppManager.initCachingPolicy(CachingPolicy.THROUGH); + + UserAccount userAccount1 = new UserAccount("001", "John", "He is a boy."); + + AppManager.save(userAccount1); + System.out.println(AppManager.printCacheContent()); + userAccount1 = AppManager.find("001"); + userAccount1 = AppManager.find("001"); + } + + /** + * Read-through and write-around + */ + public void useReadThroughAndWriteAroundStrategy() { + System.out.println("# CachingPolicy.AROUND"); + AppManager.initCachingPolicy(CachingPolicy.AROUND); + + UserAccount userAccount2 = new UserAccount("002", "Jane", "She is a girl."); + + AppManager.save(userAccount2); + System.out.println(AppManager.printCacheContent()); + userAccount2 = AppManager.find("002"); + System.out.println(AppManager.printCacheContent()); + userAccount2 = AppManager.find("002"); + userAccount2.setUserName("Jane G."); + AppManager.save(userAccount2); + System.out.println(AppManager.printCacheContent()); + userAccount2 = AppManager.find("002"); + System.out.println(AppManager.printCacheContent()); + userAccount2 = AppManager.find("002"); + } + + /** + * Read-through and write-behind + */ + public void useReadThroughAndWriteBehindStrategy() { + System.out.println("# CachingPolicy.BEHIND"); + AppManager.initCachingPolicy(CachingPolicy.BEHIND); + + UserAccount userAccount3 = new UserAccount("003", "Adam", "He likes food."); + UserAccount userAccount4 = new UserAccount("004", "Rita", "She hates cats."); + UserAccount userAccount5 = new UserAccount("005", "Isaac", "He is allergic to mustard."); + + AppManager.save(userAccount3); + AppManager.save(userAccount4); + AppManager.save(userAccount5); + System.out.println(AppManager.printCacheContent()); + userAccount3 = AppManager.find("003"); + System.out.println(AppManager.printCacheContent()); + UserAccount userAccount6 = new UserAccount("006", "Yasha", "She is an only child."); + AppManager.save(userAccount6); + System.out.println(AppManager.printCacheContent()); + userAccount4 = AppManager.find("004"); + System.out.println(AppManager.printCacheContent()); + } +} diff --git a/caching/src/main/java/com/iluwatar/caching/AppManager.java b/caching/src/main/java/com/iluwatar/caching/AppManager.java new file mode 100644 index 00000000..08132e32 --- /dev/null +++ b/caching/src/main/java/com/iluwatar/caching/AppManager.java @@ -0,0 +1,75 @@ +package com.iluwatar.caching; + +import java.text.ParseException; + +/** + * + * AppManager helps to bridge the gap in communication between the main class and the application's + * back-end. DB connection is initialized through this class. The chosen caching strategy/policy is + * also initialized here. Before the cache can be used, the size of the cache has to be set. + * Depending on the chosen caching policy, AppManager will call the appropriate function in the + * CacheStore class. + * + */ +public class AppManager { + + private static CachingPolicy cachingPolicy; + + /** + * + * Developer/Tester is able to choose whether the application should use MongoDB as its underlying + * data storage or a simple Java data structure to (temporarily) store the data/objects during + * runtime. + */ + public static void initDB(boolean useMongoDB) { + if (useMongoDB) { + try { + DBManager.connect(); + } catch (ParseException e) { + e.printStackTrace(); + } + } else { + DBManager.createVirtualDB(); + } + } + + public static void initCachingPolicy(CachingPolicy policy) { + cachingPolicy = policy; + if (cachingPolicy == CachingPolicy.BEHIND) { + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + CacheStore.flushCache(); + } + })); + } + CacheStore.clearCache(); + } + + public static void initCacheCapacity(int capacity) { + CacheStore.initCapacity(capacity); + } + + public static UserAccount find(String userID) { + if (cachingPolicy == CachingPolicy.THROUGH || cachingPolicy == CachingPolicy.AROUND) { + return CacheStore.readThrough(userID); + } else if (cachingPolicy == CachingPolicy.BEHIND) { + return CacheStore.readThroughWithWriteBackPolicy(userID); + } + return null; + } + + public static void save(UserAccount userAccount) { + if (cachingPolicy == CachingPolicy.THROUGH) { + CacheStore.writeThrough(userAccount); + } else if (cachingPolicy == CachingPolicy.AROUND) { + CacheStore.writeAround(userAccount); + } else if (cachingPolicy == CachingPolicy.BEHIND) { + CacheStore.writeBehind(userAccount); + } + } + + public static String printCacheContent() { + return CacheStore.print(); + } +} diff --git a/caching/src/main/java/com/iluwatar/caching/CacheStore.java b/caching/src/main/java/com/iluwatar/caching/CacheStore.java new file mode 100644 index 00000000..2041ac14 --- /dev/null +++ b/caching/src/main/java/com/iluwatar/caching/CacheStore.java @@ -0,0 +1,104 @@ +package com.iluwatar.caching; + +import java.util.ArrayList; + +/** + * + * The caching strategies are implemented in this class. + * + */ +public class CacheStore { + + static LRUCache cache = null; + + public static void initCapacity(int capacity) { + if (null == cache) + cache = new LRUCache(capacity); + else + cache.setCapacity(capacity); + } + + public static UserAccount readThrough(String userID) { + if (cache.contains(userID)) { + System.out.println("# Cache Hit!"); + return cache.get(userID); + } + System.out.println("# Cache Miss!"); + UserAccount userAccount = DBManager.readFromDB(userID); + cache.set(userID, userAccount); + return userAccount; + } + + public static void writeThrough(UserAccount userAccount) { + if (cache.contains(userAccount.getUserID())) { + DBManager.updateDB(userAccount); + } else { + DBManager.writeToDB(userAccount); + } + cache.set(userAccount.getUserID(), userAccount); + } + + public static void writeAround(UserAccount userAccount) { + if (cache.contains(userAccount.getUserID())) { + DBManager.updateDB(userAccount); + cache.invalidate(userAccount.getUserID()); // Cache data has been updated -- remove older + // version from cache. + } else { + DBManager.writeToDB(userAccount); + } + } + + public static UserAccount readThroughWithWriteBackPolicy(String userID) { + if (cache.contains(userID)) { + System.out.println("# Cache Hit!"); + return cache.get(userID); + } + System.out.println("# Cache Miss!"); + UserAccount userAccount = DBManager.readFromDB(userID); + if (cache.isFull()) { + System.out.println("# Cache is FULL! Writing LRU data to DB..."); + UserAccount toBeWrittenToDB = cache.getLRUData(); + DBManager.upsertDB(toBeWrittenToDB); + } + cache.set(userID, userAccount); + return userAccount; + } + + public static void writeBehind(UserAccount userAccount) { + if (cache.isFull() && !cache.contains(userAccount.getUserID())) { + System.out.println("# Cache is FULL! Writing LRU data to DB..."); + UserAccount toBeWrittenToDB = cache.getLRUData(); + DBManager.upsertDB(toBeWrittenToDB); + } + cache.set(userAccount.getUserID(), userAccount); + } + + public static void clearCache() { + if (null != cache) + cache.clear(); + } + + /** + * Writes remaining content in the cache into the DB. + */ + public static void flushCache() { + System.out.println("# flushCache..."); + if (null == cache) + return; + ArrayList listOfUserAccounts = cache.getCacheDataInListForm(); + for (UserAccount userAccount : listOfUserAccounts) { + DBManager.upsertDB(userAccount); + } + } + + public static String print() { + ArrayList listOfUserAccounts = cache.getCacheDataInListForm(); + StringBuilder sb = new StringBuilder(); + sb.append("\n--CACHE CONTENT--\n"); + for (UserAccount userAccount : listOfUserAccounts) { + sb.append(userAccount.toString() + "\n"); + } + sb.append("----\n"); + return sb.toString(); + } +} diff --git a/caching/src/main/java/com/iluwatar/caching/CachingPolicy.java b/caching/src/main/java/com/iluwatar/caching/CachingPolicy.java new file mode 100644 index 00000000..314cfaa3 --- /dev/null +++ b/caching/src/main/java/com/iluwatar/caching/CachingPolicy.java @@ -0,0 +1,20 @@ +package com.iluwatar.caching; + +/** + * + * Enum class containing the three caching strategies implemented in the pattern. + * + */ +public enum CachingPolicy { + THROUGH("through"), AROUND("around"), BEHIND("behind"); + + private String policy; + + private CachingPolicy(String policy) { + this.policy = policy; + } + + public String getPolicy() { + return policy; + } +} diff --git a/caching/src/main/java/com/iluwatar/caching/DBManager.java b/caching/src/main/java/com/iluwatar/caching/DBManager.java new file mode 100644 index 00000000..07a5daea --- /dev/null +++ b/caching/src/main/java/com/iluwatar/caching/DBManager.java @@ -0,0 +1,123 @@ +package com.iluwatar.caching; + +import java.text.ParseException; +import java.util.HashMap; + +import org.bson.Document; + +import com.mongodb.MongoClient; +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.UpdateOptions; + +/** + * + *

DBManager handles the communication with the underlying data store i.e. Database. It contains the + * implemented methods for querying, inserting, and updating data. MongoDB was used as the database + * for the application.

+ * + *

Developer/Tester is able to choose whether the application should use MongoDB as its underlying + * data storage (connect()) or a simple Java data structure to (temporarily) store the data/objects + * during runtime (createVirtualDB()).

+ * + */ +public class DBManager { + + private static MongoClient mongoClient; + private static MongoDatabase db; + private static boolean useMongoDB; + + private static HashMap virtualDB; + + public static void createVirtualDB() { + useMongoDB = false; + virtualDB = new HashMap(); + } + + public static void connect() throws ParseException { + useMongoDB = true; + mongoClient = new MongoClient(); + db = mongoClient.getDatabase("test"); + } + + public static UserAccount readFromDB(String userID) { + if (!useMongoDB) { + if (virtualDB.containsKey(userID)) + return virtualDB.get(userID); + return null; + } + if (null == db) { + try { + connect(); + } catch (ParseException e) { + e.printStackTrace(); + } + } + FindIterable iterable = + db.getCollection("user_accounts").find(new Document("userID", userID)); + if (iterable == null) + return null; + Document doc = iterable.first(); + UserAccount userAccount = + new UserAccount(userID, doc.getString("userName"), doc.getString("additionalInfo")); + return userAccount; + } + + public static void writeToDB(UserAccount userAccount) { + if (!useMongoDB) { + virtualDB.put(userAccount.getUserID(), userAccount); + return; + } + if (null == db) { + try { + connect(); + } catch (ParseException e) { + e.printStackTrace(); + } + } + db.getCollection("user_accounts").insertOne( + new Document("userID", userAccount.getUserID()).append("userName", + userAccount.getUserName()).append("additionalInfo", userAccount.getAdditionalInfo())); + } + + public static void updateDB(UserAccount userAccount) { + if (!useMongoDB) { + virtualDB.put(userAccount.getUserID(), userAccount); + return; + } + if (null == db) { + try { + connect(); + } catch (ParseException e) { + e.printStackTrace(); + } + } + db.getCollection("user_accounts").updateOne( + new Document("userID", userAccount.getUserID()), + new Document("$set", new Document("userName", userAccount.getUserName()).append( + "additionalInfo", userAccount.getAdditionalInfo()))); + } + + /** + * + * Insert data into DB if it does not exist. Else, update it. + */ + public static void upsertDB(UserAccount userAccount) { + if (!useMongoDB) { + virtualDB.put(userAccount.getUserID(), userAccount); + return; + } + if (null == db) { + try { + connect(); + } catch (ParseException e) { + e.printStackTrace(); + } + } + db.getCollection("user_accounts").updateOne( + new Document("userID", userAccount.getUserID()), + new Document("$set", new Document("userID", userAccount.getUserID()).append("userName", + userAccount.getUserName()).append("additionalInfo", userAccount.getAdditionalInfo())), + new UpdateOptions().upsert(true)); + } +} diff --git a/caching/src/main/java/com/iluwatar/caching/LRUCache.java b/caching/src/main/java/com/iluwatar/caching/LRUCache.java new file mode 100644 index 00000000..872f9725 --- /dev/null +++ b/caching/src/main/java/com/iluwatar/caching/LRUCache.java @@ -0,0 +1,146 @@ +package com.iluwatar.caching; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * + * Data structure/implementation of the application's cache. The data structure consists of a hash + * table attached with a doubly linked-list. The linked-list helps in capturing and maintaining the + * LRU data in the cache. When a data is queried (from the cache), added (to the cache), or updated, + * the data is moved to the front of the list to depict itself as the most-recently-used data. The + * LRU data is always at the end of the list. + * + */ +public class LRUCache { + + class Node { + String userID; + UserAccount userAccount; + Node previous; + Node next; + + public Node(String userID, UserAccount userAccount) { + this.userID = userID; + this.userAccount = userAccount; + } + } + + int capacity; + HashMap cache = new HashMap(); + Node head = null; + Node end = null; + + public LRUCache(int capacity) { + this.capacity = capacity; + } + + public UserAccount get(String userID) { + if (cache.containsKey(userID)) { + Node node = cache.get(userID); + remove(node); + setHead(node); + return node.userAccount; + } + return null; + } + + /** + * + * Remove node from linked list. + */ + public void remove(Node node) { + if (node.previous != null) { + node.previous.next = node.next; + } else { + head = node.next; + } + if (node.next != null) { + node.next.previous = node.previous; + } else { + end = node.previous; + } + } + + /** + * + * Move node to the front of the list. + */ + public void setHead(Node node) { + node.next = head; + node.previous = null; + if (head != null) + head.previous = node; + head = node; + if (end == null) + end = head; + } + + public void set(String userID, UserAccount userAccount) { + if (cache.containsKey(userID)) { + Node old = cache.get(userID); + old.userAccount = userAccount; + remove(old); + setHead(old); + } else { + Node newNode = new Node(userID, userAccount); + if (cache.size() >= capacity) { + System.out.println("# Cache is FULL! Removing " + end.userID + " from cache..."); + cache.remove(end.userID); // remove LRU data from cache. + remove(end); + setHead(newNode); + } else { + setHead(newNode); + } + cache.put(userID, newNode); + } + } + + public boolean contains(String userID) { + return cache.containsKey(userID); + } + + public void invalidate(String userID) { + System.out.println("# " + userID + " has been updated! Removing older version from cache..."); + Node toBeRemoved = cache.get(userID); + remove(toBeRemoved); + cache.remove(userID); + } + + public boolean isFull() { + return cache.size() >= capacity; + } + + public UserAccount getLRUData() { + return end.userAccount; + } + + public void clear() { + head = null; + end = null; + cache.clear(); + } + + /** + * + * Returns cache data in list form. + */ + public ArrayList getCacheDataInListForm() { + ArrayList listOfCacheData = new ArrayList(); + Node temp = head; + while (temp != null) { + listOfCacheData.add(temp.userAccount); + temp = temp.next; + } + return listOfCacheData; + } + + public void setCapacity(int newCapacity) { + if (capacity > newCapacity) { + clear(); // Behavior can be modified to accommodate for decrease in cache size. For now, we'll + // just clear the cache. + } else { + this.capacity = newCapacity; + } + } +} diff --git a/caching/src/main/java/com/iluwatar/caching/UserAccount.java b/caching/src/main/java/com/iluwatar/caching/UserAccount.java new file mode 100644 index 00000000..eff0878a --- /dev/null +++ b/caching/src/main/java/com/iluwatar/caching/UserAccount.java @@ -0,0 +1,47 @@ +package com.iluwatar.caching; + +/** + * + * Entity class (stored in cache and DB) used in the application. + * + */ +public class UserAccount { + private String userID; + private String userName; + private String additionalInfo; + + public UserAccount(String userID, String userName, String additionalInfo) { + this.userID = userID; + this.userName = userName; + this.additionalInfo = additionalInfo; + } + + public String getUserID() { + return userID; + } + + public void setUserID(String userID) { + this.userID = userID; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getAdditionalInfo() { + return additionalInfo; + } + + public void setAdditionalInfo(String additionalInfo) { + this.additionalInfo = additionalInfo; + } + + @Override + public String toString() { + return userID + ", " + userName + ", " + additionalInfo; + } +} diff --git a/caching/src/test/java/com/iluwatar/caching/AppTest.java b/caching/src/test/java/com/iluwatar/caching/AppTest.java new file mode 100644 index 00000000..ce5cddf0 --- /dev/null +++ b/caching/src/test/java/com/iluwatar/caching/AppTest.java @@ -0,0 +1,41 @@ +package com.iluwatar.caching; + +import org.junit.Before; +import org.junit.Test; + +/** + * + * Application test + * + */ +public class AppTest { + App app; + + /** + * Setup of application test includes: initializing DB connection and cache size/capacity. + */ + @Before + public void setUp() { + AppManager.initDB(false); // VirtualDB (instead of MongoDB) was used in running the JUnit tests + // to avoid Maven compilation errors. Set flag to true to run the + // tests with MongoDB (provided that MongoDB is installed and socket + // connection is open). + AppManager.initCacheCapacity(3); + app = new App(); + } + + @Test + public void testReadAndWriteThroughStrategy() { + app.useReadAndWriteThroughStrategy(); + } + + @Test + public void testReadThroughAndWriteAroundStrategy() { + app.useReadThroughAndWriteAroundStrategy(); + } + + @Test + public void testReadThroughAndWriteBehindStrategy() { + app.useReadThroughAndWriteBehindStrategy(); + } +} diff --git a/pom.xml b/pom.xml index 0222d7e3..3462cc86 100644 --- a/pom.xml +++ b/pom.xml @@ -83,6 +83,7 @@ message-channel fluentinterface reactor + caching