SpringBoot integrates Redis to implement caching (automatic caching + manual aop caching)

Posted by kslagdive on Thu, 28 Oct 2021 08:53:09 +0200

demo address: https://gitee.com/pdh_gitee/redis-cache-demo.git.

Redis must be available locally, which is the premise. All demo s are tested on windows. In the spring boot project, you can usually use the automatic cache policy, or you can use the RedisTemplate class to operate redis, and redis can be configured (of course, this is troublesome, unless there are special business requirements).

When using redis caching: when using @ Cacheable automatic caching, you need to turn off the Configuration information manually cached by RedisTemplate (including the annotation on the caching method, the @ Configuration annotation on the Configuration class, etc.), and vice versa.

1, New SpringBoot project

Create a new SpringBoot project[ Click me to see how to quickly build a SpringBoot project].

1. Dependence

Import redis, swagger3, lombok, mp, web and other dependencies.

<!-- redis use -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--swagger3-->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>3.0.0</version>
</dependency>

<!-- mp -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.3</version>
</dependency>

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

<!-- mysql -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

<!-- lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

2. Configuration file

application.yml

debug: true # Viewing Automatic Configuration

server:
  port: 8082

spring:
  redis:
    host: localhost
    port: 6379

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test
    username: root
    password: # you root password

mybatis-plus:
  mapper-locations: classpath*:com/pdh/mapper/*.xml
  global-config:
    db-config:
      table-prefix:
  configuration:
    # log of sql
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # hump
    map-underscore-to-camel-case: true
    

3.sql and entity classes

Just execute the sql script (stored in the sql package of the demo)

CREATE TABLE `user_db`  (
  `id` int(4) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `user_db` VALUES (1, 'Zhang San');
INSERT INTO `user_db` VALUES (2, 'Li Si');
INSERT INTO `user_db` VALUES (3, 'WangTwo ');
INSERT INTO `user_db` VALUES (4, 'Pockmarks');
INSERT INTO `user_db` VALUES (5, 'Wang San');
INSERT INTO `user_db` VALUES (6, 'Li San');
INSERT INTO `user_db` VALUES (7, 'hh');

User

package com.pdh.entity;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import nonapi.io.github.classgraph.json.Id;
import org.apache.commons.lang3.StringUtils;

import java.io.Serializable;

/**
 *@Author: Peng Dehua
 *@Date: 2021-10-26 11:24
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("user_db")
public class User implements Serializable {
    @Id
    private Integer id;

    private String username;
}

4. Uniformly return Result

All requests return results uniformly

package com.pdh.entity;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.Serializable;

/**
 * @Author: Peng Dehua
 * @Date: 2021-10-26 15:27
 * Unified packaging of results
 */
@Data
@AllArgsConstructor
public class Result implements Serializable {
    
    private boolean success;

    private int code;

    private String msg;

    private Object data;


    /**
     * success Method, identification successful
     * @param data
     * @return
     */
    public static Result success(Object data){
        return new Result(true,200,"success",data);
    }

    /**
     * fail Method, identification failed
     * @param code
     * @param msg
     * @return
     */
    public static Result fail(int code, String msg){
        return new Result(false,code,msg,null);
    }
}

2, Connection test

Test the interface through swagger3. After receiving the request, the interface accesses the persistence layer to obtain the data in MySQL. Access steps: Browser - controller interface - service - mapper - mysql. After accessing the data, return one by one.

1.controller

UserController

@RequestMapping("/user")
@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/get/{id}")
    public User get(@PathVariable("id") Integer id){
        return userService.get(id);
    }

    @PostMapping("/insert")
    public boolean insert(@RequestBody User user){
        return userService.insert(user);
    }

    @DeleteMapping("/delete/{id}")
    public boolean delete(@PathVariable("id") Integer id){
        return userService.delete(id);
    }

}

2.service

UserService

@Service
public class UserService {

    @Resource
    private UserMapper userMapper;

