The introduction

Two days ago, I was writing a real-time data processing project, and the project requirement was to process 1K of data in 1s. At this time, it was obviously not enough to just check the database. When selecting the technology, the boss told me to use layering-Cache, an open source project, as the Cache framework.

Between asked the side of the small partner, it seems that this understanding is not much. Redis is generally used to cache, should be rarely used multi-level cache framework to specifically manage the cache.

I took the opportunity to learn more about caching techniques in SpringBoot, and this article came about!


When the performance requirements of the project are high, the database access cannot be relied on alone to obtain data, and caching technology must be introduced.

Local cache and Redis cache are commonly used.

  • Local cache: also known as memory, fast, but not persistent, once the project is closed, data will be lost. Moreover, it cannot meet the application scenarios of distributed systems (such as data inconsistency).
  • Redis cache: using databases, etc. The most common is Redis. Redis is also fast to access, allowing you to set expiration times and persistence methods. The disadvantage is that it is affected by network and concurrent access.

This section introduces three caching technologies: Spring Cache, Layering Cache framework, and Alibaba JetCache framework. The example uses SpringBoot version 2.1.3.release. For non-Springboot projects, refer to the documentation address provided in this article.

Project source code address: github.com/laolunsi/sp…


A, Spring Cache

Spring Cache is the native Cache solution of Spring. It is easy to use. You can use local Cache or Redis

CacheType include:

GENERIC, JCACHE, EHCACHE, HAZELCAST, INFINISPAN, COUCHBASE, REDIS, CAFFEINE, SIMPLE, NONE

Spring-boot-starter-web is a web project, and the spring-boot-starter-web is a web project.

The spring-boot-starter-data-redis dependency is added to the spring-boot-starter-data-redis dependency:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<! --Redis-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Copy the code

Add the @enablecaching annotation to the configuration class or Application class to EnableCaching.

The configuration file is concise (and has few features) :

server:
  port: 8081
  servlet:
    context-path: /api
spring:
  cache:
    type: redis
  redis:
    host: 127.0. 01.
    port: 6379
    database: 1
Copy the code

Next, we write a Controller to add, delete, change and check the User, and realize the three operations of save/delete/findAll for the User. For demonstration purposes, the DAO layer does not access the database, but instead uses a HashMap to directly simulate database operations.

Let’s look directly at the service layer interface implementation:

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDAO userDAO;

    @Override
    @Cacheable(value = "user", key = "#userId")
    public User findById(Integer userId) {
        return userDAO.findById(userId);
    }

    @Override
    @CachePut(value = "user", key = "#user.id", condition = "#user.id ! = null")
    public User save(User user) {
        user.setUpdateTime(new Date());
        userDAO.save(user);
        return userDAO.findById(user.getId());
    }

    @Override
    @CacheEvict(value = "user", key = "#userId")
    public boolean deleteById(Integer userId) {
        return userDAO.deleteById(userId);
    }

    @Override
    public List<User> findAll(a) {
        returnuserDAO.findAll(); }}Copy the code

We can see the @cacheable, @cacheput, @cacheevict annotations used.

  • Cacheable: Enables caching, where data is first looked up from the cache and, if present, read from the cache; If not, the method is executed and the method return value is added to the cache
  • @cachePUT: Updates the cache, adding the method return value to the cache if condition evaluates to true
  • @cacheevict: Deletes cached data by calculating the cache address based on the value and key fields

The default object stored in Redis is binary. This can be modified by modifying the serialization rules in RedisCacheConfiguration. Such as:

@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(a){
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = newJackson2JsonRedisSerializer<>(Object.class); RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig(); configuration = configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer )).entryTtl(Duration.ofDays(30));
        returnconfiguration; }}Copy the code

Spring Cache has relatively simple functions, such as Cache refresh and second-level Cache. Here is an open source project: Layering-cache, which implements Cache flush, two levels of Cache (one level of memory, two levels of Redis). It is also easier to extend the implementation to its own cache framework.

2. Layering Cache framework

Documents: github.com/xiaolyuh/la…

