缓存空间优化实践

导读

缓存 Redis,是我们最常用的服务,其适用场景广泛,被大量应用到各业务场景中。也正因如此,缓存成为了重要的硬件成本来源,我们有必要从空间上做一些优化,降低成本的同时也会提高性能。

下面以我们的案例说明,将缓存空间减少 70% 的做法。

场景设定

1、我们需要将 POJO 存储到缓存中,该类定义如下

public class TestPOJO implements Serializable {
    private String testStatus;
    private String userPin;
    private String investor;
    private Date testQueryTime;
    private Date createTime;
    private String bizInfo;
    private Date otherTime;
    private BigDecimal userAmount;
    private BigDecimal userRate;
    private BigDecimal applyAmount;
    private String type;
    private String checkTime;
    private String preTestStatus;
    
    public Object[] toValueArray(){
        Object[] array = {testStatus, userPin, investor, testQueryTime,
                createTime, bizInfo, otherTime, userAmount,
                userRate, applyAmount, type, checkTime, preTestStatus};
        return array;
    }
    
    public CreditRecord fromValueArray(Object[] valueArray){         
        //具体的数据类型会丢失,需要做处理
    }
}

2、用下面的实例作为测试数据

TestPOJO pojo = new TestPOJO();
pojo.setApplyAmount(new BigDecimal("200.11"));
pojo.setBizInfo("XX");
pojo.setUserAmount(new BigDecimal("1000.00"));
pojo.setTestStatus("SUCCESS");
pojo.setCheckTime("2023-02-02");
pojo.setInvestor("ABCD");
pojo.setUserRate(new BigDecimal("0.002"));
pojo.setTestQueryTime(new Date());
pojo.setOtherTime(new Date());
pojo.setPreTestStatus("PROCESSING");
pojo.setUserPin("ABCDEFGHIJ");
pojo.setType("Y");

常规做法

System.out.println(JSON.toJSONString(pojo).length());

使用 JSON 直接序列化、打印 length=284这种方式是最简单的方式,也是最常用的方式,具体数据如下:

{"applyAmount":200.11,"bizInfo":"XX","checkTime":"2023-02-02","investor":"ABCD","otherTime":"2023-04-10 17:45:17.717","preCheckStatus":"PROCESSING","testQueryTime":"2023-04-10 17:45:17.717","testStatus":"SUCCESS","type":"Y","userAmount":1000.00,"userPin":"ABCDEFGHIJ","userRate":0.002}

我们发现,以上包含了大量无用的数据,其中属性名是没有必要存储的。

改进 1 - 去掉属性名

System.out.println(JSON.toJSONString(pojo.toValueArray()).length());

通过选择数组结构代替对象结构,去掉了属性名,打印 length=144,将数据大小降低了 50%,具体数据如下:

["SUCCESS","ABCDEFGHIJ","ABCD","2023-04-10 17:45:17.717",null,"XX","2023-04-10 17:45:17.717",1000.00,0.002,200.11,"Y","2023-02-02","PROCESSING"]

我们发现,null 是没有必要存储的,时间的格式被序列化为字符串,不合理的序列化结果,导致了数据的膨胀,所以我们应该选用更好的序列化工具。

改进 2 - 使用更好的序列化工具

//我们仍然选取JSON格式,但使用了第三方序列化工具
System.out.println(new ObjectMapper(new MessagePackFactory()).writeValueAsBytes(pojo.toValueArray()).length);

选取更好的序列化工具,实现字段的压缩和合理的数据格式,打印 length=92,空间比上一步又降低了 40%。

这是一份二进制数据,需要以二进制操作 Redis,将二进制转为字符串后,打印如下:

