redis缓存生产实践(一)---大key压缩

news2025/2/27 2:06:31

文章目录

  • 前言
  • 一、缓存到底是使用String还是hash我该如何选择
  • 二、什么是大key及其影响
    • 2.1 什么是 Redis 大 key?
    • 2.2 大key带来的影响
  • 三、大key压缩
    • 3.1 注解标记可能需要压缩的数据
    • 3.2 获取注解信息判断内存占用大小
    • 3.2 判断对象占用内存
    • 3.2 gzip压缩json
    • 3.2 判断当前缓存是否为压缩json并对压缩数据进行解压
  • 总结


前言

  如果你在查找一些redis最佳实践或者内存优化的解决方案时常常会在各种繁杂的文章中充斥着大key,热key等字眼,其中的内容也是大径相庭。但是却基本看不到一些实际案例或者代码让你看了之后也云里雾里毕竟喜欢借鉴是我们国人传承至今的优秀美德,今天这个文章就简单分享下我的解决方案跟脱敏代码希望给需要的同学一点帮助。

一、缓存到底是使用String还是hash我该如何选择

  在使用什么类型作为缓存的问题上我想很难有一个标准的答案,因为string和hash可以说各有优点。并且影响我们做出选择的往往是多个方面包括有数据量的大小,代码复杂度,投入回报率等。下面我们简单对比下两者擅长的领域

1.适合用 String 存储的情况:
  每次需要访问大量的字段,存储的结构具有多层嵌套的时候。对于缓存的读取缓存的场景更多,并且更新缓存不频繁(或者每次更新都更新json数据中的大多数key),那么选择string类型作为存储方式会比较好

2.适合用 Hash 存储的情况:
在大多数情况中只需要访问少量字段,自己始终知道哪些字段可用,防止使用 mget 时获取不到想要的数据。对于缓存的更新比较频繁(特别是每次只更新少数几个键)时, 或者我们每次只想取json数据中的少数几个键值时,我们选择hash类型作为我们的存储方式会比较好。

   在经过测试我们发现虽然hash一些情况下确实能减少内存占用,但是与String相比节省的内存微乎其微并没有想象中那么大影响。所以为了避免代码的复杂度我全部都使用了String类型作为缓存,具体选择还需要根据项目实际情况来应对很多时候我们需要避免过度设计。

下面向推荐两个有用的链接有兴趣的同学可以自行研究:
https://segmentfault.com/a/1190000019552836
https://stackoverflow.com/questions/16375188/redis-strings-vs-redis-hashes-to-represent-json-efficiency

二、什么是大key及其影响

   下面我将使用脱敏代码进行演示,这里说明下为了代码的自由度和可控制性实际操作中我并没有采用被很多人推崇的spring cache来进行整合而是采用自定义注解 + AOP的方式进行了一些定制化的开发。

2.1 什么是 Redis 大 key?

对于大Key的定义你如果到网上搜或许能看到许多如下答案:

在这里插入图片描述
-===========================================================================================
在这里插入图片描述
-====================================================================================================
在这里插入图片描述
-============================================================================================================
  其实对于大key的定义在不同的机器配跟业务场景下都是灵活的,这个要在实际环境中亲自去验证。如果数据量不大内存不是很吃紧阈值就可以放宽松些,如果内存比较吃紧string类型10k-xxk要根据响应实践数据处理跟网络传输时间来判断。Redis大key问题的定义及评判准则并非一成不变,而应根据Redis的实际运用以及业务需求来综合评估。例如,在高并发且低延迟的场景中,仅10kb可能就已构成大key;然而在低并发、高容量的环境下,大key的界限可能在100kb。因此,在设计与运用Redis时,要依据业务需求与性能指标来确立合理的大key阈值。