    public User get(Integer id){
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getId,id);
        User user = userMapper.selectOne(wrapper);
        return user;
    }

    public boolean insert(User user){
        int line = userMapper.insert(user);
        if(line > 0)
            return true;
        return false;
    }

    public boolean delete(Integer id){
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getId,id);
        int line = userMapper.delete(wrapper);
        if(line > 0)
            return true;
        return false;
    }

}

3.mapper

UserMapper

package com.pdh.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pdh.entity.User;

/**
 * @Author: Peng Dehua
 * @Date: 2021-10-26 13:17
 */
public interface UserMapper extends BaseMapper<User> {
}

4.swagger3 Startup Test

If the database connection is correct, start the project.

visit: http://localhost:8082/swagger-ui/index.html (port application custom).

Enter the test page, test the get interface, and get the following response (corresponding to the data in the data table, the operation is successful).

3, Redis cache (*)

The biggest benefits of using redis cache are nothing more than two points: improving system response speed and reducing database interaction pressure. Redis data is cached in memory, and the access speed is particularly fast. Take a look at using redis cache and not using redis cache (the following analysis only considers data caching in redis and database):

redis for caching

When accessing a data for the first time (redis will be queried for each access) , if there is no specified data in redis, it will access the database to obtain the data. After returning, it will backfill the data obtained for the first time into redis. When accessing the data again within the expiration time, it will return directly without accessing the database. This strategy is necessary when there are many data requests.

Interact directly with the database

Every visit directly requests the database. When the number of requests is large, the pressure on the database is very large, which directly leads to the slow response of the system.

1.redis cache policy

There are many options for caching strategies. I have used two types:

(1) Use @ EnableCaching+@Cacheable to realize automatic caching, (2) use RedisTemplate to manually cache (annotation + aop).

At the beginning of contacting the cache, the first kind of @ EnableCaching+@Cacheable must be much easier to realize automatic cache. However, Shuai b, who has some contact with program development, knows that the automatically configured redis cache is less flexible and cannot customize his own redis cache requirements.

For manual caching using RedisTemplate (annotation + aop) For example, we have obtained absolute control over the redis cache, and the cache logic is implemented by ourselves. This makes us more flexible in the actual development process, and we can configure a lot of information, such as log output, cache time update, setting different cache expiration times for different methods, custom key format.

Here are two ways to use examples (after analyzing the source code).

2.@Cacheable auto cache

@Cacheable is marked on the method or class to be cached, @ EnableCaching means to enable automatic caching (it can be placed on the startup class and configuration class).

Write the configuration class RedisConfig1. The automatically generated key is in the form of cacheNames::params. However, in order to clearly see which method and parameter value the cached key is, you need to customize the generation form of the key, that is, rewrite the keyGenerator() method of the cacheingconfigurersupport class:

package com.pdh.config;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * @Author: Peng Dehua
 * @Date: 2021-10-27 16:32
 */
@EnableCaching
@Configuration
public class RedisConfig1 extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // Additional information can also be set
        
        return redisTemplate;
    }

    /**
     * Custom key generation policy
     *
     * @return
     */
    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            public Object generate(Object target, Method method, Object... objects) {
                StringBuilder sb = new StringBuilder();
                Cacheable annotation = method.getAnnotation(Cacheable.class);
                String[] cacheNames = annotation.cacheNames();
                for (String elem : cacheNames) {
                    sb.append(elem + ".");
                }
                sb.deleteCharAt(sb.length() - 1); // Delete the last point (.) in cacheNames

                sb.append("::").append(target.getClass()
                        .getSimpleName()).append("::")
                        .append(method.getName()).append("::")
                        .append(Arrays.toString(objects));

                return sb.toString();
            }
        };
    }
}

Then, take the get method of UserController as an example to test the cache:

@GetMapping("/get/{id}")
@Cacheable(cacheNames = {"get"})
public Result get(@PathVariable("id") Integer id){
    return userService.get(id);
}

Then start the project and start the redis service

visit http://localhost:8082/swagger-ui/index.html (the default configuration of swagger is used here), test the get interface, and test the same id twice. Check the console, get it from the database for the first time, and get it directly from redis for the second time.

redis data is shown in the figure below:

