Redis Hash结构存储Long取出为Integer原因及解决方案?

Redis库使用的是spring-boot-data-redis,Redis的Hash结构存储Long数字类型,但取出来的是Integer,不用Hash直接存,取的就是Long,这是为什么?有办法Hash取的也是Long吗

直接存:

存到hash中:

存储hash的代码:

    private void setArticleActiveHash(Long articleId) {
        String key = "article_active:"+ articleId;
        Map<String, Long> articleActiveMap = new HashMap<>();
        articleActiveMap.put("love", 0L);
        articleActiveMap.put("commentCount", 0L);
        articleActiveMap.put("watch", 0L);
        articleActiveMap.put("collectionCount", 0L);
        articleActiveMap.put("articleId",articleId);
        redisCache.setCacheMap(key,articleActiveMap);
    }

RedisConfig

@Configuration
public class RedisConfig {
    @Resource(type = RedisConnectionFactory.class)
    private RedisConnectionFactory redisConnectionFactory;

    @Primary
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        // 解决反序列化 LocalDateTime 的错误
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        // 解决 LocalDateTime 序列化失败的问题
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(
                LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.WRAPPER_ARRAY);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key 采用 String 的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // value 序列化方式采用 jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash 的 key 也采用 String 的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // hash 的 value 序列化方式采用 jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }
}

虽然可以拿到后转Long,但每次都转好麻烦

阅读 4.1k
3 个回答
✓ 已被采纳

这是jackson一直存在的一个问题,虽然现在被解决了,但是低版本的jackson仍然存在问题,问题存在于ObjectMapper.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY);
不论DefaultTyping选择什么,Long类型序列化,都是不携带typing的。

高版本新增了DefaultTyping.EVERYTHING来解决,但是低版本可以参考redisson的解决方案,位于org.redisson.codec.JsonJacksonCodec#initTypeInclusion

TypeResolverBuilder<?> mapTyper = new DefaultTypeResolverBuilder(DefaultTyping.NON_FINAL) {
    public boolean useForType(JavaType t) {
        switch (_appliesFor) {
        case NON_CONCRETE_AND_ARRAYS:
            while (t.isArrayType()) {
                t = t.getContentType();
            }
            // fall through
        case OBJECT_AND_NON_CONCRETE:
            return (t.getRawClass() == Object.class) || !t.isConcrete();
        case NON_FINAL:
            while (t.isArrayType()) {
                t = t.getContentType();
            }
            // to fix problem with wrong long to int conversion
            if (t.getRawClass() == Long.class) {
                return true;
            }
            if (t.getRawClass() == XMLGregorianCalendar.class) {
                return false;
            }
            return !t.isFinal(); // includes Object.class
        default:
            // case JAVA_LANG_OBJECT:
            return t.getRawClass() == Object.class;
        }
    }
};
mapTyper.init(JsonTypeInfo.Id.CLASS, null);
mapTyper.inclusion(JsonTypeInfo.As.PROPERTY);
mapObjectMapper.setDefaultTyping(mapTyper);

重点在

// to fix problem with wrong long to int conversion
if (t.getRawClass() == Long.class) {
    return true;
}

Redisson解决非常完美,可以参考,我在两年前做架构师时,将该方案已经用到了生产上。

序列化器的问题,配置类改为的序列化器改为GenericFastJsonRedisSerializer就好了

问题的根因是 hash散列表格对象的值的序列化使用jackson,应该使用string,其默认值就是string
hash散列表格对象的值,不需要存储JSON字符串,我的理解这可能是不合理的设计表现。

// template.setHashValueSerializer(jackson2JsonRedisSerializer);
// 改为
template.setHashValueSerializer(stringRedisSerializer);

hash散列表格对象的值的序列化一定要使用jackson,还有另一种解法,使用spring-data-redis框架中HashMapper<T, K, V>,其有两个实现类Jackson2HashMapperBeanUtilsHashMapper。可以看看

Hash Mapping :: Spring Data Redis
https://docs.spring.io/spring-data/redis/reference/redis/hash...

/**
 * Core mapping contract between Java types and Redis hashes/maps. It's up to the implementation to support nested
 * objects.
 *
 * @param <T> Object type
 * @param <K> Redis Hash field type
 * @param <V> Redis Hash value type
 */
public interface HashMapper<T, K, V> {