Introducing dependencies:

 <dependency>
 		<groupId>com.github.xiaolyuh</groupId>
 		<artifactId>layering-cache-starter</artifactId>
 		<version>2.0.7</version>
 </dependency>
Copy the code

Configuration files do not need to be modified. The startup class is still annotated @enablecaching.

Then you need to configure the RedisTemplate:

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return createRedisTemplate(redisConnectionFactory);
    }

    public RedisTemplate createRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        / / use Jackson2JsonRedisSerialize replace the default serialization
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // Set the serialization rules for value and key
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        //Map
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        returnredisTemplate; }}Copy the code

Let’s replace the Spring Cache default annotation with @cacheable @cacheput @catchevict annotations in the Layering package.

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDAO userDAO;

    @Override
    //@Cacheable(value = "user", key = "#userId")
    @Cacheable(value = "user", key = "#userId",
        firstCache = @FirstCache(expireTime = 5, timeUnit = TimeUnit.MINUTES),
        secondaryCache = @SecondaryCache(expireTime = 10, preloadTime = 3, forceRefresh = true, isAllowNullValue = true, timeUnit = TimeUnit.MINUTES))
    public User findById(Integer userId) {
        return userDAO.findById(userId);
    }

    @Override
    //@CachePut(value = "user", key = "#user.id", condition = "#user.id ! = null")
    @CachePut(value = "user", key = "#user.id",
            firstCache = @FirstCache(expireTime = 5, timeUnit = TimeUnit.MINUTES),
            secondaryCache = @SecondaryCache(expireTime = 10, preloadTime = 3, forceRefresh = true, isAllowNullValue = true, timeUnit = TimeUnit.MINUTES))
    public User save(User user) {
        user.setUpdateTime(new Date());
        userDAO.save(user);
        return userDAO.findById(user.getId());
    }

    @Override
    //@CacheEvict(value = "user", key = "#userId")
    @CacheEvict(value = "user", key = "#userId")
    public boolean deleteById(Integer userId) {
        return userDAO.deleteById(userId);
    }

    @Override
    public List<User> findAll(a) {
        returnuserDAO.findAll(); }}Copy the code

Third, Alibaba JetCache framework

Documents: github.com/alibaba/jet…

JetCache is a Java-based caching system package that provides a unified API and annotations to simplify the use of caches. JetCache provides more powerful annotations than SpringCache, with native support for TTL, two-level caching, distributed auto-refresh, and a Cache interface for manual caching. Currently there are four implementations, RedisCache, TairCache (not available on Github), CaffeineCache(in Memory) and a simple LinkedHashMapCache(in memory). It is also very easy to add new implementations.

All features:

  • Access the Cache system through a unified API
  • Declarative method caching with annotations, support for TTL and two-level caching
  • Created and configured by annotationsCacheThe instance
  • For allCacheAutomatic statistics for instance and method caches
  • The Key generation strategy and the Value serialization strategy are configurable
  • Distributed cache automatic refresh, distributed lock (2.2+)
  • Asynchronous Cache API (2.2+, when using Redis’s lettuce client)
  • Spring Boot support

In the SpringBoot project, we introduce the following dependencies:

<dependency>
	<groupId>com.alicp.jetcache</groupId>
	<artifactId>jetcache-starter-redis</artifactId>
	<version>2.5.14</version>
</dependency>
Copy the code

Configuration:

server:
  port: 8083
  servlet:
    context-path: /api

jetcache:
  statIntervalMinutes: 15
  areaInCacheName: false
  local:
    default:
      type: caffeine
      keyConvertor: fastjson
  remote:
    default:
      expireAfterWriteInMillis: 86400000 The default timeout is set to 24 hours in milliseconds
      type: redis
      keyConvertor: fastjson
      valueEncoder: java #jsonValueEncoder #java
      valueDecoder: java #jsonValueDecoder
      poolConfig:
        minIdle: 5
        maxIdle: 20
        maxTotal: 50
      host: ${redis.host}
      port: ${redis.port}
      database: 1

redis:
  host: 127.0. 01.
  port: 6379

Copy the code

Application.class

