Home | 简体中文 | 繁体中文 | 杂文 | Github | 知乎专栏 | Facebook | Linkedin | Youtube | 打赏(Donations) | About
知乎专栏

26.32. Spring boot with Caching

https://docs.spring.io/spring-boot/docs/current/reference/html/io.html#io.caching.provider.redis

maven

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

Redis

使用 Redis

        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>        
        
			
    	
spring.data.redis.host=cch.netkiller.cn
spring.data.redis.port=6379
spring.data.redis.password=passw0rd
spring.data.redis.database=10
spring.data.redis.timeout=30000
spring.data.redis.lettuce.pool.max-active=32
spring.data.redis.lettuce.pool.max-wait=-1
spring.data.redis.lettuce.pool.max-idle=8
spring.data.redis.lettuce.pool.min-idle=2
spring.cache.type=redis
spring.cache.redis.key-prefix=spring:
spring.cache.redis.use-key-prefix=true
spring.cache.redis.time-to-live=604800000
spring.cache.redis.cache-null-values=false
    	
			

启用缓存 @EnableCaching

添加 @EnableCaching

        
package hello;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class Application {

	public static void main(String[] args) {
	    SpringApplication.run(Application.class, args);
	}

}			
        
		

设置缓存 @Cacheable

        
@Cacheable(value="users", key="#id")
public User find(Integer id) {

	return null;

}			
        
		

多参数处理

			
    @Cacheable(value = "picture:click", key = "#deviceId+'-'+#pictureId", unless = "#result == null")
    public AigcResponse click(Integer deviceId, Integer pictureId) {
        Optional<PictureClick> optional = pictureClickRepository.findByDeviceIdAndPictureId(deviceId, pictureId);
        return optional.map(AigcResponse::new).orElseGet(() -> new AigcResponse(false, AigcResponse.Code.DataNotFoundException, "数据不存在", null));
    }			
			
			

对象参数

引用对象

        
@Cacheable(value="users", key="#user.id")
public User find(User user) {

return null;

}			
        
			

条件判断

        
@Cacheable(value="messagecache", key="#id", condition="id < 10")
public String getMessage(int id){

return "hello"+id;

}

@Cacheable(value="test",condition="#userName.length()>2")
@Cacheable(value={"users"}, key="#user.id", condition="#user.id%2==0")
        
			

参数索引

#p0 参数索引,p0表示第一个参数

        
@Cacheable(value="users", key="#p0")
public User find(Integer id) {

return null;

}

@Cacheable(value="users", key="#p0.id")
public User find(User user) {

return null;

}
        
			

自动生成 key

@Cacheable 如果没有任何参数将会自动生成 key ,前提是必须设置 @CacheConfig(cacheNames = "test")

        
@GetMapping("/cache/auto")
@Cacheable()
public Attribute auto() {
    Attribute attribute = new Attribute();
    attribute.setName("sdfsdf");
    return attribute;
}			
        
			
        
127.0.0.1:6379> keys *
1) "test::SimpleKey []"			
        
			

SpEL表达式

        
@GetMapping("/cache/expire")
@Cacheable("test1#${select.cache.timeout:1000}")
public String expire() {
    return "Test";
}

@GetMapping("/cache/expire")
@Cacheable("test1#${select.cache.timeout:1000}#${select.cache.refresh:600}")
public String expire() {
    return "Test";
}
        
			

排除 null 结果

使用 unless 排除 null 结果

			
    @Cacheable(value = "translate", key = "#chinese", unless="#result == null")
    public String translate(String chinese) {
    }			
			
			

通过配置文件设置spring.cache.redis.cache-null-values

			
spring.cache.redis.cache-null-values=false			
			
			

排除 empty

List 不会返回 null,怎么不缓存空的 list 呢?使用 unless="#result.empty" 可以实现

			
@Cacheable(unless="#result.empty")
public List<Object> getList() {
  // method implementation
}			
			
			

更新缓存 @CachePut

@CachePut 每次都会执行方法,都会将结果存入指定key的缓存中,@CachePut 不会判断是否 key 已经存在,二是始终覆盖。

        
@CachePut("users")
public User find(Integer id) {

return null;

}
        
		

删除缓存 @CacheEvict

缓存返回结果

            
@Cacheable("cacheable")
@RequestMapping("/test/cacheable")
@ResponseBody
public String cacheable() {
    Date date = new Date();
    String message = date.toString();
    return message;
}
            
		

5秒钟清楚一次缓存

            
@Scheduled(fixedDelay = 5000)
@CacheEvict(allEntries = true, value = "cacheable")	
public void expire() {
    Date date = new Date();
    String message = date.toString();
    System.out.println(message);
}
            
		

组合操作 @Caching

    	
    @Caching(
	    cacheable = {
	            @Cacheable(value = "emp",key = "#p0"),
	            ...
	    },
	    put = {
	            @CachePut(value = "emp",key = "#p0"),
	            ...
	    },
	    evict = {
	            @CacheEvict(value = "emp",key = "#p0"),
	            ....
	    }
    )
    public User save(User user) {
        ....
    }
    	
    	
		
    	
    @Caching(
            evict = {
                    @CacheEvict(value = "picture:click", key = "#deviceId+'-'+#pictureId"),
                    @CacheEvict(cacheNames = "picture:share:previous", key = "#pictureId"),
                    @CacheEvict(cacheNames = "picture:share:next", key = "#pictureId")
            }
    )
    	
		

