所有分类
  • 所有分类
  • 未分类

SpringBoot缓存–@Cacheable的用法

简介

说明

本文用示例介绍SpringBoot的缓存注解@Cacheable的用法。

本文重点展示@Cacheable的配置及其基础用法,详细用法见:SpringBoot缓存-注解的用法 – 自学精灵

示例介绍

需求:给分页接口加缓存,且设置其过期时间。

第1次访问时,真实请求,执行成功后@Cacheable注解会将结果缓存到Redis。

之后访问时,先从缓存中取,若缓存中有则直接从缓存中取,不再执行方法内的逻辑。

过期时间统一在配置类中设置,里边设置部分key的过期时间,其余的用默认的过期时间。

实例

配置及依赖

application.yml

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    # password:
    # database: 0     #指定数据库,默认为0
    # timeout: 3000   #连接超时时间,单位毫秒,默认为0。也可以这么写:3s
    # ssl: false      # 是否启用SSL连接,默认false
    # pool: #连接池配置
    #   max-active: 8 #最大活跃连接数,默认8个。
    #   max-idle: 8   #最大空闲连接数,默认8个。
    #   max-wait: -1  #获取连接的最大等待时间,默认-1,表示无限制,单位毫秒。
    #                 #默认值可能会因为获取不到连接,导致事务无法提交,数据库被锁,大量线程处于等待状态的情况。
    #   min-idle: 0   #最小空闲连接数,默认0。
    # sentinel:
    #   master: myMaster             #哨兵master
    #   nodes: host1:port,host2:port #哨兵节点
    # cluster:
    #   max-redirects:               # 集群模式下,集群最大转发的数量
    #   nodes: host1:port,host2:port # 集群节点

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo_Cacheable_SpringBoot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo_Cacheable_SpringBoot</name>
    <description>demo_Cacheable_SpringBoot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

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

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Redis配置

 常量

package com.example.demo.constant;

public class CachingConstant {
    /**
     * @Cacheable的cacheNames属性使用的值
     */
    public interface CacheNames {
        String CACHE_30_SECOND = "redis_cache_30_second";
        String CACHE_1_MINUTE = "redis_cache_1_minute";
        String CACHE_5_MINUTE = "redis_cache_5_minute";
        String CACHE_10_MINUTE = "redis_cache_10_minute";
        String CACHE_30_MINUTE = "redis_cache_30_minute";
        String CACHE_1_HOUR = "redis_cache_1_hour";
        String CACHE_2_HOUR = "redis_cache_2_hour";
        String CACHE_6_HOUR = "redis_cache_6_hour";
        String CACHE_12_HOUR = "redis_cache_12_hour";
        String CACHE_1_DAY = "redis_cache_1_day";
        String CACHE_15_DAY = "redis_cache_15_day";
        String CACHE_30_DAY = "redis_cache_30_day";
        String CACHE_60_DAY = "redis_cache_60_day";
        String CACHE_180_DAY = "redis_cache_180_day";
        String CACHE_365_DAY = "redis_cache_365_day";
    }
}

配置

package com.example.demo.config;

import com.example.demo.constant.CachingConstant;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
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.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;
import org.springframework.util.StringUtils;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableCaching
public class RedisCachingConfig extends CachingConfigurerSupport {

    /**
     * 重写缓存Key生成策略。
     * 包名+方法名+参数列表。防止缓存Key冲突
     */
    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return (target, method, params) -> {
            // 存放最终结果
            StringBuilder resultStringBuilder = new StringBuilder("cache:key:");
            // 执行方法所在的类
            resultStringBuilder.append(target.getClass().getName()).append(".");
            // 执行的方法名称
            resultStringBuilder.append(method.getName()).append("(");

            // 存放参数
            StringBuilder paramStringBuilder = new StringBuilder();
            for (Object param : params) {
                if (param == null) {
                    paramStringBuilder.append("java.lang.Object[null],");
                } else {
                    paramStringBuilder
                            .append(param.getClass().getName())
                            .append("[")
                            .append(String.valueOf(param))
                            .append("],");
                }
            }
            if (StringUtils.hasText(paramStringBuilder.toString())) {
                // 去掉最后的逗号
                String trimLastComma = paramStringBuilder.substring(0, paramStringBuilder.length() - 1);
                resultStringBuilder.append(trimLastComma);
            }

            return resultStringBuilder.append(")").toString();
        };
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        Map<String, RedisCacheConfiguration> configurationMap = new HashMap<>();
        configurationMap.put(RedisConstant.CacheNames.CACHE_30_SECOND, createCacheConfig(Duration.ofSeconds(30)));
        configurationMap.put(RedisConstant.CacheNames.CACHE_1_MINUTE, createCacheConfig(Duration.ofMinutes(1)));
        configurationMap.put(RedisConstant.CacheNames.CACHE_5_MINUTE, createCacheConfig(Duration.ofMinutes(5)));
        configurationMap.put(RedisConstant.CacheNames.CACHE_10_MINUTE, createCacheConfig(Duration.ofMinutes(10)));
        configurationMap.put(RedisConstant.CacheNames.CACHE_30_MINUTE, createCacheConfig(Duration.ofMinutes(30)));
        configurationMap.put(RedisConstant.CacheNames.CACHE_1_HOUR, createCacheConfig(Duration.ofHours(1)));
        configurationMap.put(RedisConstant.CacheNames.CACHE_2_HOUR, createCacheConfig(Duration.ofHours(2)));
        configurationMap.put(RedisConstant.CacheNames.CACHE_6_HOUR, createCacheConfig(Duration.ofHours(6)));
        configurationMap.put(RedisConstant.CacheNames.CACHE_12_HOUR, createCacheConfig(Duration.ofHours(12)));
        configurationMap.put(RedisConstant.CacheNames.CACHE_1_DAY, createCacheConfig(Duration.ofDays(1)));
        configurationMap.put(RedisConstant.CacheNames.CACHE_15_DAY, createCacheConfig(Duration.ofDays(15)));
        configurationMap.put(RedisConstant.CacheNames.CACHE_30_DAY, createCacheConfig(Duration.ofDays(30)));
        configurationMap.put(RedisConstant.CacheNames.CACHE_60_DAY, createCacheConfig(Duration.ofDays(60)));
        configurationMap.put(RedisConstant.CacheNames.CACHE_180_DAY, createCacheConfig(Duration.ofDays(180)));
        configurationMap.put(RedisConstant.CacheNames.CACHE_365_DAY, createCacheConfig(Duration.ofDays(365)));

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofDays(7))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
                .disableCachingNullValues();

        return RedisCacheManager.builder(factory)
                .initialCacheNames(configurationMap.keySet())
                .withInitialCacheConfigurations(configurationMap)
                // 如果key不在configurationMap中,则使用此配置
                .cacheDefaults(config)
                .build();
    }

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

        template.setKeySerializer(keySerializer());
        template.setValueSerializer(valueSerializer());

        template.setHashKeySerializer(keySerializer());
        template.setHashValueSerializer(valueSerializer());

        template.afterPropertiesSet();
        return template;
    }

    private RedisSerializer<String> keySerializer() {
        return new StringRedisSerializer();
    }

    private RedisSerializer<Object> valueSerializer() {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
                new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

        // 此项必须配置,否则如果序列化的对象里边还有对象,会报如下错误:
        //     java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX
        objectMapper.activateDefaultTyping(
                objectMapper.getPolymorphicTypeValidator(),
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);
        // 旧版写法:
        // objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        return jackson2JsonRedisSerializer;
    }

    private RedisCacheConfiguration createCacheConfig(Duration ttl) {
        return RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(ttl)
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()));
    }
}