@EnableMethodCache(basePackages = "com.example.springcachealibaba")
@EnableCreateCacheAnnotation
@SpringBootApplication
public class SpringCacheAlibabaApplication {

    public static void main(String[] args) { SpringApplication.run(SpringCacheAlibabaApplication.class, args); }}Copy the code

Words such as its meaning, @ EnableMethodCache used to annotate the Cache on the opening method function, @ EnableCreateCacheAnnotation for annotations open @ CreateCache to introduce Cache the Bean’s function. Both sets can be enabled at the same time.

Here is an example of the above User add, delete, change and check function:


3.1 Creating a Cache Instance using @createcache

@Service
public class UserServiceImpl implements UserService {

    // The following example is an example of creating a Cache object to Cache data using the @createcache annotation

    @CreateCache(name = "user:", expire = 5, timeUnit = TimeUnit.MINUTES)
    private Cache<Integer, User> userCache;

    @Autowired
    private UserDAO userDAO;

    @Override
    public User findById(Integer userId) {
        User user = userCache.get(userId);
        if (user == null || user.getId() == null) {
            user = userDAO.findById(userId);
        }
        return user;
    }

    @Override
    public User save(User user) {
        user.setUpdateTime(new Date());
        userDAO.save(user);
        user = userDAO.findById(user.getId());

        // cache
        userCache.put(user.getId(), user);
        return user;
    }

    @Override
    public boolean deleteById(Integer userId) {
        userCache.remove(userId);
        return userDAO.deleteById(userId);
    }

    @Override
    public List<User> findAll(a) {
        returnuserDAO.findAll(); }}Copy the code

3.2 Implement method caching through annotations

@Service
public class UserServiceImpl implements UserService {

    // The following is an example of using AOP to cache data

    @Autowired
    private UserDAO userDAO;

    @Autowired
    private UserService userService;

    @Override
    @Cached(name = "user:", key = "#userId", expire = 1000)
    //@Cached( name = "user:", key = "#userId", serialPolicy = "bean:jsonPolicy")
    public User findById(Integer userId) {
        System.out.println("userId: " + userId);
        return userDAO.findById(userId);
    }

    @Override
    @CacheUpdate(name = "user:", key = "#user.id", value = "#user")
    public User save(User user) {
        user.setUpdateTime(new Date());
        boolean res = userDAO.save(user);
        if (res) {
            return userService.findById(user.getId());
        }
        return null;
    }

    @Override
    @CacheInvalidate(name = "user:", key = "#userId")
    public boolean deleteById(Integer userId) {
        return userDAO.deleteById(userId);
    }

    @Override
    public List<User> findAll(a) {
        returnuserDAO.findAll(); }}Copy the code

There are three annotations: @cached / @cacheUpdate / @cacheinVALIDATE, which correspond to @cacheable / @cacheput / @cacheevict in Spring Cache

For details, see: github.com/alibaba/jet…

3.3 Custom serializers

The default value storage format is binary. JetCache provides only Java and Kryo serializers for Redis key and value. You can customize the serializer to achieve the desired serialization, such as JSON.

The JetCache developers propose:

Jetcache had three serializers in previous versions: Java, Kryo, and FastJSON. However, Fastjson serialization compatibility is not very good, and after one upgrade, the unit test failed, afraid that people will feel a hole after using it, so they will scrap it. The default serializer is now the worst performing, most compatible, and most familiar Java serializer.

Following the advice in the ORIGINAL repository FAQ, you can define your own serializers in two ways.

3.3.1 Implement the SerialPolicy interface

The first is to define an implementation class for SerialPolicy, register it as a bean, and specify the bean:name in the SerialPolicy property in @cached

Such as:

import com.alibaba.fastjson.JSONObject;
import com.alicp.jetcache.CacheValueHolder;
import com.alicp.jetcache.anno.SerialPolicy;

import java.util.function.Function;

public class JsonSerialPolicy implements SerialPolicy {

