通过版本化 Redis 缓存键防止 Spring Boot 部署期间的过时数据
Source: Dev.to
应用程序经常会生成需要在运行时稍后消费的内容。这些内容可能存储在文件系统中、写入数据库表、通过消息中间件共享,或放置在共享缓存中。随着软件系统随时间演进,之前生成内容的消费者理应能够处理这些变化——但这并不总是可行的。
示例(未使用版本化)
@Service
public class UserProfileService {
private final UserRepository userRepository;
public UserProfileService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Cacheable("userProfile")
public UserProfile getProfile(Long userId) {
User user = userRepository.findById(userId).orElseThrow();
return new UserProfile(
user.getId(),
user.getFirstName(),
user.getLastName()
);
}
}
随后我们通过添加 email 字段来扩展 UserProfile:
return new UserProfile(
user.getId(),
user.getFirstName(),
user.getLastName(),
user.getEmail()
);
由于 Spring 已经在缓存中存放了先前对象结构的实例,后续调用 getProfile() 时会出现反序列化错误,例如:
org.springframework.data.redis.serializer.SerializationException: Cannot serialize
具体的错误信息取决于序列化器的配置。
为什么需要版本化
虽然问题本身以及解决思路在概念上很简单,但在生产环境中的影响可能非常严重。错误可能导致长时间的停机、部署失败或被迫回滚——尤其是当缓存条目的 TTL(生存时间)较长时。
带版本号的缓存键
一种常见且有效的做法是,在缓存键中加入版本标识(在使用共享缓存时可再加上应用名称)。这样可以确保新版本的应用不会尝试读取旧版本创建的不兼容的缓存数据。
@Configuration
public class CacheConfiguration {
@Bean
public RedisCacheConfiguration redisCacheConfiguration(
ObjectProvider<BuildProperties> buildPropertiesProvider) {
BuildProperties buildProperties = buildPropertiesProvider.getIfAvailable();
String name = (buildProperties != null) ? buildProperties.getName() : "application";
String version = (buildProperties != null) ? buildProperties.getVersion() : "dev";
return RedisCacheConfiguration.defaultCacheConfig()
.prefixCacheNameWith(String.format("%s:%s:", name, version));
}
}
缓存键可以使用 Maven 的构建信息(通过 Maven 资源过滤或 spring-boot-maven-plugin 的 build-info 目标)、Git 提交哈希(例如使用 git-commit-id-plugin),或任何由 CI/CD 流水线注入的元数据来构造。
生成的 Redis 键
当 Redis 按上述方式配置了全局键前缀后,缓存条目会使用带版本号的键存储,例如:
127.0.0.1:6379> keys *
1) "payment-gateway:1.1.0:userProfile::1"
2) "payment-gateway:1.1.1:userProfile::1"
由于每个应用实例只会访问与自身版本匹配的缓存键,部署和回滚可以安全进行,而不会因不兼容的缓存数据而触发反序列化错误。
参考资料
- 缓存键版本化代码示例
- 向 Spring Boot 应用添加构建属性
- 将 Git 信息注入 Spring
- Spring Data Redis 配置