首页 > 数据库 >解决Redis缓存数据类型丢失问题

解决Redis缓存数据类型丢失问题

时间:2025-01-03 18:24:58浏览次数:5  
标签:缓存 Object Redis value 缓存数据 丢失 Date 序列化

一、背景

在通用的数据开放平台中,支持用户编写基于Groovy脚本的接口,Groovy脚本中可以查询数据库,然后对数据库中的数据进行一些处理。平台支持任何接口都可以启用缓存。

缓存不是缓存整个脚本的结果,而是只支持缓存数据库查询语句的结果,这样Groovy脚本中的其他逻辑依然可以处理数据。之前一直运行良好没有问题,最近数据接口开发人员反馈,某个新上的接口一旦开启缓存就会报错,结果沟通与排查发现, 是因为在缓存数据库查询结果到Redis中的时候,丢失了数据类型,导致后续的Groovy处理逻辑出现异常。

二、问题复现

首先来看这个Groovy查询脚本。这个脚本查询数据库中的数据,然后针对数据中的日期时间字段进行需要的处理,然后返回数据。

def result_list = db.selectList('''
    select 
        id, title, url, publish_time
    from t1
    order by publish_time desc limit 50
''')

def date = (new Date().time / 1000).longValue()
for (data in result_list) {
   
    def publishTime = (data.publishTime.time / 1000).longValue()
    def diff_time = Math.round((date - publishTime) / 60)
    if (diff_time < 60) {
   
        data.put("diffTime", String.valueOf(diff_time) + '分钟前')
    } else if (diff_time >= 60 && diff_time < 1440) {
   
        data.put("diffTime", String.valueOf(Math.round(diff_time / 60)) + '小时前')
    } else {
   
        data.put("diffTime", data.publishTime)
    }
}
return result_list
   

在SQL的查询结果中,包含了publish_time 字段,该字段在StarRocks中的类型为DATETIME 类型,db.selectList 方法的实现中,是直接将数据库返回的结果,包装为一个List>进行返回,这时候我们在结果中会发现 publish_time 字段的类型为Date,因此开发同学直接在下面的处理逻辑中使用了 data.publishTime.time 方法来获取时间戳,这样如果从数据库中获取数据都是没有问题。

由于平台本身逻辑为开发阶段即使开启缓存也不会走缓存,因此在开发阶段,没有发现这个错误,接口上线发布之后,并且开启缓存之后,发现接口异常,报错信息显示:属性丢失,Long类型没有time属性

三、问题分析

接口在开启缓存之后出现问题,也就是表示缓存中的数据存在问题,平台设计为既支持本地缓存也支持Redis缓存,测试之后发现,都存在问题,因此对缓存模块代码进行检查。

缓存采用AOP实现,通过拦截自定义注解来对数据库的查询结果进行缓存,简化后的缓存代码如下:

@Around("dataCacheLayerPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
   
    CacheParams cacheParams = parseCacheParams(apiInfoContent);
    Boolean isLocalTest = apiInfoContent.getIsLocalTest();
    String cacheKey = null;
    // 不是开发环境 并且 启用缓存
    if(!isLocalTest && cacheParams.getEnableCache()){
   
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        //参数名数组
        String[] parameters =  methodSignature.getParameterNames();
        //参数值
        Object[] args = joinPoint.getArgs();
        DataCacheLayer dataCacheLayer = methodSignature.getMethod()
                                .getDeclaredAnnotation(DataCacheLayer.class);
        DataCacheType dataCacheType = dataCacheLayer.dataCacheType();
        cacheKey = generalCacheKey(dataCacheType, parameters, args);
        // 查询缓存
        Object cacheData = queryCache(cacheKey);
        if(cacheData != null){
   
            return JSONArray.parseArray(cacheData.toString(), Map.class);
        }
    }
    // 走原始逻辑
    Object proceed = joinPoint.proceed();
    // 设置缓存
    if(!isLocalTest && cacheParams.getEnableCache()){
   
        addCache(cacheKey, proceed);
    }
    return proceed;
}
   

从这个代码来看,我们发现从缓存中拿出来的数据是字符串,然后将其转换为了List, 这时候我们就发现,从字符串转换过来的数据,其数据类型发生了变化,publishTime 字段的类型从Date转变为了 Long,我们这时候继续查看具体的缓存代码,这里只看Redis缓存的实现。

@Override
public void set(String cacheKey, Object value, Long cacheTime) {
   
    try {
   
        String jsonString = JSON.toJSONString(value);
        redisUtil.set(buildApiInfoKey(cacheKey), jsonString, cacheTime);
    }catch (Exception e){
   
        LOGGER.error("写缓存失败",e);
    }
}

@Override
public String get(String cacheKey) {
   
    return redisUtil.get(buildApiInfoKey(cacheKey));
}
   

可以看到我们将一个对象转换为了一个json字符串,将其存储到了Redis中,然后在读取的时候也是读取到了一个字符串。分析到这里,我们认为这是由于json字符串转换导致,因此我们想到,如果我们在Redis中不存储转换之后的字符串而是直接存储对象,是否就能够解决这个问题。