    @Override
    public Function<Object, byte[]> encoder() {
        return  o -> {
            if(o ! =null) {
                CacheValueHolder cacheValueHolder = (CacheValueHolder) o;
                Object realObj = cacheValueHolder.getValue();
                String objClassName = realObj.getClass().getName();
                // To prevent exceptions where Value cannot be forcibly cast to an object of the specified type, a JsonCacheObject is generated that holds the type of the target object (such as User)
                JsonCacheObject jsonCacheObject = new JsonCacheObject(objClassName, realObj);
                cacheValueHolder.setValue(jsonCacheObject);
                return JSONObject.toJSONString(cacheValueHolder).getBytes();
            }
            return new byte[0];
        };
    }

    @Override
    public Function<byte[], Object> decoder() {
        return bytes -> {
            if(bytes ! =null) {
                String str = new String(bytes);
                CacheValueHolder cacheValueHolder = JSONObject.parseObject(str, CacheValueHolder.class);
                JSONObject jsonObject = JSONObject.parseObject(str);
                // First parse out the JsonCacheObject and then get the realObj and its type
                JSONObject jsonOfMy = jsonObject.getJSONObject("value");
                if(jsonOfMy ! =null) {
                    JSONObject realObjOfJson = jsonOfMy.getJSONObject("realObj");
                    String className = jsonOfMy.getString("className");
                    try {
                        Object realObj = realObjOfJson.toJavaObject(Class.forName(className));
                        cacheValueHolder.setValue(realObj);
                    } catch(ClassNotFoundException e) { e.printStackTrace(); }}return cacheValueHolder;
            }
            return null; }; }}Copy the code

Note that in the JetCache source, we see a CacheValueHolder for the actual cached object, which includes a generic field V, which is the actual cached data. To convert JSON strings to and from CacheValueHolder (including the generic field V), I use CacheValueHolder and a custom JsonCacheObject class in my conversion, which looks like this:

public class JsonCacheObject<V> {

    private String className;
    private V realObj;

    public JsonCacheObject(a) {}public JsonCacheObject(String className, V realObj) {
        this.className = className;
        this.realObj = realObj;
    }

    // ignore get and set methods
}
Copy the code

Then define a configuration class:

@Configuration
public class JetCacheConfig {
    @Bean(name = "jsonPolicy")
    public JsonSerializerPolicy jsonSerializerPolicy(a) {
        return newJsonSerializerPolicy(); }}Copy the code

It’s easy to use, for example:

@Cached( name = "user:", key = "#userId", serialPolicy = "bean:jsonPolicy")
Copy the code

This serialization method is local and only works for a single cache.

Here’s how to serialize the global method.


3.3.2 Configuring SpringConfigProvider globally

JetCache provides two serialization rules by default: KRYO and JAVA (case insensitive).

Here we define a new SpringConfigProvider based on the JSONSerialPolicy above:

@Configuration
public class JetCacheConfig {

    @Bean
    public SpringConfigProvider springConfigProvider(a) {
        return new SpringConfigProvider() {
            @Override
            public Function<byte[], Object> parseValueDecoder(String valueDecoder) {
                if (valueDecoder.equalsIgnoreCase("myJson")) {
                    return new JsonSerialPolicy().decoder();
                }
                return super.parseValueDecoder(valueDecoder);
            }

            @Override
            public Function<Object, byte[]> parseValueEncoder(String valueEncoder) {
                if (valueEncoder.equalsIgnoreCase("myJson")) {
                    return new JsonSerialPolicy().encoder();
                }
                return super.parseValueEncoder(valueEncoder); }}; }}Copy the code

Here we use the type myJson as the name of the new serialization type, So we can in the configuration file jetcache. XXX. ValueEncoder and jetcache. XXX. ValueDecoder setting values on the two configuration items myJson/Java/kryo one of the three.

That’s it for the caching framework in Java, along with some more in-depth knowledge about how to ensure consistency of cached data in a distributed environment, refresh cached data, and customize cache policies for multi-level caching. These are all for later study and introduction!


References:

  • Spring Cache: docs. Spring. IO/Spring/docs…

  • Caffeine cache: www.jianshu.com/p/9a80c662d…

  • Layering-Cache:github.com/xiaolyuh/la…

  • Alibaba JetCache: github.com/alibaba/jet…

  • JetCache FAQ: github.com/alibaba/jet…