我刚接触Redis,这可能是一个基本的问题.

我使用@Cacheable()@CacheEvict()注释.当用户更新时,如果我通过id获取用户,它将获取缓存的(过时的)数据.当然,如果我使用@CacheEvict(),这种情况就不会发生.

然而,我对@CacheEvict()感到困惑,因为结果与我不使用它是一样的--那么使用它有什么意义呢?如果有一个过程需要3秒才能完成,那么使用CacheEvict()也需要3秒.

以下是我的UserServiceImpl.java个班级:

package com.example.demo.serviceImpl;

import lombok.AllArgsConstructor;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import com.example.demo.service.UserService;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;

@Service
@EnableCaching
@AllArgsConstructor
public class UserServiceImpl implements UserService {

    private UserRepository userRepository;

    @Override
    public User createUser(User user) {
        return userRepository.save(user);
    }

    @Override
    @CacheEvict(value = "users")
    public User findUser(String userId) {
        doLongRunningTask();
        return userRepository.findById(userId).orElseThrow();
    }

    @Override
    @Cacheable(value = "users")
    public List<User> findAll() {
        return (List<User>) userRepository.findAll();
    }

    @Override
    @CacheEvict(value = "users", key = "#user.id")
    public User updateUser(String userId, User user) {
        doLongRunningTask();
        user.setUpdatedAt(LocalDateTime.now());
        return userRepository.save(user);
    }

    @Override
    @CacheEvict(value = "users", key = "#userId")
    public void deleteUser(String userId) {
        userRepository.deleteById(userId);
    }

    private void doLongRunningTask() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

我的RedisConfig.java个班级:

package com.example.demo.config;

import org.springframework.beans.factory.annotation.Value;
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.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;

import java.time.Duration;

import static org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair.fromSerializer;

@Configuration
public class RedisConfig {

    @Value("${redis.host}")
    private String redisHost;

    @Value("${redis.port}")
    private int redisPort;

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        configuration.setHostName(redisHost);
        configuration.setPort(redisPort);
        return new LettuceConnectionFactory(configuration);
    }

    @Bean
    public RedisCacheManager cacheManager() {
        RedisCacheConfiguration cacheConfig = myDefaultCacheConfig(Duration.ofMinutes(10)).disableCachingNullValues();

        return RedisCacheManager
                .builder(redisConnectionFactory())
                .cacheDefaults(cacheConfig)
                .withCacheConfiguration("users", myDefaultCacheConfig(Duration.ofMinutes(5)))
                .build();
    }

    private RedisCacheConfiguration myDefaultCacheConfig(Duration duration) {
        return RedisCacheConfiguration
                .defaultCacheConfig()
                .entryTtl(duration)
                .serializeValuesWith(fromSerializer(new GenericJackson2JsonRedisSerializer()));
    }

}

第一次获取数据需要3秒.下一次获取相同的数据需要5毫秒(这一次从Redis而不是postgres获取).然而,更新该用户并再次获取它时,会给出过时的数据,而不是新更新的用户,从而导致数据不一致.

这是我的model/User.java名模特班

package com.example.demo.model;

import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

@Data
@Builder
@RedisHash("user")
public class User {

    @Id
    private String id;
    private String name;
    private Integer age;

}

我还有dto/UserDTO.java个用于通过API将模型转换为REST响应/请求:

package com.example.demo.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO implements Serializable {

    @JsonProperty(value = "id")
    private String id;

    @JsonProperty(value = "name")
    private String name;

    @JsonProperty(value = "age")
    private Integer age;

}

多亏了@Max Kozlov,这个DTO类现在是Serializable,所以Redis缓存可以正常工作.

感谢@Max Kozlov的新RedisCacheConfig.java人如下所示:

package com.example.demo.config;

import com.example.demo.handler.DefaultCacheErrorHandler;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
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.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

import java.time.Duration;

@Configuration
@EnableCaching
public class RedisCacheConfig implements CachingConfigurer {

    @Value("${redis.host}")
    private String redisHost;

    @Value("${redis.port}")
    private int redisPort;

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        configuration.setHostName(redisHost);
        configuration.setPort(redisPort);
        return new LettuceConnectionFactory(configuration);
    }

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        return RedisCacheConfiguration
                .defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(15));
    }

    @Bean
    @Override
    public CacheErrorHandler errorHandler() {
        return new DefaultCacheErrorHandler();
    }

    @Bean("longLifeCacheManager")
    public CacheManager longLifeCacheManager() {
        RedisCacheConfiguration defaultConfiguration = RedisCacheConfiguration
                .defaultCacheConfig()
                .entryTtl(Duration.ofDays(90));

        return RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory())
                .cacheDefaults(defaultConfiguration)
                .build();
    }

    @Primary
    @Bean("shortLifeCacheManager")
    public CacheManager shortLifeCacheManager() {
        RedisCacheConfiguration defaultConfiguration = RedisCacheConfiguration
                .defaultCacheConfig()
                .entryTtl(Duration.ofDays(1));

        return RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory())
                .cacheDefaults(defaultConfiguration)
                .build();
    }

}

推荐答案

您使用注释@Cachable的逻辑是错误的,因为您缓存的是没有特定键的整个用户列表.

换句话说,您需要缓存特定的用户,例如,按id缓存.

现在,您已经有了使用键users缓存的用户的完整列表.但是具有users:id键的条目被删除.因此,您的藏身之处不会被驱逐.

要使缓存工作,您需要以这种方式重写服务类.

@Service
@EnableCaching
@AllArgsConstructor
public class UserServiceImpl implements UserService {

