Caching can be an efficient way to enhance the performance and responsiveness of your applications by storing frequently accessed data in memory to prevent time-consuming operations, such as database SQL queries or computing complex calculations. There are several caching strategies, and the implementation will depend on the specific needs and constraints of your system. Here are some of the most popular caching strategies:
1. Least Recently Used (LRU):
In an LRU caching scheme, the system removes the least recently used items first. This strategy requires keeping track of what was used when, which is expensive if your cache is large.
Here is a pseudo code example:
class LRUCache:
def __init__(self, capacity):
self.cache = OrderedDict()
self.capacity = capacity
def get(self, key):
if key not in self.cache:
return -1
self.cache.move_to_end(key)
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False)
In this code, the ordered dictionary keeps track of the order of operations (insertion order by default). When a new pair is inserted, it’s added to the end of the cache. If the cache exceeds its capacity, the pair at the front of the cache (i.e., the least recently used one) is evicted.
2. Time to Live (TTL):
TTL is a method where a value (time in seconds) is associated with every cache record. When the record is requested, the application checks if the current time is beyond the stored timestamp+TTL. If it is, the record is not used, and the data source is queried (and the cache record is optionally recreated for future use).
3. Least Frequently Used (LFU):
In this caching scheme, the system removes the least frequently used items first. Particular attention needs to be paid to items used with the same frequency, for which an additional "recency" discriminator might be necessary.
A pseudo code of LFU with recency factor would look something like this:
class LFUCache:
def __init__(self, capacity):
self.capacity = capacity
self.cache = {} # key:val
self.frequency = {} # key: [frequency, recency]
self.recency = 0
def get(self, key):
if key in self.cache:
self.frequency[key][0] += 1
self.frequency[key][1] = self.recency
self.recency += 1
return self.cache[key]
return -1
def put(self, key, val):
if len(self.cache) == self.capacity:
lfu = min(self.frequency.keys(), key=lambda k:self.frequency[k])
del self.cache[lfu]
del self.frequency[lfu]
self.cache[key] = val
self.frequency[key] = [1, self.recency]
self.recency += 1
In the code above, we maintain two maps, one for the cache that maps a key to a value, and another for the frequencies of the keys. The frequency map actually stores a list against each key - the first element being the frequency itself, and the second one being the recency factor (which is the simple increasing time from when elements were accessed or put).
Note: The performance of LFU and LRU as cache eviction policies over others, such as First In, First Out (FIFO), largely depends on the specifics of the workload at hand. In most general cases, though, LFU and LRU tend to perform quite well.