You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.26 11:24:03