    /**
     * Convert an {@code object} to a map that can be used with Redis hashes.
     */
    Map<K, V> toHash(T object);

    /**
     * Convert a {@code hash} (map) to an object.
     */
    T fromHash(Map<K, V> hash);
}
public class Jackson2HashMapper implements HashMapper<Object, String, Object>, HashObjectReader<String, Object> {
  // ...
}
public class BeanUtilsHashMapper<T> implements HashMapper<T, String, String>, HashObjectReader<String, String> {
  // ...
}

代码示例

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity>
        implements UserService, InitializingBean {
    private final UserDomainRepository userDomainRepository;

    private final RedisTemplate<Object, Object> redisTemplate;
//    private final StringRedisTemplate stringRedisTemplate;
//    private final RedisTemplate<String, UserEntity> userRedisTemplate;

    /**
     * HashMapper
     */
    private static final Jackson2HashMapper JACKSON_2_HASH_MAPPER = new Jackson2HashMapper(true);
    private static final BeanUtilsHashMapper<UserEntity> BEAN_UTILS_HASH_MAPPER = new BeanUtilsHashMapper<>(UserEntity.class);

    /**
     * <a href="https://docs.spring.io/spring-data/redis/reference/redis/hash-mappers.html">
     *     Spring Data Redis / Redis / Hash Mapping</a>
     */
    private UserEntity getUserEntityById(Long id) {
        // get from Cache
        String cacheKey = applyAsCacheKey(id);
        Map<String, Object> entries = redisTemplate.<String, Object>opsForHash()
                .entries(cacheKey);
        if (!entries.isEmpty()) {
            return JACKSON_2_HASH_MAPPER.fromHash(UserEntity.class, entries);
        }
//        // 所有字段值都为null
//        Map<String, String> entries = redisTemplate.<String, String>opsForHash()
//                .entries(cacheKey + ":BeanUtils");
//        if (!entries.isEmpty()) {
//            return BEAN_UTILS_HASH_MAPPER.fromHash(UserEntity.class, entries);
//        }

        // get from Database
        UserEntity userEntity = userDomainRepository.getById(id);
        if (userEntity == null) {
            return null;
        }

        // set to Cache
        redisTemplate.<String, Object>opsForHash()
                .putAll(cacheKey, JACKSON_2_HASH_MAPPER.toHash(userEntity));

        String key = cacheKey + ":BeanUtils";
        log.info("async redisTemplate opsForHash, key={}", key);
        redisTemplate.<String, String>opsForHash()
                            .putAll(key, BEAN_UTILS_HASH_MAPPER.toHash(userEntity));

        return userEntity;
    }

    private static String applyAsCacheKey(Long id) {
        return "user:" + id;
    }
}
/**
 * 用户实体
 */
@Data
@Accessors(chain = true)
@TableName(value = "t_digital_user", autoResultMap = true)
public class UserEntity {
    /**
     * 主键
     * <p></p>
     * 用户身份
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 真实姓名
     */
    @TableField(value = "user_name")
    private String userName;

    /**
     * 手机号
     */
    @TableField(value = "mobile")
    private String mobile;

    /**
     * 创建时间
     */
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}

Redis hash散列表格对象的底层数据,前者多了"@class"字段。
两者的区别主要是"id"字段值,前者是"123456"对象,后者是"\"123456\""字符串。

localhost:6379> TYPE "user:123456"
hash
localhost:6379> HGETALL "user:123456"
 1) "@class"
 2) "\"com.spring.boot.observability.example.domain.entity.UserEntity\""
 3) "createTime"
 4) "\"2024-08-09T19:59:24\""
 5) "mobile"
 6) "\"13588886666\""
 7) "updateTime"
 8) "\"2024-08-09T19:59:24\""
 9) "id"
10) "123456"
11) "userName"
12) "\"lihuagang-123456\""

localhost:6379> TYPE "user:123456:BeanUtils"
hash
localhost:6379> HGETALL "user:123456:BeanUtils"
 1) "createTime"
 2) "\"2024-08-09T19:59:24\""
 3) "mobile"
 4) "\"13588886666\""
 5) "updateTime"
 6) "\"2024-08-09T19:59:24\""
 7) "id"
 8) "\"123456\""
 9) "userName"
10) "\"lihuagang-123456\""
推荐问题
宣传栏