Caffeine Cache: Principles, High‑Performance Read/Write, and Practical Usage in Java
Caffeine is a high‑performance Java 8 local‑cache library that replaces Guava by using the W‑TinyLFU algorithm with three‑queue LRU structures and lock‑free read/write buffers, offering extensive configuration, dynamic runtime adjustments, and safe back‑source loading with distributed locks to prevent cache‑stampede.
Caffeine is a high‑performance local cache library built on Java 8. It offers near‑optimal hit rates and has become the default cache in Spring 5, replacing Guava Cache.
The official benchmark (linked in the original article) shows that Caffeine outperforms other open‑source local caches in concurrent read, write, and mixed scenarios.
Cache eviction algorithms
Typical in‑process caches use simple structures such as HashMap and need eviction policies because memory is limited. Common policies include FIFO, LRU, and LFU, each with distinct drawbacks.
W‑TinyLFU algorithm
Caffeine adopts the W‑TinyLFU algorithm, which combines a Count‑Min Sketch to reduce memory overhead of frequency counters and a “PK” (victim vs. attacker) mechanism to admit hot entries. The sketch works like a Bloom filter: each key is hashed multiple times, and the minimum counter among the hashed positions is taken as the estimated frequency. When the total frequency reaches a threshold, all counters are halved (reset).
The cache is organized into three queues: WindowDeque (a small LRU for newly accessed items), ProbationDeque and ProtectedDeque (the segmented LRU). Items flow from WindowDeque to ProbationDeque , may be promoted to ProtectedDeque on hits, and eviction is performed by comparing the head (victim) and tail (attacker) of ProbationDeque .
High‑performance read/write
Caffeine treats reads as frequent and writes as occasional. Both read‑side and write‑side updates to frequency information are performed asynchronously.
Read buffer
Each thread writes read events into its own RingBuffer . The ring buffer is a fixed‑size array that enables lock‑free batch processing and minimizes GC pressure, similar to a WAL mechanism in databases.
Write buffer
Write events are stored in a MpscGrowableArrayQueue (multi‑producer, single‑consumer) taken from JCTools, ensuring thread‑safe enqueuing with a single consumer thread.
Configuration parameters
Caffeine’s builder offers many options, such as expireAfterWrite , expireAfterAccess , refreshAfterWrite , maximumSize , weakKeys , softValues , executor , recordStats , removalListener , etc.
Example of creating a cache:
Cache cache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(6, TimeUnit.MINUTES)
.softValues()
.build();Loading cache specifications from a caffeine.properties file and building caches dynamically:
@Component
@Slf4j
public class CaffeineManager {
private final ConcurrentMap
cacheMap = new ConcurrentHashMap<>(16);
@PostConstruct
public void afterPropertiesSet() {
String filePath = CaffeineManager.class.getClassLoader().getResource("").getPath()
+ File.separator + "config" + File.separator + "caffeine.properties";
Resource resource = new FileSystemResource(filePath);
if (!resource.exists()) {
return;
}
Properties props = new Properties();
try (InputStream in = resource.getInputStream()) {
props.load(in);
Enumeration propNames = props.propertyNames();
while (propNames.hasMoreElements()) {
String caffeineKey = (String) propNames.nextElement();
String caffeineSpec = props.getProperty(caffeineKey);
CaffeineSpec spec = CaffeineSpec.parse(caffeineSpec);
Caffeine caffeine = Caffeine.from(spec);
Cache manualCache = caffeine.build();
cacheMap.put(caffeineKey, manualCache);
}
} catch (IOException e) {
log.error("Initialize Caffeine failed.", e);
}
}
}Dynamic adjustments at runtime:
// Adjust maximum size
cache.policy().eviction().ifPresent(eviction -> {
eviction.setMaximum(2 * eviction.getMaximum());
});
// Adjust expiration policies
cache.policy().expireAfterAccess().ifPresent(expiration -> ...);
cache.policy().expireAfterWrite().ifPresent(expiration -> ...);
cache.policy().expireVariably().ifPresent(expiration -> ...);
cache.policy().refreshAfterWrite().ifPresent(expiration -> ...);Retrieving a value with a back‑source function uses Cache.get(key, mappingFunction) , which internally calls ConcurrentHashMap.compute to guarantee that only one thread performs the back‑source operation for a hot key.
caffeineManager.getCache(cacheName)
.get(redisKey, value -> getTFromRedis(redisKey, targetClass, supplier));The back‑source method acquires a distributed lock, reads from Redis, falls back to a supplier (e.g., a MySQL query), writes the result back to Redis, and finally releases the lock.
private
T getTFromRedis(String redisKey, Class targetClass, Supplier supplier) {
String data;
T value;
String redisValue = UUID.randomUUID().toString();
if (tryGetDistributedLockWithRetry(redisKey + RedisKey.DISTRIBUTED_SUFFIX, redisValue, 30)) {
try {
data = getFromRedis(redisKey);
if (StringUtils.isEmpty(data)) {
value = (T) supplier.get();
setToRedis(redisKey, JackSonParser.bean2Json(value));
} else {
value = json2Bean(targetClass, data);
}
} finally {
releaseDistributedLock(redisKey + RedisKey.DISTRIBUTED_SUFFIX, redisValue);
}
} else {
value = json2Bean(targetClass, getFromRedis(redisKey));
}
return value;
}In distributed deployments, a distributed lock prevents cache‑stampede when Redis misses.
Conclusion
Caffeine is currently one of the best local‑cache solutions. By leveraging the W‑TinyLFU algorithm it achieves high hit rates with low memory consumption. Users familiar with Guava Cache can adopt it with minimal changes, and the article provides concrete migration steps from Ehcache.
vivo Internet Technology
Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.