2.2 大key带来的影响

  • 内存占用过高。大Key占用过多的内存空间,可能导致可用内存不足,从而触发内存淘汰策略。在极端情况下,可能导致内存耗尽,Redis实例崩溃,影响系统的稳定性。

  • 性能下降。大Key会占用大量内存空间,导致内存碎片增加,进而影响Redis的性能。对于大Key的操作,如读取、写入、删除等,都会消耗更多的CPU时间和内存资源,进一步降低系统性能。

  • 阻塞其他操作。某些对大Key的操作可能会导致Redis实例阻塞。例如,使用DEL命令删除一个大Key时,可能会导致Redis实例在一段时间内无法响应其他客户端请求,从而影响系统的响应时间和吞吐量。

  • 网络拥塞。每次获取大key产生的网络流量较大,可能造成机器或局域网的带宽被打满,同时波及其他服务。例如:一个大key占用空间是1MB,每秒访问1000次,就有1000MB的流量。

  • 主从同步延迟。当Redis实例配置了主从同步时,大Key可能导致主从同步延迟。由于大Key占用较多内存,同步过程中需要传输大量数据,这会导致主从之间的网络传输延迟增加,进而影响数据一致性。

  • 数据倾斜。在Redis集群模式中,某个数据分片的内存使用率远超其他数据分片,无法使数据分片的内存资源达到均衡。另外也可能造成Redis内存达到maxmemory参数定义的上限导致重要的key被逐出,甚至引发内存溢出。

  • 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。

  • 刷盘当使用 Always 策略的时候,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。当 AOF 日志写入了很多的大 Key,AOF 日志文件的大小会很大,那么很快就会触发 AOF 重写机制。AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 fork() 函数创建一个子进程来处理任务。在创建子进程的过程中,操作系统会把父进程的「页表」复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。这样一来,子进程就共享了父进程的物理内存数据了,这样能够节约物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为只读。随着 Redis 存在越来越多的大 Key,那么 Redis 就会占用很多内存,对应的页表就会越大。在通过 fork() 函数创建子进程的时候,虽然不会复制父进程的物理内存,但是内核会把父进程的页表复制一份给子进程,如果页表很大,那么这个复制过程是会很耗时的,那么在执行 fork 函数的时候就会发生阻塞现象。而且,fork 函数是由 Redis 主线程调用的,如果 fork 函数发生阻塞,那么意味着就会阻塞 Redis 主线程。由于 Redis 执行命令是在主线程处理的,所以当 Redis 主线程发生阻塞,就无法处理后续客户端发来的命令。

三、大key压缩

   首先我们没必要对每个缓存值都做压缩,因为在压缩和解压缩的过程中也会消耗cpu同时也会增加数据处理时间,而且在我之前接触的业务中大key占比相对较小大概只站2百分之10不到。同时压缩后也会破坏数据的可读性,所以没有必要尽量不要对数据压缩处理。

3.1 注解标记可能需要压缩的数据

我们只需要对已知可能出现大key的地方进行是否需要压缩处理的判断

@cache(expire = 60 * 15, isDetectionReduce = true, reduceThresholdValue = 15)

isDetectionReduce为true则开启压缩检测,在注入缓存的时候会判断当前对象是否达到自定义的内存占用阈值,达到则进行压缩处理
reduceThresholdValue 这里是压缩判断阈值,默认单位为kb,意味着当前对象占用内存达到20kb则进行压缩

3.2 获取注解信息判断内存占用大小

 Object target = proceedingJoinPoint.getTarget();
 Method method = target.getClass().getMethod(signature.getName(), signature.getParameterTypes());
 
 RedisCache annotation = method.getAnnotation(RedisCache.class);
 
 int reduceThresholdValue= annotation.reduceThresholdValue();
 boolean isDetectionReduce = annotation.isDetectionReduce ();

3.2 判断对象占用内存

这里我判断的是在java中的内存占用并不是存储在redis后的内存,在转为json后占用空间会有些偏差这里需要大家自行转换。判断内存占用我们可以使用jdk8为我们提供的ObjectSizeCalculator.getObjectSize(result),其实简单点也可以将对象string作为判断依据。这里我选择了ObjectSizeCalculator它本身的效率也是比较高的,如果你没用过可以试着打印下一个int占用的内存空间,它的默认输出单位为byte。一个对象对象头,对齐填充跑去data外的其他部分在64位系统下会占用12byte,所以一个int数据的内存占用是16。