��SUCCESS�ABCDEFGHIJ�ABCD��j�6���XX��j�6����?`bM����@i��Q�Y�2023-02-02�PROCESSING

顺着这个思路再深挖,我们发现,可以通过手动选择数据类型,实现更极致的优化效果,选择使用更小的数据类型,会获得进一步的提升。

改进 3 - 优化数据类型

在以上用例中,testStatus、preCheckStatus、investor 这 3 个字段,实际上是枚举字符串类型,如果能够使用更简单数据类型(比如 byte 或者 int 等)替代 string,还可以进一步节省空间。其中 checkTime 可以用 Long 类型替代字符串,会被序列化工具输出更少的字节。

public Object[] toValueArray(){
    Object[] array = {toInt(testStatus), userPin, toInt(investor), testQueryTime,
    createTime, bizInfo, otherTime, userAmount,
    userRate, applyAmount, type, toLong(checkTime), toInt(preTestStatus)};
    return array;
}

在手动调整后,使用了更小的数据类型替代了 String 类型,打印 length=69

改进 4 - 考虑 ZIP 压缩

除了以上的几点之外,还可以考虑使用 ZIP 压缩方式获取更小的体积,在内容较大或重复性较多的情况下,ZIP 压缩的效果明显,如果存储的内容是 TestPOJO 的数组,可能适合使用 ZIP 压缩。

但 ZIP 压缩并不一定会减少体积,在小于 30 个字节的情况下,也许还会增加体积。在重复性内容较少的情况下,无法获得明显提升。并且存在 CPU 开销。

在经过以上优化之后,ZIP 压缩不再是必选项,需要根据实际数据做测试才能分辨到 ZIP 的压缩效果。

最终落地

上面的几个改进步骤体现了优化的思路,但是反序列化的过程会导致类型的丢失,处理起来比较繁琐,所以我们还需要考虑反序列化的问题。

在缓存对象被预定义的情况下,我们完全可以手动处理每个字段,所以在实战中,推荐使用手动序列化达到上述目的,实现精细化的控制,达到最好的压缩效果和最小的性能开销。

可以参考以下 msgpack 的实现代码,以下为测试代码,请自行封装更好的 Packer 和 UnPacker 等工具:

<dependency>    
    <groupId>org.msgpack</groupId>    
    <artifactId>msgpack-core</artifactId>    
    <version>0.9.3</version>
</dependency>
    public byte[] toByteArray() throws Exception {
        MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
        toByteArray(packer);
        packer.close();
        return packer.toByteArray();
    }

    public void toByteArray(MessageBufferPacker packer) throws Exception {
        if (testStatus == null) {
            packer.packNil();
        }else{
            packer.packString(testStatus);
        }

        if (userPin == null) {
            packer.packNil();
        }else{
            packer.packString(userPin);
        }

        if (investor == null) {
            packer.packNil();
        }else{
            packer.packString(investor);
        }

        if (testQueryTime == null) {
            packer.packNil();
        }else{
            packer.packLong(testQueryTime.getTime());
        }

        if (createTime == null) {
            packer.packNil();
        }else{
            packer.packLong(createTime.getTime());
        }

        if (bizInfo == null) {
            packer.packNil();
        }else{
            packer.packString(bizInfo);
        }

        if (otherTime == null) {
            packer.packNil();
        }else{
            packer.packLong(otherTime.getTime());
        }

        if (userAmount == null) {
            packer.packNil();
        }else{
            packer.packString(userAmount.toString());
        }

        if (userRate == null) {
            packer.packNil();
        }else{
            packer.packString(userRate.toString());
        }

        if (applyAmount == null) {
            packer.packNil();
        }else{
            packer.packString(applyAmount.toString());
        }

        if (type == null) {
            packer.packNil();
        }else{
            packer.packString(type);
        }

        if (checkTime == null) {
            packer.packNil();
        }else{
            packer.packString(checkTime);
        }

        if (preTestStatus == null) {
            packer.packNil();
        }else{
            packer.packString(preTestStatus);
        }
    }


    public void fromByteArray(byte[] byteArray) throws Exception {
        MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(byteArray);
        fromByteArray(unpacker);
        unpacker.close();
    }

    public void fromByteArray(MessageUnpacker unpacker) throws Exception {
        if (!unpacker.tryUnpackNil()){
            this.setTestStatus(unpacker.unpackString());
        }
        if (!unpacker.tryUnpackNil()){
            this.setUserPin(unpacker.unpackString());
        }
        if (!unpacker.tryUnpackNil()){
            this.setInvestor(unpacker.unpackString());
        }
        if (!unpacker.tryUnpackNil()){
            this.setTestQueryTime(new Date(unpacker.unpackLong()));
        }
        if (!unpacker.tryUnpackNil()){
            this.setCreateTime(new Date(unpacker.unpackLong()));
        }
        if (!unpacker.tryUnpackNil()){
            this.setBizInfo(unpacker.unpackString());
        }
        if (!unpacker.tryUnpackNil()){
            this.setOtherTime(new Date(unpacker.unpackLong()));
        }
        if (!unpacker.tryUnpackNil()){
            this.setUserAmount(new BigDecimal(unpacker.unpackString()));
        }
        if (!unpacker.tryUnpackNil()){
            this.setUserRate(new BigDecimal(unpacker.unpackString()));
        }
        if (!unpacker.tryUnpackNil()){
            this.setApplyAmount(new BigDecimal(unpacker.unpackString()));
        }
        if (!unpacker.tryUnpackNil()){
            this.setType(unpacker.unpackString());
        }
        if (!unpacker.tryUnpackNil()){
            this.setCheckTime(unpacker.unpackString());
        }
        if (!unpacker.tryUnpackNil()){
            this.setPreTestStatus(unpacker.unpackString());
        }
    }

场景延伸

假设,我们为 2 亿用户存储数据,每个用户包含 40 个字段,字段 key 的长度是 6 个字节,字段是分别管理的。

正常情况下,我们会想到 hash 结构,而 hash 结构存储了 key 的信息,会占用额外资源,字段 key 属于不必要数据,按照上述思路,可以使用 list 替代 hash 结构。

通过 Redis 官方工具测试,使用 list 结构需要 144G 的空间,而使用 hash 结构需要 245G 的空间(当 50% 以上的属性为空时,需要进行测试,是否仍然适用)

在以上案例中,我们采取了几个非常简单的措施,仅仅有几行简单的代码,可降低空间 70% 以上,在数据量较大以及性能要求较高的场景中,是非常值得推荐的。:

• 使用数组替代对象(如果大量字段为空,需配合序列化工具对 null 进行压缩)

• 使用更好的序列化工具

• 使用更小的数据类型

• 考虑使用 ZIP 压缩

• 使用 list 替代 hash 结构(如果大量字段为空,需要进行测试对比)

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Git】Gitee免密push(TencentCloudLinux)

前提&#xff1a; 我用的是腾讯云的Centos(Linux)服务器 我创建好了仓库 我配置过git 可以正常用密码push 以上自行解决 我们直接配置公钥解决免密push 1.在服务器上创建公钥 在用户根目录创建 公钥 邮箱写自己的 随意写 我写的是gitee绑定的邮箱 ssh-keygen -t ed25519 -C…

数据结构(六)—— 二叉树(2)遍历

文章目录 递归三要素一、深度优先遍历&#xff08;前中后序&#xff09;1.1 递归遍历1.1.1 前序&#xff08;中左右&#xff09;1.1.2 中序&#xff08;左中右&#xff09;1.1.3 后序&#xff08;左右中&#xff09; 1.2 迭代遍历1.2.1 前序1.2.2 后序1.2.3 中序 二、广度优先遍…

Renesas瑞萨A4M2和STM32 CAN通信

刚好拿到一块瑞萨开发板&#xff0c;捣鼓玩下CAN&#xff0c;顺便试下固件升级。 A4M2 工程创建 详细可以参考&#xff0c;我之前写的文章 Renesa 瑞萨 A4M2 移植文件系统FAT32 CAN0 配置信息&#xff0c;使能FIFO&#xff0c;接收标准帧 ID为0x50&#xff0c;数据帧。 代…

密码学【java】初探究加密方式之对称加密

文章目录 一 常见加密方式二 对称加密2.1 Cipher类简介2.2 Base算法2.3 补充&#xff1a;Byte&bit2.4 DES加密演示2.5 DES解密2.6 补充&#xff1a;对于IDEA控制台乱码的解决方法2.7 AES加密解密2.8 补充&#xff1a; toString()与new String ()用法区别2.9 加密模式2.9.1 …

内网渗透(六十二)之 NTLM Realy 攻击

NTLM Realy 攻击 NTLM Realy 攻击其实应该称为Net-NTLM Realy 攻击,它发生在NTLM认证的第三步,在Response 消息中存在Net-NTLM Hash,当攻击者获得了 Net-NTLM Hash 后,可以重放Net-NTLM Hash 进行中间人攻击。 NTLM Realy 流程如图所示,攻击者作为中间人在客户端和服务器…

【C++】异常,你了解了吗?

在之前的C语言处理错误时&#xff0c;会通过assert和错误码的方式来解决&#xff0c;这导致了发生错误就会直接把程序关闭&#xff0c;或者当调用链较长时&#xff0c;就会一层一层的去确定错误码&#xff0c;降低效率&#xff0c;所以c针对处理错误&#xff0c;出现了异常&…

奥斯汀独家对话|从机构的「拉扯」中成长的美国加密监管

‍前言 4月25日&#xff0c;在美国得克萨斯州的首府奥斯汀&#xff0c;这座充满活力和创造力的城市&#xff0c;欧科云链研究院与来自哥伦比亚商学院的Austin Campbell教授就美国加密监管以及其相关话题进行了一次深入探讨。双方讨论了美国整体的监管问题、监管逻辑、最新的稳…

git把我本地文件传到我的指定的仓库

在使用Git将本地文件推送到指定仓库之前&#xff0c;请确保已经安装了Git并进行了基本配置。接下来&#xff0c;遵循以下步骤将本地文件推送到远程仓库&#xff1a; 兄弟先赏析悦目一下&#xff0c;摸个鱼 首先&#xff0c;在本地文件夹中打开命令行界面&#xff08;在Windows上…

SpringBoot整合Mybatis-Plus、Jwt实现登录token设置

Spring Boot整合Mybatis-plus实现登录常常需要使用JWT来生成用户的token并设置用户权限的拦截器。本文将为您介绍JWT的核心讲解、示例代码和使用规范&#xff0c;以及如何实现token的生成和拦截器的使用。 一、JWT的核心讲解 JWT&#xff08;JSON Web Token&#xff09;是一种…

【P1】Jmeter 准备工作

文章目录 一、Jmeter 介绍1.1、Jmeter 有什么样功能1.2、Jmeter 与 LoadRunner 比较1.3、常用性能测试工具1.4、性能测试工具如何选型1.5、学习 Jmeter 对 Java 编程的要求 二、Jmeter 软件安装2.1、官网介绍2.2、JDK 安装及环境配置2.3、Jmeter 三种模式2.4、主要配置介绍2.4.…

外卖项目优化-02-

文章目录 瑞吉外卖项目优化-Day02课程内容前言1. MySQL主从复制1.1 介绍1.2 搭建1.2.1 准备工作1.2.2 主库配置1.2.3 从库配置 1.3 测试 2. 读写分离案例2.1 背景介绍2.2 ShardingJDBC介绍2.3 数据库环境2.4 初始工程导入2.5 读写分离配置2.6 测试 3. 项目实现读写分离3.1 数据…

HTML(四) -- 多媒体设计

目录 1. 视频标签 2. 音频标签 3. 资源标签&#xff08;定义媒介资源 &#xff09; 1. 视频标签 属性值描述autoplayautoplay如果出现该属性&#xff0c;则视频在就绪后马上播放。controlscontrols表示添加标准的视频控制界面&#xff0c;包括播放、暂停、快进、音量等…

【LLM】离线部署ChatGLM-6B模型

目录 前言 准备环境 打包环境 下载/上传模型 部署模型 前言 甲方出手&#xff0c;天下我有&#x1f929;。圆梦了圆梦了~一直想整一台GPU服务器尝尝鲜&#xff0c;奈何钱包空空&#xff0c;虽然有可以在CPU上部署的方案&#xff0c;但效果却不是让人那么满意&#xff0c…

跟着杰哥学强化学习:多臂老虎机问题

多臂老虎机问题 现在有3台外观一模一样的老虎机,每个老虎机的赔率是不同的,摇动一次需要1块钱,现在给你100块钱,如何获取最大的收益。 如果我们知道了每个老虎的赔率,那么只要选择收益最高的那个老虎机就可以了,但现在问题是并不知道每个老虎机的收益。为了简单,我们假…

忆暖行动|“他一个人推着老式自行车在厚雪堆的道路上走,车上带着学生考试要用的司机”

忆暖行动|“他一个人推着老式自行车在厚雪堆的道路上走&#xff0c;车上带着学生考试要用的sj” 一头白发&#xff0c;满山青葱 在那斑驳的物件褶皱中&#xff0c;透过泛黄的相片&#xff0c;掩藏着岁月的冲刷和青葱的时光。曾经的青年早已经不复年轻&#xff0c;但是那份热爱…

Nachos系统的上下文切换

Fork调用创建进程 在实验1中通过gdb调试初步熟悉了Nahcos上下文切换的基本流程&#xff0c;但这个过程还不够清晰&#xff0c;通过源码阅读进一步了解这个过程。 在实验1中通过执行Threadtest&#xff0c;Fork创建子进程&#xff0c;并传入SimpleThread执行currentThread->…

MySQL学习笔记第七天

第07章单行函数 2. 数值函数 2.4 指数函数、对数函数 函数用法POW(x,y)&#xff0c;POWER(X,Y)返回x的y次方EXP(X)返回e的x次方&#xff0c;其中e是一个常数&#xff0c;2.718281828459045LN(X)&#xff0c;LOG(X)返回以e为底的X的对数&#xff0c;当x<0时&#xff0c;返…

Linux Ansible任务控制(循环判断、处理程序、失败任务)

目录 Ansible的Loop循环 简单的Loop循环 数组列表方式的Loop循环 字典方式的Loop循环 基于外部变量的Loop循环 Ansible的When判断 通过魔法变量、事实变量作为条件 通过剧本执行结果的变量来作为条件 Ansible处理程序 Ansible处理失败任务 处理失败任务ignore_error…

Stable Diffusion 本地部署教程不完全指南

ChatGPT免费体验入口网址 http://chat.xutongbao.top 参考链接&#xff1a; ERROR: Could not find a version that satisfies the requirement torch1.7.0 ERROR: No matching……_congcongiii的博客-CSDN博客 下载链接&#xff1a; https://download.pytorch.org/whl/cu11…

KeepChatGPT插件-提效神器,解决ChatGPT报错!

KeepChatGPT插件-提效神器&#xff0c;解决ChatGPT报错&#xff01; 文章目录 KeepChatGPT插件-提效神器&#xff0c;解决ChatGPT报错&#xff01;一、错误提示关于 为何会出现大规模地网络错误二、解决方案三、安装步骤电脑端使用1.Chrome浏览器安装2.Firefox浏览器安装 四、使…