Spring Boot集成Redis使用@Cacheable注解能否将返回值存储为Redis Hash
答案
完全可以通过自定义Spring Cache的Redis缓存配置,结合原生@Cacheable注解实现将返回结果以Redis Hash格式存储的需求,无需放弃注解的便利性,以下是适配你使用的Spring Boot 2.1.4.RELEASE + Jedis客户端的具体实现步骤:
核心实现步骤
1. 自定义支持Hash结构的Redis缓存管理器
我们通过改写RedisCacheWriter的读写逻辑,实现将返回对象自动序列化为Hash结构存储,同时原生支持TTL过期时间设置:
import org.springframework.cache.annotation.EnableCaching; 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.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.data.redis.hash.Jackson2HashMapper; import com.fasterxml.jackson.databind.ObjectMapper; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Map; import java.util.stream.Collectors; @Configuration @EnableCaching public class RedisCacheConfig { private final ObjectMapper objectMapper = new ObjectMapper(); private final Jackson2HashMapper hashMapper = new Jackson2HashMapper(objectMapper, true); @Bean public RedisCacheManager defaultCacheManager(RedisConnectionFactory redisConnectionFactory) { // 自定义缓存读写逻辑 RedisCacheWriter customCacheWriter = new RedisCacheWriter() { private final RedisCacheWriter defaultWriter = RedisCacheWriter.lockingRedisCacheWriter(redisConnectionFactory); @Override public void put(String name, byte[] key, byte[] value, Duration ttl) { try (RedisConnection conn = redisConnectionFactory.getConnection()) { // 将序列化的value转回对象后转为Hash结构 UserDto userDto = objectMapper.readValue(value, UserDto.class); Map<String, Object> hashMap = hashMapper.toHash(userDto); // 执行hset写入 Map<byte[], byte[]> byteHash = hashMap.entrySet().stream() .collect(Collectors.toMap( entry -> entry.getKey().getBytes(StandardCharsets.UTF_8), entry -> objectMapper.writeValueAsBytes(entry.getValue()) )); conn.hMSet(key, byteHash); // 设置TTL过期时间 if (ttl != null && !ttl.isZero() && !ttl.isNegative()) { conn.expire(key, ttl.getSeconds()); } } catch (Exception e) { throw new RuntimeException("Hash结构序列化失败", e); } } @Override public byte[] get(String name, byte[] key) { try (RedisConnection conn = redisConnectionFactory.getConnection()) { Map<byte[], byte[]> hashEntries = conn.hGetAll(key); if (hashEntries.isEmpty()) return null; // 将Hash结构转回UserDto对象 Map<String, Object> hashMap = hashEntries.entrySet().stream() .collect(Collectors.toMap( entry -> new String(entry.getKey(), StandardCharsets.UTF_8), entry -> objectMapper.readValue(entry.getValue(), Object.class) )); UserDto userDto = (UserDto) hashMapper.fromHash(hashMap); return objectMapper.writeValueAsBytes(userDto); } catch (Exception e) { throw new RuntimeException("Hash结构反序列化失败", e); } } // 剩余方法直接委托默认实现即可 @Override public byte[] putIfAbsent(String name, byte[] key, byte[] value, Duration ttl) { return defaultWriter.putIfAbsent(name, key, value, ttl); } @Override public void remove(String name, byte[] key) { defaultWriter.remove(name, key); } @Override public void clean(String name, byte[] pattern) { defaultWriter.clean(name, pattern); } }; // 基础缓存配置 RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisCacheConfiguration.defaultCacheConfig().getKeySerializationPair()) .serializeValuesWith(RedisCacheConfiguration.defaultCacheConfig().getValueSerializationPair()) // 可在这里配置全局默认TTL,也可以单独针对缓存空间配置 .entryTtl(Duration.ofHours(1)); return RedisCacheManager.builder(customCacheWriter) .cacheDefaults(cacheConfig) .build(); } }
2. 原有业务代码无需修改
你现在使用的@Cacheable注解可以直接保留,不需要做任何调整:
@Cacheable(value = "cache", key = "#request.userId", cacheManager = "defaultCacheManager") public UserDto createOrFetch(CreateUserRequest request) { // 原有业务逻辑不变 }
补充说明
纠正你之前的认知误区:
RedisTemplate的HashOperations其实是支持为Hash设置TTL的,只需要额外调用redisTemplate.expire(key, 过期时长, 时间单位)方法即可实现,只是这种方式需要硬编码缓存逻辑,不如注解式缓存优雅。
- 如果需要支持多种不同类型的DTO以Hash结构存储,可以将上述代码中的硬编码
UserDto改为泛型实现,通过缓存名称或者类类型标识动态判断需要序列化的对象类型即可 - 该实现完全基于Spring Data Redis的抽象层,和你使用的Jedis客户端完全兼容,不需要额外调整Jedis的连接配置
内容的提问来源于stack exchange,提问作者SBhogal
相关产品推荐
相关产品推荐