int memoryUsage = ObjectSizeCalculator.getObjectSize(result) - 12
// 如果内存大小大于15k则进行压缩

实际对象如果是15k的话如果转换为json内存会上升一些,因为一些换行对齐等都会占用空间。大概思路就是这样具体操作根绝业务来调整

3.2 gzip压缩json

楼主推荐使用jackson作为序列化框架,切记要配置让jackson携带类信息,否则反序列化可能会出现class java.util.LinkedHashMap cannot be cast to class异常

ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL
                , JsonTypeInfo.As.PROPERTY);
String json = objectMapper.writeValueAsString(result);                

Gzip压缩工具使用jdk自带的就好

 /**
     * 使用gzip压缩字符串
     * GZip压缩 256字节以上才有压缩效果
     * @param str 要压缩的字符串
     * @return 压缩后的字符串
     */

    public static String compress(String str) {

        if (str == null || str.length() <= 0) {

            return str;

        }

        ByteArrayOutputStream out = new ByteArrayOutputStream();

        try (GZIPOutputStream gzip = new GZIPOutputStream(out)) {

            gzip.write(str.getBytes(StandardCharsets.UTF_8));

        } catch (IOException e) {

            log.error("字符串压缩失败str:{},错误信息:{}", str, e.getMessage());

            throw new RuntimeException("字符串压缩失败");

        }

        return Base64.encodeBase64String(out.toByteArray());

    }

    /**
     * 使用gzip解压缩
     *
     * @param compressedStr 压缩字符串
     * @return 解压后的字符串
     */

    public static String uncompress(String compressedStr) {

        if (compressedStr == null || compressedStr.length() <= 0) {

            return compressedStr;

        }

        ByteArrayOutputStream out = new ByteArrayOutputStream();

        ByteArrayInputStream in;

        GZIPInputStream gzip = null;

        byte[] compressed;

        String decompressed;

        try {

            compressed = Base64.decodeBase64(compressedStr);

            in = new ByteArrayInputStream(compressed);

            gzip = new GZIPInputStream(in);

            byte[] buffer = new byte[1024];

            int offset;

            while ((offset = gzip.read(buffer)) != -1) {

                out.write(buffer, 0, offset);

            }

            decompressed = out.toString(StandardCharsets.UTF_8.name());

        } catch (IOException e) {

            log.error("字符串解压失败compressedStr:{},错误信息:{}", compressedStr, e.getMessage());

            throw new RuntimeException("字符串解压失败");

        } finally {

            if (gzip != null) {

                try {

                    gzip.close();

                } catch (IOException ignored) {

                }

            }

            try {

                out.close();

            } catch (IOException ignored) {

            }

        }

        return decompressed;

    }

将压缩后的数据存储到redis就可以了

 String gzip = CompressUtil.compress(json);
 redisTemplate.opsForValue().set(key,gzip,expire, timeUnit);

原来一个10k出头的json压缩后大概只有2k可以看到gzip的效率还是相当高的
在这里插入图片描述

3.2 判断当前缓存是否为压缩json并对压缩数据进行解压

  把字符串转换成byte数组 判断该文件的文件头 GZIP文件头是0x1F 0x8B,也就是bytes[0] == 0x1F&& bytes[1] == 0x8B ,就像我们使用传说中的咖啡baby魔术判断是否为一个java文件一样gzip也有它的标识魔术。一个并不安全合理的方式也可以做到判断的效果,仔细观察你会发现所有压缩后的string都是以H4sIAAAAAAAAA开头的当然这个会受编码跟不同系统等影响并不建议这样判断。

String uncompress = CompressUtil.uncompress(cacheData.toString());
cacheData = objectMapper.readValue(uncompress,Object.class);

最后将解压后的字符串反序列化为java对象返回即可,

总结

  在生产中对大key进行压缩后我发现并没有减少接口响应时间,一个10kb的数据最终响应耗时依然是未压缩前的400-500ms。网络传输节省的时间基本被解压缩的消耗抵消了,不过我们的主要目的已经达到节省了redis的内存消耗并且消除了大key带来的各种隐患。大概的流程就上文都有提到。有问题或不同见解欢迎留言或者私信交流。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/468694.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Reid之损失函数理论学习讲解