经过实践之后发现,这样并不能解决问题,因为数据在存储到Redis中的时候,会经过Redis的数据序列化策略。默认的数据序列化策略,是将数据序列化为json进行存储,这时候Date类型的字段经过序列化和反序列化之后就变成了Long类型的时间戳。

经过分析和测试,明确了问题出现的根本原因,是因为Redis的数据序列化和反序列导致的;为了保持平台的灵活性和通用性,决定对平台缓存逻辑进行优化。

四、问题解决

经过调研,可以自定义Redis的序列化和反序列化逻辑。因此如果我们在数据存储进行Redis的时候保持其类型,那么在将数据读取出来之后,就可以根据类型进行恢复,这样就不会发现数据类型丢失。

经过分析之后,发现目前只有Date类型的存在问题,因此我们目前只考虑了Date类型的丢失问题,具体的解决方案为:

  1. 在将数据写入Redis的时候,也就是序列化阶段,判断数据是否为Date类型,如果是,将其值转换为特定的字符串数据格式:yyyy-MM-dd HH:mm:ss[type:date] ,然后将其写入Redis。
  2. 在从Redis读取数据的时候,也就是发序列化阶段,判断数据的值是否以[type:date]结尾,如果是将其转换为Date 类型的值。

自定义的序列化器如下:

public class ComplexTypeRedisSerializer implements RedisSerializer<Object> {
   

    // 使用 FastDateFormat,它是线程安全的
    private static final FastDateFormat fastDateFormat = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss");
    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");


    @Override
    public byte[] serialize(Object value) throws SerializationException {
   
        if (value == null) {
   
            return new byte[0];
        }
        // 对所有 Map 和 List 类型的数据,遍历处理其中的 Date 字段并嵌入类型信息
        processDates(value);
        // 使用 fastjson 将对象序列化为 JSON 字符串
        String jsonString = JSON.toJSONString(value, SerializerFeature.WRITE_MAP_NULL_FEATURES, SerializerFeature.QuoteFieldNames);
        return jsonString.getBytes(StandardCharsets.UTF_8);
    }

    @Override
    public Object deserialize(byte[] bytes) throws SerializationException {
   
        if (bytes == null || bytes.length == 0) {
   
            return null;
        }
        // 将字节数组转换为 JSON 字符串
        String jsonString = new String(bytes, StandardCharsets.UTF_8);
        // 反序列化 JSON 字符串为 Map 或其他对象
        Object jsonObject = JSON.parse(jsonString);
        recursiveParse(jsonObject);
        // 根据字段的类型信息来还原字段类型
        return jsonObject;
    }


    // 递归解析 JSON 对象
    private  void recursiveParse(Object object) {
   
        if (object instanceof JSONObject) {
   
            JSONObject jsonObject = (JSONObject) object;
            for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
   
                // 递归调用解析每一个字段
                Object value = entry.getValue();
                if(value != null && value.toString().endsWith("[type:date]")){
   
                    jsonObject.put(entry.getKey(), getDate(value.toString()));
                }else{
   
                    recursiveParse(value);
                }
            }
        } else if (object instanceof JSONArray) {
   
            JSONArray jsonArray = (JSONArray) object;
            for (Object o : jsonArray) {
   
                // 递归调用解析每个数组元素
                if(o != null && o.toString().endsWith("[type:date]")){
   
                    jsonArray.add(getDate(o.toString()));
                }else{
   
                    recursiveParse(o);
                }

            }
        }
    }


    private Object getDate(String value) {
   
        if(value.endsWith("[type:date]")){
   
            String dateString = value.substring(0, value.indexOf("[type:date]"));
            try {
   
               return Date.from(LocalDateTime.parse(dateString, formatter).atZone(ZoneId.systemDefault()).toInstant());
            }catch (Exception e){
   
                return dateString;
            }
        }
        return value;
    }

    /**
     *  递归处理Map或者List<Map>中的Date类型
     * @param object 对象
     */
    private  void processDates(Object object) {
   
        if(object instanceof PageList){
   
            object = ((PageList) object).getDataList();
        }
        if (object instanceof Map) {
   
            // 如果是 Map 类型,递归处理每个条目
            Map<Object, Object> map = (Map<Object, Object>) object;
            for (Map.Entry<?, ?> entry : map.entrySet()) {
   
                // 如果值是 Date 类型,更新为格式化字符串
                if (entry.getValue() instanceof Date) {
   
                    map.put(entry.getKey(), fastDateFormat.format(entry.getValue()) + "[type:date]");
                }else{
   
                    // 递归更新每个条目的值
                    processDates(entry.getValue());
                }
            }
        } else if (object instanceof List) {
   
            // 如果是 List 类型,递归处理每个元素
            List<Object> list = (List<Object>) object;
            for (int i = 0; i < list.size(); i++) {
   
                Object item = list.get(i);
                // 如果元素是 Date 类型,修改它
                if (item instanceof Date) {
   
                    list.set(i, fastDateFormat.format(item) + "[type:date]");
                }else{
   
                    // 递归更新列表中的元素
                    processDates(item);
                }
            }
        }
    }
}
   