业务代码

Controller

package com.example.demo.controller;

import com.example.demo.entity.Result;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

    @GetMapping("page")
    public Result page(int pageNo, int pageSize) {
        return userService.page(pageNo, pageSize);
    }
}

Service

接口

package com.example.demo.service;

import com.example.demo.entity.Result;

public interface UserService {
    Result page(int pageNo, int pageSize);
}

实现

package com.example.demo.service.impl;

import com.example.demo.constant.RedisConstant;
import com.example.demo.entity.Result;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Service
public class UserServiceImpl implements UserService {
    private final List<User> allUsers = Arrays.asList(
            new User(1L, "Tony1", 20),
            new User(2L, "Tony2", 18),
            new User(3L, "Tony3", 30),
            new User(4L, "Tony4", 25),
            new User(5L, "Tony5", 28)
    );

    @Override
    @Cacheable(cacheNames = CachingConstant.CacheNames.CACHE_5_MINUTE)
    public Result<List<User>> page(int pageNo, int pageSize) {
        String format = String.format("pageNo: %s, pageSize: %s", pageNo, pageSize);
        System.out.println("从数据库中读数据。" + format);

        int from = (pageNo - 1) * pageSize;
        int to = Math.min(allUsers.size(), (pageNo) * pageSize);

        List<User> users = new ArrayList<>(allUsers.subList(from, to));
        return new Result<List<User>>().data(users);
    }
}

Entity

User

package com.example.demo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
// 必须要有无参构造函数。因为Redis反序列化为对象时要用到
@NoArgsConstructor
public class User {
    private Long id;

    private String userName;

    private Integer age;
}

Result

package com.example.demo.entity;

import lombok.Data;
 
@Data
public class Result<T> {
    private boolean success = true;
 
    private int code = 1000;
 
    private String message;
 
    private T data;
 
    public Result() {
    }
 
    public Result(boolean success) {
        this.success = success;
    }
 
    public Result<T> success(boolean success) {
        Result<T> result = new Result<>(success);
        if (success) {
            result.code = 1000;
        } else {
            result.code = 1001;
        }
        return result;
    }
 
    public Result<T> success() {
        return success(true);
    }
 
    public Result<T> failure() {
        return success(false);
    }
 
    /**
     * @param code {@link ResultCode#getCode()}
     */
    public Result<T> code(int code) {
        this.code = code;
        return this;
    }
 
    public Result<T> message(String message) {
        this.message = message;
        return this;
    }
 
    public Result<T> data(T data) {
        this.data = data;
        return this;
    }
}

测试

第1次请求

http://localhost:8080/user/page?pageNo=1&pageSize=2

postman结果:

后端结果:

从数据库中读数据。pageNo: 1, pageSize: 2

Redis结果:

第2次请求(重复第1次)

访问:http://localhost:8080/user/page?pageNo=1&pageSize=2

postman结果:

后端结果:无输出 

Redis结果:(不会更新TTL)

第3次请求(使用新参数)

访问:http://localhost:8080/user/page?pageNo=2&pageSize=2

postman结果:

后端结果:

从数据库中读数据。pageNo: 2, pageSize: 2

Redis结果:(方法返回值存入Redis,与其他的key的超时时间是分开的) 

本次请求的结果:

之前请求的结果:

0

评论0

请先

显示验证码
没有账号?注册  忘记密码?

社交账号快速登录