基于深度学习的Reid主要流程为输入图像-->CNN(提取特征)-->Global average pooling-->特征向量&#xff0c;将用这些特征来衡量图像的相似情况。并用这些特征进行检索&#xff0c;返回分类情况。 在训练网络的时候需要涉及损失函数&#xff0c;因此就引出了表征学习和…

大数据专业好找工作么

现在&#xff0c;在数字化转型的推动下&#xff0c;越来越多的企业意识到大数据的魅力&#xff0c;并不断在这个领域投入资金&#xff0c;Python大数据开发相关人才也备受青睐&#xff01; 学Python之前&#xff1a;这玩意真有传说中那么好么&#xff1f; 学Python之后&#…

【browser】浏览器跨域处理

好久没有更新博客了&#xff0c;前段时间在疯狂面试&#xff0c;最近工作了才有时间来写博客。 准备来讲讲面试里常问到的跨域处理吧。 说到跨域&#xff0c;我们可能会下意思的说出jsonp&#xff0c;服务端配置cors&#xff0c;node配置代理等&#xff0c;再多了&#xff0c;我…

加密脱胎于去中心化理想,但力求合规 细数各国政府态度之演变

比特币诞生之始&#xff0c;只是极客文化圈内流行的小众货币。如今&#xff0c;加密市场已经发展到无法忽视的程度&#xff0c;虽然全球仍未对加密货币形成共识&#xff0c;但监管已成为各国政府不得不考虑的问题。 美国&#xff1a;监管愈发模糊且不可测 美国始终是加密领域全…

八股文(Mybatis)

文章目录 1. Mybatis缓存机制2. 动态SQL语句3. 分页3. 1 几种分页方式3.2 MyBatis 是如何进行分页的&#xff1f;分页插件的原理是什么&#xff1f;3.3 逻辑分页和物理分页 1. Mybatis缓存机制 作用&#xff1a;避免每次都去查db 一级缓存是SqlSession级别的缓存&#xff0c;也…

Mysql统计分组后每组数据与每组数量区别

下边统计的是分组后每组的数量&#xff1a;结果是多个 select p.id (count(*)) from p group by p.id 如果想统计分组后&#xff0c;有多个分组&#xff0c;需要如下执行&#xff1a; select count(*) from (select p.id (count(*)) from p group by p.id) as a 此外&#xf…

大数据之Hadoop数据仓库Hive

目录&#xff1a; 一、简介二、HQL的执行流程三、索引四、索引案例五、Hive常用DDL操作六、Hive 常用DML操作七、查询结果插入到表八、更新和删除操作待完善。。。 一、简介 Hive 是一个构建在 Hadoop 之上的数据仓库&#xff0c;它可以将结构化的数据文件映射成表&#xff0c…

【Linux】线程-线程安全之互斥

操作系统核心数centos 3.10.032位单核 线程之线程安全 线程不安全的现象互斥死锁线程饥饿 线程不安全的现象 进程线程的背景概念&#xff1a; 临界资源&#xff1a;多线程执行流之间共享的资源 临界区&#xff1a;每个线程内部&#xff0c;访问临界资源的代码 互斥&#xff1a…

设计模式——责任链

目录 1、传统方案&#xff0c;OA系统的采购审批项目 2、职责链模式基本介绍 3、职责链模式解决 OA 系统采购审批项目 4、职责链模式在 SpringMVC 框架应用的源码分析 责任链模式类似一个链表&#xff0c;每个具体处理人层层判断对请求的处理权限&#xff0c;没权限的话把请…

基于C++/CLI实现C#调用C++类对象过程中的注意事项

目录 一、基于C/CLI 的调用原理二、注意事项如何基于VS2010完成上述一系列开发过程1、生成C应用程序&#xff08;非托管代码&#xff09;2、基于C/CLI生成托管代码3、C#调用 三、C/CLI与COM组件对比 一、基于C/CLI 的调用原理 C/CLI &#xff08;Common Language Infrastructu…