然后将自定义的序列化器设置给Redis的value的序列化策略。

public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
   
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(redisConnectionFactory);
    // 设置value的序列化规则和 key的序列化规则
    redisTemplate.setValueSerializer(new ComplexTypeRedisSerializer());
    // 使用自定义的序列化器
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.afterPropertiesSet();
    return redisTemplate;
}
   

五、存在待优化的地方

目前整个判断是否包含Date类型的数据的逻辑比较粗暴,直接通过遍历的方式,后续可以优化,在其中加入特殊标识来标识是存在,提示数据处理速度。

标签:缓存,Object,Redis,value,缓存数据,丢失,Date,序列化
From: https://www.cnblogs.com/cnblogsisgod/p/18650695

相关文章

  • Redis 爆高危漏洞,请速度修复。。
    大家好,我是R哥。今天一早收到了腾讯云给我的【主机安全】漏洞通知:好家伙,大名鼎鼎的Redis爆高危漏洞了,R哥的题库「Java面试库」也用到了Redis来缓存面试题内容,所以这一下子就引起了我的警惕,赶紧看看什么鬼。漏洞描述下面是漏洞描述和修复说明:https://github.com/redis/r......
  • 解决Redis缓存数据类型丢失问题
    一、背景在通用的数据开放平台中,支持用户编写基于Groovy脚本的接口,Groovy脚本中可以查询数据库,然后对数据库中的数据进行一些处理。平台支持任何接口都可以启用缓存。缓存不是缓存整个脚本的结果,而是只支持缓存数据库查询语句的结果,这样Groovy脚本中的其他逻辑依然可以处理数据......
  • Redis数据库——内存淘汰机制
    大家好,这里是GoodNote,关注公主号:Goodnote,专栏文章私信限时Free。本文详细介绍Redis的8种内存淘汰机制。文章目录过期键删除策略内存淘汰机制内存限制设置常见策略Redis3.0的淘汰机制——近似LRU算法Redis4.0的新增的淘汰机制——LFU算法历史文章MySQL数......
  • Java Redis多限流
    在现代Web应用中,限流(RateLimiting)是保护系统资源和防止滥用的重要机制。Redis由于其高性能和原子操作特性,成为实现限流的理想选择。本文将详细介绍如何在Java中使用Redis实现多种限流策略,包括固定窗口限流、滑动窗口限流和令牌桶算法。一、准备工作1.安装Redis确保Redis已经......
  • springboot3 redis 常用操作工具类
    在SpringBoot3中,操作Redis通常使用SpringDataRedis提供的工具类,如RedisTemplate和StringRedisTemplate。以下是一个详细的Redis操作工具类的实现,涵盖了常用功能。完整的Redis工具类以下工具类可以实现基本的Redis操作,例如字符串、哈希、列表、集合和有......
  • redis的学习(一)
    1.环境搭建1.1在ubuntu上安装redis1.2reids客户端介绍    redis也是一个客户端-服务器结构的程序。redis客户端和服务器可以在同一份主机上,也可以在不同的主机上,因为二者是通过网络进行发送和接收请求的。    redis服务器是负责存储和管理数据的。 ......
  • Redis,MongoDB,MySQL,ES之间的区别与适用场景
    Redis,MongoDB,MySQL,ES之间的区别与适用场景:redis是一种高性能键值存储数据库,基于内存操作,支持数据持久化,支持数据类型丰富(如:字符串,哈希,列表,集合,有序集合等),redis还提供了订阅/发布,事务,lua脚本,主从同步等功能,适用于访问频繁,数据量较小,对性能要求比较高的业务场景,如缓存,队列,计数......
  • 《docker基础篇:8.Docker常规安装简介》包括:docker常规安装总体步骤、安装tomcat、安装
    @目录8.Docker常规安装简介8.1docker常规安装总体步骤8.2安装tomcat8.3安装mysql8.3.1dockerhub上面查找mysql镜像8.3.2从dockerhub上(阿里云加速器)拉取mysql镜像到本地标签为5.78.3.3使用mysql5.7镜像创建容器(也叫运行镜像)简单版实战版8.4安装redis8.5安装nginx本人......
  • 20道Redis面试题核心技术知识点
    1、什么是Redis,Redis有哪些特点?2、Redis有哪些数据结构?3、一个字符串类型的值能存储最大容量是多少?4、能说一下Redis每种数据结构的使用场景吗?......
  • Redis(Windows)开机自启配置
    1.下载RedisReleases·redis-windows/redis-windows2.将Redis的路径添加到系统的环境变量当中3.将Redis进程注册为服务(1)在Redis目录下启动cmd,并执行以下指令:redis-server.exe--service-installredis.windows.conf--loglevelverbose(2)或win+R启动cmd,并根据情况执行以......