英文:
Java Spring Boot Redis – How to know when is the Entity updated so i can fetch from database instead of Redis?
问题
I understand that you want a translation of the provided code and text without any additional information or responses. Here is the translated code:
我对Redis还不熟悉,这可能是一个基本问题。
我使用了`@Cacheable()`和`@CacheEvict()`注解。当用户被更新时,如果我通过ID获取用户,它会获取到缓存的(过时的)数据。当然,如果我使用`@CacheEvict()`,这种情况就不会发生。
然而,我对`@CacheEvict()`感到困惑,因为它的结果与不使用它的结果相同,那么使用它有什么意义呢?如果有一个需要3秒才能完成的过程,那么使用`CacheEvict()`也需要3秒。
这是我的`UserServiceImpl.java`类:
```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而不是PostgreSQL中获取)。然而,更新此用户并再次获取它会返回过时的数据而不是新更新的用户,导致数据不一致。
英文:
I'm new to redis, this might be a basic question.
I use @Cacheable()
and @CacheEvict()
annotation. When the user gets updated, and if i fetch the user by id, it fetches the cached (outdated) data. Of course, if i were to use @CacheEvict()
this wouldn't happen.
However, i'm confused about @CacheEvict()
, because the results are the same as if i don't use it -- so whats the point of using it? If there is a process that takes 3 seconds to finish, then using CacheEvict()
would also take 3 seconds.
Here is my UserServiceImpl.java
class:
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();
}
}
}
My RedisConfig.java
class:
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()));
}
}
Fetching data for the first time takes 3 seconds. Fetching the same data next time takes 5 ms (this time gets pulled from Redis instead of postgres). However updating this user and fetching it again, gives outdated data instead of the newly updated user, causing data inconsistencies.
UPDATE: this is my model/User.java
model class
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;
}
I also have dto/UserDTO.java
for converting the model into a REST response/request via API:
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;
}
Thanks to @Max Kozlov this DTO class is now a Serializable
so that Redis Cache can work properly.
The new RedisCacheConfig.java
thanks to @Max Kozlov's answer looks like this:
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();
}
}
答案1
得分: 1
你对使用注解 @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 中缓存数据,你需要避免列表缓存,只对特定的实体应用这个方法。你还需要注意,如果你想要将实体存储在缓存中,那么你需要在实体本身创建一个序列化版本。
希望我的回答对你有帮助 =)
更新
对于 spring-boot 2.7.* 版本,这是 CacheConfiguration
类。
@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(
RuntimeException exception,
Cache cache,
Object key
) {
LOG.info(
"handleCacheGetError ~ {}: {} - {}",
exception.getMessage(),
cache.getName(),
key
);
}
@Override
public void handleCachePutError(
RuntimeException exception,
Cache cache,
Object key,
Object value
) {
LOG.info(
"handleCachePutError ~ {}: {} - {}",
exception.getMessage(),
cache.getName(),
key
);
super.handleCachePutError(exception, cache, key, value);
}
@Override
public void handleCacheEvictError(
RuntimeException exception,
Cache cache,
Object key
) {
LOG.info(
"handleCacheEvictError ~ {}: {} - {}",
exception.getMessage(),
cache.getName(),
key
);
super.handleCacheEvictError(exception, cache, key);
}
@Override
public void handleCacheClearError(
RuntimeException exception,
Cache cache
) {
LOG.info(
"handleCacheClearError ~ {}: {}",
exception.getMessage(),
cache.getName()
);
super.handleCacheClearError(exception, cache);
}
}
在这种情况下,使用了简单的 Java 序列化程序。需要缓存的对象类需要实现 Serializable
接口。
public class User implements Serializable {
private Long id;
private Long name;
// getters/setters
}
配置类是启用通过 Redis 进行缓存的标准配置,除了一个细节,即 DefaultCacheErrorHandler
。如果更改实体类并相应地更改该类的 serialVersionUID
,则需要 DefaultCacheErrorHandler
以重置缓存。通常情况下,由于某种原因,Spring 在发生序列化错误时不会自动删除缓存,而是会引发错误,导致需要手动从 Redis 中删除必要的键。
附加回答
将实体序列化为 JSON 的问题与缓存本身不再相关。这是一个旧问题,更容易避免,并没有真正的解决方案。链接。总的来说,这种方法属于不良架构。因此,如果需要将从数据库中获取的数据序列化,那么最正确的选项将是创建一个 DTO 对象,并从实体中填充数据。因此,由于从数据库缓存数据是一个常见的任务,最好妥协而不是以不良架构告终。
还需要记住序列化和反序列化的速度。通常,在不需要大量时间来处理业务逻辑的任务中,大部分时间都用于 JSON 数据的序列化和反序列化。尽管 Jackson 库无论多么出色,都需要定期记住这
英文:
You have the wrong logic for using the annotation @Cachable
because you are caching the entire list of users without a specific key.
In other words, you need to cache a specific user, for example, by id
.
Now you have a full list of users cached with key users
. But the entry with the users:id
key is deleted. Therefore your cache is not evict.
For the cache to work, you need to rewrite your service class in this way.
@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);
}
}
Here I moved the annotation @Cacheable(value = "users", key = "#userId")
from method findAll()
to method findUser(String userId)
. I also corrected the annotation @Cacheable
and added the key key = "#userId"
there.
Anyway, if you want to cache data in Redis. You need to avoid list caching, and only apply this approach to specific entities. You also need to pay attention to the fact that if you want to store entities in the cache, then you need to create a serial version in the entity itself.
Anyway, if you want to cache data in Redis. You need to avoid list caching, and only apply this approach to specific entities. You also need to pay attention to the fact that you are serializing entities in json. It is highly discouraged to do this, because relationships such as @ManyToOne
and @OneToMany
can cause you to get a recursive call to these relationships at the time of serialization.
Hope my answer helps you =)
UPDATE
CacheConfiguration
class for spring-boot 2.7.* version.
@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();
}
}
and DefaultCacheErrorHandler
class for exception handlings
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);
}
}
in this case, a simple java serealizer is used. The object classes that need to be cached need to be implemented from the Serializable
interface.
public class User implements Serializable {
private Long id;
provate Long name;
// getters/setters
}
The config class is standard for enabling caching via redis, except for one detail, namely the DefaultCacheErrorHandler
. The DefaultCacheErrorHandler
is needed in order to reset the cache if you change the entity class and change the serialVersionUID of this class accordingly. This is usually required if you are adding or removing fields from a class. By default, for some reason, spring does not delete the cache when a serialization error occurs, but throws an error that leads to the need to manually delete the necessary keys from the redis.
Additional Answers
The problem of serializing entities to json is no longer related to the cache as such. This is an old problem that is easier to avoid and has no real solution. link In general, this approach lies in the plane of bad architecture. For this reason, if it is necessary to serialize data received from the database, then the most correct option would be to make an DTO object and fill it with data from the entity. For this reason, since caching data from the database is a common task, it is better to use standard java serialization.
It is also necessary to remember about the speed of serialization and deserialization. Often, in tasks where a lot of time is not required to process business logic, most of the time is taken just by serialization and deserialization of data from json. Unfortunately, no matter how good the Jackson library is, it needs to be remembered periodically. This question is beyond the scope of this discussion, but there are many interesting answers on this topic in the stackoverflow.
Plus, you can speculate on the amount of memory that is needed to store the cache. Json, as shown by recent changes in the backend development, is well replaced by the serialization approach in the same grpc. This allows in some cases to significantly save in terms of information transmitted over the network, which can also save a lot of memory in terms of caching. True, I do not know how much the standard java serialization is better in this respect than json.
Summarizing, we can say that although the data in the radis is better perceived by a person in the form of a json object. But as my experience suggests, for this task it is better to compromise than to end up with a bad architecture.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论