    private UserRepository userRepository;

    @Override
    public User createUser(User user) {
        return userRepository.save(user);
    }

    @Override
    @Cacheable(value = "users", key = "#userId")
    public User findUser(String userId) {
        doLongRunningTask();
        return userRepository.findById(userId).orElseThrow();
    }

    @Override
    public List<User> findAll() {
        return (List<User>) userRepository.findAll();
    }

    @Override
    @CacheEvict(value = "users", key = "#user.id")
    public User updateUser(String userId, User user) {
        doLongRunningTask();
        user.setUpdatedAt(LocalDateTime.now());
        return userRepository.save(user);
    }

    @Override
    @CacheEvict(value = "users", key = "#userId")
    public void deleteUser(String userId) {
        userRepository.deleteById(userId);
    }

}

在这里,我将注释@Cacheable(value = "users", key = "#userId")从方法findAll()移到了方法findUser(String userId).我还更正了注释@Cacheable,并在那里添加了键key = "#userId".

无论如何,如果你想在Redis中缓存数据.您需要避免列表缓存,并且只将此方法应用于特定实体.您还需要注意,如果希望在缓存中存储实体,则需要在实体本身中创建一个序列版本.

无论如何,如果你想在Redis中缓存数据.您需要设置为avoid list caching,并且仅将此方法应用于特定实体.您还知道您正在序列化json中的实体.这是非常不鼓励这样做的,因为像@ManyToOne@OneToMany这样的关系会导致你得recursive call to these relationships at the time of serialization分.

希望我的回答能帮到你=)

UPDATE

CacheConfiguration类,用于spring-boot 2.7.*版本.

@EnableCaching
@Configuration
public class CacheConfiguration extends CachingConfigurerSupport {

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
                                      .entryTtl(Duration.ofMinutes(15));
    }

    @Bean
    @Override
    public CacheErrorHandler errorHandler() {
        return new DefaultCacheErrorHandler();
    }

    @Bean("longLifeCacheManager")
    public CacheManager longLifeCacheManager(
        RedisConnectionFactory redisConnectionFactory
    ) {
        RedisCacheConfiguration defaultConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofDays(90));

        return RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .cacheDefaults(defaultConfiguration)
                .build();
    }

    @Bean("shortLifeCacheManager")
    @Primary
    public CacheManager shortLifeCacheManager(
        RedisConnectionFactory redisConnectionFactory
    ) {
        RedisCacheConfiguration defaultConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofDays(1));

        return RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .cacheDefaults(defaultConfiguration)
                .build();
    }

}

和用于异常处理的DefaultCacheErrorHandler个类

public class DefaultCacheErrorHandler extends SimpleCacheErrorHandler {

    private static final Logger LOG = LoggerFactory.getLogger(DefaultCacheErrorHandler.class);

    @Override
    public void handleCacheGetError(
        @NotNull RuntimeException exception,
        @NotNull Cache cache,
        @NotNull Object key
    ) {
        LOG.info(
            "handleCacheGetError ~ {}: {} - {}",
            exception.getMessage(),
            cache.getName(),
            key
        );
    }

    @Override
    public void handleCachePutError(
        @NotNull RuntimeException exception,
        @NotNull Cache cache,
        @NotNull Object key,
        Object value
    ) {
        LOG.info(
            "handleCachePutError ~ {}: {} - {}",
            exception.getMessage(),
            cache.getName(),
            key
        );
        super.handleCachePutError(exception, cache, key, value);
    }

    @Override
    public void handleCacheEvictError(
        @NotNull RuntimeException exception,
        @NotNull Cache cache,
        @NotNull Object key
    ) {
        LOG.info(
            "handleCacheEvictError ~ {}: {} - {}",
            exception.getMessage(),
            cache.getName(),
            key
        );
        super.handleCacheEvictError(exception, cache, key);
    }

    @Override
    public void handleCacheClearError(
        @NotNull RuntimeException exception,
        @NotNull Cache cache
    ) {
        LOG.info(
            "handleCacheClearError ~ {}: {}",
            exception.getMessage(),
            cache.getName()
        );
        super.handleCacheClearError(exception, cache);
    }
}

在本例中,使用了一个简单的Java序列化程序.需要缓存的对象类需要从Serializable接口实现.


public class User implements Serializable {

    private Long id;
    provate Long name;

    // getters/setters

}

Java相关问答推荐

如何让HikariCP指标在NewRelic中正确显示?

基于仅存在于父级中的字段查询子文档?

将linkedHashMap扩展到Java中的POJO类

Java:根据4象限中添加的行数均匀分布行的公式

如何将kotlin代码转换为java

Java事件系统通用转换为有界通配符

ApachePOI:不带换行的新行

Java inline Double条件和值解装箱崩溃

流迭代列表<;对象>;上的NoSuchElementException

使用Spring Boot3.2和虚拟线程的并行服务调用

Docker不支持弹性APM服务器

如何在JavaFX中制作鼠标透明stage

SpringBoot:在条件{Variable}.isBlank/{Variable}.isEmpty不起作用的情况下进行路径变量验证

使用SWIG将C++自定义单元类型转换为基本Java类型

JXBrowser是否支持加载Chrome扩展?

如何设计包含已知和未知键值对映射的Java类?

为什么没有加载java.se模块?

没有Google Play服务,Firebase Auth无法工作

错误:JOIN/ON的参数必须是boolean类型,而不是bigint类型.Java Spring启动应用程序

JavaFX中ListView中的问题