preface

For a Java backend developer, when it comes to caching, the first thing that comes to mind is Redis and memcache. This type of cache is sufficient to solve most performance problems, and Java has very mature apis for both. However, we should also remember that both of these are remote cache (distributed cache). The application process and the cached process are usually distributed on different servers, and the different processes communicate with each other through RPC or HTTP. The advantages of this cache are that the cache and application services are decoupled, and the storage of large amounts of data is supported. The disadvantage is that data must be transmitted over the network, resulting in certain performance loss.

The local cache corresponds to the distributed cache. The cached process and the application process are the same, and data reading and writing are completed in the same process. This method has the advantages of no network overhead and fast access. Disadvantages are limited by JVM memory, not suitable for storing big data.

This article focuses on some common scenarios for Java native caching.

Common techniques for local caching

Local caches and applications belong to the same process. Improper use of local caches may affect service stability. Therefore, more factors need to be considered, such as capacity limitation, expiration policy, obsolescence policy, and automatic refresh. Common local caching schemes are:

  • Self-implement local caching according to the HashMap
  • Guava Cache
  • Caffeine
  • Encache

The following are introduced respectively:

1. Customize local cache based on HashMap

The essence of cache is KV data structure stored in memory, corresponding to the HashMap in JDK. However, to implement cache, we also need to consider concurrency security, capacity limit and other policies. The following is a simple way to implement cache using LinkedHashMap:

public class LRUCache extends LinkedHashMap {

    /** * Can re-enter the read/write lock to ensure concurrent read/write security */
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Lock readLock = readWriteLock.readLock();
    private Lock writeLock = readWriteLock.writeLock();

    /**
     * 缓存大小限制
     */
    private int maxSize;

    public LRUCache(int maxSize) {
        super(maxSize + 1.1.0 f.true);
        this.maxSize = maxSize;
    }

    @Override
    public Object get(Object key) {
        readLock.lock();
        try {
            return super.get(key);
        } finally{ readLock.unlock(); }}@Override
    public Object put(Object key, Object value) {
        writeLock.lock();
        try {
            return super.put(key, value);
        } finally{ writeLock.unlock(); }}@Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return this.size() > maxSize; }}Copy the code

LinkedHashMap maintains a linked-list structure to store the insertion order or access order of nodes, and encapsulates some business logic internally to override the removeEldestEntry method to implement the LRU elimination strategy of the cache. In addition, we use read/write locks to ensure the concurrency security of the cache. It is important to note that this example does not support the expiration time obsolescence strategy.

Self-implemented cache is easy to implement and does not require the introduction of third-party packages. Therefore, it is suitable for some simple service scenarios. The disadvantage is that if more features are needed, customized development is required, the cost will be relatively high, and stability and reliability are difficult to guarantee. For complex scenarios, it is recommended to use stable open source tools.

2. Implement local Cache based on Guava Cache

Guava is an open source Java core enhancement library developed by The Google team. It contains collection, concurrent primitives, cache, IO, reflection and other toolkits. It has guaranteed performance and stability and is widely used. Guava Cache supports many features:

  • Maximum capacity limits are supported
  • Support for two expiration deletion strategies (insert time and access time)
  • Supports simple statistics
  • Based on LRU algorithm

Guava Cache is very simple to use, first need to import maven package:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>18.0</version>
</dependency>
Copy the code

A simple example code is as follows:

public class GuavaCacheTest {

    public static void main(String[] args) throws Exception {
        // Create guava cache
        Cache<String, String> loadingCache = CacheBuilder.newBuilder()
                // The initial capacity of the cache
                .initialCapacity(5)
                //cache Indicates the maximum number of caches
                .maximumSize(10)
                // Set write cache to expire in n seconds
                .expireAfterWrite(17, TimeUnit.SECONDS)
                // expireAfterWrite expires after n seconds
                //.expireAfterAccess(17, TimeUnit.SECONDS)
                .build();
        String key = "key";
        // Write data to cache
        loadingCache.put(key, "v");

        If the key does not exist, call the collable method to fetch the value, load it into the key, and return it
        String value = loadingCache.get(key, new Callable<String>() {
            @Override
            public String call(a) throws Exception {
                returngetValueFromDB(key); }});/ / delete key
        loadingCache.invalidate(key);
    }