序列化

		
package cn.netkiller.config;
import java.time.Duration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

@Configuration
public class CacheConfiguration {
	@Bean    
	public RedisCacheConfiguration cacheConfiguration() {
		return RedisCacheConfiguration                
			.defaultCacheConfig()                
			.entryTtl(Duration.ofMinutes(10)) // 全局设置,统一TTL时间
			.disableCachingNullValues()                
			.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
	}
}		
		
		

解决Expire 和 TTL 过期时间

分别配置 products 和 users key 的过期时间

		
package cn.netkiller.config;
import java.time.Duration;
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.RedisCacheManagerBuilder;
import org.springframework.data.redis.cache.RedisCacheManagerBuilderCustomizer;

@Configuration
public class CacheConfiguration {
    @Bean
    public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
        return (RedisCacheManagerBuilder builder) -> builder
            .withCacheConfiguration("products", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(5)))
			.withCacheConfiguration("users", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(1))
		);    
	}
}		
		
		

Springboot 1.x

        
@Bean
public CacheManager cacheManager(RedisTemplate redisTemplate) {
    RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
    cacheManager.setDefaultExpiration(60);	//缓存默认 60 秒
    Map<String, Long> expiresMap = new HashMap<>();
    expiresMap.put("Product", 5L);  //设置 key = Product 时 5秒缓存。你可以添加很多规则。 
    cacheManager.setExpires(expiresMap);
    return cacheManager;
}			
        
		

Springboot 2.x

        
package api.config;

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

import org.springframework.cache.CacheManager;
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.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;

@Configuration
public class CachingConfigurer {

public CachingConfigurer() {
    // TODO Auto-generated constructor stub
}

@Bean
public KeyGenerator simpleKeyGenerator() {
    return (o, method, objects) -> {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(o.getClass().getSimpleName());
        stringBuilder.append(".");
        stringBuilder.append(method.getName());
        stringBuilder.append("[");
        for (Object obj : objects) {
            stringBuilder.append(obj.toString());
        }
        stringBuilder.append("]");

        return stringBuilder.toString();
    };
}

@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
    return new RedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory), 
    this.redisCacheConfiguration(600), 	// 默认配置
    this.initialCacheConfigurations());	// 指定key过期时间配置
}
           
private Map<String, RedisCacheConfiguration> initialCacheConfigurations() {
    Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
    redisCacheConfigurationMap.put("UserInfoList", this.redisCacheConfiguration(3000));
    redisCacheConfigurationMap.put("UserInfoListAnother", this.redisCacheConfiguration(18000));

    return redisCacheConfigurationMap;
}

private RedisCacheConfiguration redisCacheConfiguration(Integer seconds) {
    Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
    ObjectMapper om = new ObjectMapper();
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    jackson2JsonRedisSerializer.setObjectMapper(om);

    RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
    redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).entryTtl(Duration.ofSeconds(seconds));

    return redisCacheConfiguration;
}

}

        
		

        
@Cacheable(value = "DefaultKey", keyGenerator = "simpleKeyGenerator") // 600秒,使用默认策略
@Cacheable(value = "UserInfoList", keyGenerator = "simpleKeyGenerator") // 3000秒
@Cacheable(value = "UserInfoListAnother", keyGenerator = "simpleKeyGenerator") // 18000秒

        
		
        
127.0.0.1:6379> keys *
1) "test2::SimpleKey []"

127.0.0.1:6379> ttl "test2::SimpleKey []"
(integer) 584
        
        
		

Cannot serialize

org.springframework.data.redis.serializer.SerializationException: Cannot serialize

Caused by: java.io.InvalidClassException: cn.netkiller.domain.PictureClick; class invalid for deserialization

解决方法

		
package cn.netkiller.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Table;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.*;

import java.io.Serializable;

@Entity
@Table
@Data
@DynamicInsert
@DynamicUpdate
@Comment("图片点击数据")
public class PictureClick implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", unique = true, nullable = false, insertable = false, updatable = false, columnDefinition = "int unsigned")
    @Comment("主键")
    private Integer id;

    @ManyToOne
    @OnDelete(action = OnDeleteAction.CASCADE)
    @JoinColumn(nullable = false, insertable = true, updatable = false)
    @JsonIgnore
    @Comment("设备")
    private Device device;

    @Comment("图片")
    @ManyToOne
    @OnDelete(action = OnDeleteAction.CASCADE)
//    @JoinColumn(name = "picture_id", nullable = false, insertable = true, updatable = false)
//    @JsonIgnore
    private Picture picture;

    @Comment("点赞")
    private Boolean likes = false;

    @Comment("收藏")
    private Boolean favorites = false;

    @Comment("转发")
    private Boolean forward = false;
}
		
		
		

public class PictureClick implements Serializable