阿里P7晒出工资单:狠补了这个,真香...

据阿里HR部门发布的最新信息&#xff0c;2023年招聘岗位数将扩招3000&#xff0b;。但就2022年就业形势来看&#xff0c;大厂缩招裁员导致优质岗位竞争变得更加激烈&#xff0c;2023开年以来&#xff0c;也有不少大厂纷纷传来裁员的消息&#xff01;除了对面试者技术的要求变高…

Netty BIO/NIO/AIO介绍

概念介绍 1、 BIO(blocking I/O):同步阻塞IO,也即是传统的I/O。 2、 NIO (non-blocking IO): 也即是New I/O,使用它可以提供非阻塞式的高伸缩性网络。 3、AIO 即 NIO2.0, 叫做异步不阻塞的 IO。 AIO 引入异步通道的概念, 采用了 Proactor 模式, 简化了程序编写,有…

零拷贝(Zero-Copy)

一&#xff0c;数据的四次拷贝与四次上下文切换 很多应用程序在面临客户端请求时&#xff0c;可以等价为进行如下的系统调用&#xff1a; File.read(file, buf, len);Socket.send(socket, buf, len); 例如消息中间件 Kafka 就是这个应用场景&#xff0c;从磁盘中读取一批消息…

kafka介绍

1.kafka是什么 Kafka是一种高性能、可扩展、容错的分布式流处理平台&#xff0c;广泛应用于日志收集、实时数据处理、消息传递等场景所开发的一个消息队列中间件 2.kafka的优势 Kafka的优势在于其高吞吐量、可扩展性、容错性以及灵活的数据保留策略。它的高吞吐量是因为Kafk…

十一、Node.js

一、Node.js是什么&#xff1f; 在了解Node.js之前&#xff0c;我们先去了解一下什么叫v8引擎。这里参考一下其他博主的资料。 聊聊V8引擎_努力学习前端的77的博客-CSDN博客 这个时候我们再去看下Node.js的定义。 官方对Node.js的定义&#xff1a; Node.js是一个基于V8 Ja…

mysql优化-减少查询回表次数和回表数据量

减少数据回表常见的三种方式分别是1&#xff09;查询条件使用聚集索引&#xff1b;2&#xff09;使用索引下推&#xff1b;3&#xff09;使用索引覆盖。 1 查询条件使用聚集索引-避免回表查询 按照索引使用数据结构B树叶子结点是否包含表中全部字段&#xff0c;mysql 索引可以…

“数字中国·福启海丝”多屏互动光影艺术秀27日在福州举办

作为深化“数字海丝”的核心区、海上丝绸之路的枢纽城市&#xff0c;为喜迎第六届数字中国建设峰会盛大召开之际&#xff0c;福州市人民政府特此举办“数字中国福启海丝”多屏互动光影秀活动。本次光影秀活动是由福建省文化和旅游厅指导&#xff0c;福州市人民政府主办&#xf…

USB转串口芯片CH9101U

CH9101是一个USB总线的转接芯片&#xff0c;实现USB转异步串口。提供了常用的MODEM联络信号&#xff0c;用于为计算机扩展异步串口&#xff0c;或者将普通的串口设备或者MCU直接升级到USB总线。 特点 全速USB设备接口&#xff0c;兼容USB V2.0。内置固件&#xff0c;仿真标准串…

CH9121网络串口透传应用

概述 随着物联网技术的普及&#xff0c;越来越多的传统设备出现联网功能需求。串口作为使用较为广泛的一种通信接口&#xff0c;串口转以太网&#xff0c;进行远程数据传输需求逐渐显现出来。CH9121内部集成TCP/IP协议栈&#xff0c;无需编程&#xff0c;即可轻松实现网络数据…

撰写项目文档: 节省时间的技巧和模板

高质量的项目文档具有长期价值。它不仅有助于确保项目的成功&#xff0c;而且还可以作为未来项目和计划的参考&#xff01; 项目文档是任何项目的脉搏&#xff0c;它连接了成功运行项目所需的一切。 文档必须足够宽泛&#xff0c;以便开发能够取得进展。但要足够灵活&#xf…