The expiration time setting can be configured in application.yml (only all can be set. Different key s and different validity periods need to be set, so another caching policy must be used)

spring:
  # cache setting
  cache:
    redis:
      time-to-live: 60000 # 60s

3.RedisTemplate manual caching

RedisTemplate is manually cached, that is, RedisTemplate is used to implement class operations.

You can directly add cache logic code to the normal business logic. What people criticize is that you need to write cache logic every time you need cache, and the biggest problem is to change the original business logic, which is inappropriate.

Did you learn aop and annotations? Use them. It doesn't matter if you're not impressed[ Click me to quickly learn AOP principle],[Click me to quickly learn the principle of annotation].

With AOP + annotation, it is easy to implement RedisTemplate manual caching. The key point is that the caching strategy can be customized. Let's start. I can't wait!

3.1 Cache annotation and cache logic

Cacahe annotation

package com.pdh.cache;

import java.lang.annotation.*;

/**
 * @Author: Peng Dehua
 * @Date: 2021-10-26 15:24
 * Custom annotation class Cache
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {

    /**
     * Expiration time: 60s by default
     * @return
     */
    long expire() default 1 * 60 * 1000;

    /**
     * Cache ID name
     * @return
     */
    String name() default "";

}

Cache logic

Use aop to capture the annotation marked by @ Cache to realize the surround notification operation (Cache logic). The following is the code I directly copy my project:

package com.pdh.cache;

import com.alibaba.fastjson.JSON;
import com.pdh.entity.Result;
import com.pdh.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.aspectj.lang.annotation.Aspect;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;


/**
 * @Author: Peng Dehua
 * @Date: 2021-10-26 15:27
 */
@Component
@Aspect
@Slf4j
public class CacheAspect {

    @Autowired
    private RedisUtils redisUtils; // json data

    /**
     * aop Tangent point
     * Intercepts methods modified by the specified annotation
     */
    @Pointcut("@annotation(com.pdh.cache.Cache)")
    public void cache() {
    }

    /**
     * Cache operation
     *
     * @param pjp
     * @return
     */
    @Around("cache()")
    public Object toCache(ProceedingJoinPoint pjp) {

        try {
            // Idea: set the storage format and get it

            Signature signature = pjp.getSignature();
            // Class name
            String className = pjp.getTarget().getClass().getSimpleName();
            // Method name
            String methodName = signature.getName();

            // Parameter processing
            Object[] args = pjp.getArgs();
            Class[] parameterTypes = new Class[args.length];
            String params = "";
            for (int i = 0; i < args.length; i++) {
                if (args[i] != null) {
                    parameterTypes[i] = args[i].getClass();
                    params += JSON.toJSONString(args[i]);
                }
            }
            if (StringUtils.isNotEmpty(params)) {
                //Encrypt to prevent the key from being too long and the character escape cannot be obtained
                params = DigestUtils.md5Hex(params);
            }

            // Get the corresponding method in the controller
            Method method = signature.getDeclaringType().getMethod(methodName, parameterTypes);

            // Get Cache annotation
            Cache annotation = method.getAnnotation(Cache.class);
            long expire = annotation.expire();
            String name = annotation.name();

            // Access redis (try to get it first, otherwise access the database)
            String redisKey = name + "::" + className + "::" + methodName + "::" + params;
            String redisValue = redisUtils.get(redisKey);
            if (StringUtils.isNotEmpty(redisValue)) {
                // Not null return data
                Result result = JSON.parseObject(redisValue, Result.class);
                log.info("Data from redis Get from cache,key: {}", redisKey);
                return result; // Jump out method
            }
            Object proceed = pjp.proceed();
            redisUtils.set(redisKey, JSON.toJSONString(proceed), expire, TimeUnit.MILLISECONDS);
            log.info("Data storage redis cache,key: {}", redisKey);
            return proceed;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return Result.fail(999, "System error");
    }

}

3.2 configuration class RedisConfig2

For the expiration time of the key, if a user has a set time, the set time is preferred.

package com.pdh.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @Author: Peng_ Dehua
 * @Date: 2021-10-26 11:15
 *  redis Configuration class (use the default configuration without configuration)
 */
@Configuration
public class RedisConfig2 {