    private static String getValueFromDB(String key) {
        return "v"; }}Copy the code

In general, Guava Cache is an excellent caching tool that is rich in functionality and thread-safe enough to be used in engineering applications. The above code only introduces general usage. In fact, SpringBoot also supports Guava, which can be easily integrated into the code using configuration files or annotations.

3. Caffeine

Caffeine is a new generation of cache tool based on java8, and its cache performance is close to the theoretical optimal. Caffeine can be regarded as an enhanced version of Guava Cache, which has similar functions. The difference is that Caffeine adopts w-TinylFu, an algorithm combining the advantages of LRU and LFU, which has obvious advantages in performance. The maven package needs to be introduced into Caffeine:

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.5.5</version>
</dependency>
Copy the code

It is similar to Guava Cache in usage:

public class CaffeineCacheTest {

    public static void main(String[] args) throws Exception {
        // Create guava cache
        Cache<String, String> loadingCache = Caffeine.newBuilder()
                // The initial capacity of the cache
                .initialCapacity(5)
                //cache Indicates the maximum number of caches
                .maximumSize(10)
                // Set write cache to expire in n seconds
                .expireAfterWrite(17, TimeUnit.SECONDS)
                // expireAfterWrite expires after n seconds
                //.expireAfterAccess(17, TimeUnit.SECONDS)
                .build();
        String key = "key";
        // Write data to cache
        loadingCache.put(key, "v");

        // Get the value of value. If key does not exist, get value and return
        String value = loadingCache.get(key, CaffeineCacheTest::getValueFromDB);

        / / delete key
        loadingCache.invalidate(key);
    }

    private static String getValueFromDB(String key) {
        return "v"; }}Copy the code

Caffeine has significant advantages over Guava Cache both in terms of functionality and performance. The API is similar to Caffeine’s, making it easy to switch to Caffeine using Guava Cache to save on migration costs. It should be noted that SpringFramework5.0 (SpringBoot2.0) has also abandoned the Guava Cache local caching scheme in favour of Caffeine.

4. Encache

Encache is a pure Java in-process caching framework. It is fast and clean. It is the default CacheProvider in Hibernate. Compared to Caffeine and Guava Cache, Encache is more feature-rich and scalable:

  • Supports a variety of cache elimination algorithms, including LRU, LFU and FIFO
  • The cache supports in-heap storage, out-of-heap storage, and disk storage (supporting persistence)
  • Supports multiple cluster solutions to solve data sharing problems

To use Encache, first import maven package:

<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.8.0</version>
</dependency>
Copy the code

Here’s a simple use case:

public class EncacheTest {

    public static void main(String[] args) throws Exception {
        Declare a cacheBuilder
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
                .withCache("encacheInstance", CacheConfigurationBuilder
                        // Declare an in-heap cache of 20 capacity
                        .newCacheConfigurationBuilder(String.class,String.class, ResourcePoolsBuilder.heap(20)))
                .build(true);
        // Get the Cache instance
        Cache<String,String> myCache =  cacheManager.getCache("encacheInstance", String.class, String.class);
        / / write cache
        myCache.put("key"."v");
        / / read the cache
        String value = myCache.get("key");
        // Remove the roughing
        cacheManager.removeCache("myCache"); cacheManager.close(); }}Copy the code

conclusion

  • In terms of ease of use, Guava Cache, Caffeine and Encache all have mature access solutions that are easy to use.
  • In terms of functionality, While Guava Cache and Caffeine are similar in that they only support in-heap caching, Encache is more feature-rich than Caffeine
  • In terms of performance, Caffeine is the best, GuavaCache is the second, and Encache is the worst.