    @Bean
    public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory factory){
        RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<>();

        // Set connection factory class
        redisTemplate.setConnectionFactory(factory);

        // Set k-v serialization mode
        // Jackson2JsonRedisSerializer implements the RedisSerializer interface
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        // You can also set many configurations... (use the default configuration if it is not set)

        return redisTemplate;
    }

}

3.3 Redis tool class writing

For the add, delete and query operations of redis, a unified interface is provided to facilitate management and simplify the complexity of the code. When you need to make different caches for different data, the code is very concise

@Service
public class RedisUtils {
    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    /**
     * Write cache + expiration time
     * @param key
     * @param value
     * @param expireTime
     * @param timeUnit
     * @return
     */
    public boolean set( String key, String value, Long expireTime , TimeUnit timeUnit){
        ValueOperations<String, String> operations = redisTemplate.opsForValue();
        operations.set(key,value);
        redisTemplate.expire(key,expireTime,timeUnit);
        return true;
    }

    /**
     * Get value through key
     * @param key
     * @return
     */
    public String get(String key){
        ValueOperations<String, String> operations = redisTemplate.opsForValue();
        return operations.get(key);
    }

    /**
     * Batch delete k-v
     * @param keys
     * @return
     */
    public boolean remove(final String... keys){
        for(String key : keys){
            if(redisTemplate.hasKey(key)){ //Delete key if it exists
                redisTemplate.delete(key);
            }
        }
        return true;
    }

}

3.4 testing

First, close all the annotations of RedisConfig1 configuration class, and then remove the @ Cacheable annotation on the method in UserController. (if you forget, you may report an error...)

The benefits of using annotation + aop to implement caching have been mentioned above, so you need to mark (annotate) the previously written controller method. Add @ Cache(name = "get method") to get() method:

@GetMapping("/get/{id}")
@Cache(name = "get method")
public Result get(@PathVariable("id") Integer id){
    return userService.get(id);
}

Then start the project, start redis, and access http://localhost:8082/swagger-ui/index.html (default configuration of swagger is used here), test the get interface, and test the same id twice:

The test shows that the data with id=6 is obtained twice. In the sql statement printed by mybatis plus, the sql is executed only once, and the data is obtained from the redis cache the second time (the expiration time is set by us, and the default time also exists).

4, RedisTemplate partial source code

1.redis cache automatic configuration

RedisAutoConfiguration class implements the automatic injection of redisTemplate and stringRedisTemplate, which can be used directly.

**@The ConditionalOnMissingBean annotation indicates that if there is a RedisTemplate object in the Spring container, the automatically configured RedisTemplate will not be instantiated. Therefore, we can directly write a configuration class to configure RedisTemplate** In addition, there are many configuration information that can be configured automatically (such as serialization policy, connection factory, etc.).

In the RedisConfig configuration class written by ourselves, the generic type of redistplate instance object (i.e. K and V in redistemplate < K, V >) does not affect the use of custom specified generic types. When obtaining entity classes from IoC container, such as:

// 1.RedisConfig
RedisTemplate<Integer,Integer> redisTemplate = new RedisTemplate<>();
// 2. Injection
@Autowired
RedisTemplate<String,String> redisTemplate;

In my test, there was no exception, and it did execute the business logic normally. The reason I think is the role of Java type erasure (not sure, just personal)[ Click my quick learning type erase].

2. Data serialization

Why serialize?

Serializable serialization is the process of converting a Java object into a byte stream. In Java, everything is an object, and the state information of an object must be serialized to a form that can be stored or transmitted[ Please move here for details]

How to set serialization policy?

RedisSerializer is the serialization interface of redis data. It provides the following data serialization strategies:

The specific purpose will not be repeated here. When there are business needs, you can directly query the developer manual and select the corresponding serialization strategy.

To set the serialization policy, you need to squeeze out the default RedisTemplate entity class in the IoC container and configure the RedisTemplate yourself (write the RedisConfig configuration class, just like the example used above).

Topics: Redis Spring Spring Boot Cache