服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

Mysql|Sql Server|Oracle|Redis|MongoDB|PostgreSQL|Sqlite|DB2|mariadb|Access|数据库技术|

服务器之家 - 数据库 - Redis - Redis缓存空间优化实践详解

Redis缓存空间优化实践详解

2023-05-22 15:22京东云开发者 Redis

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

导读

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

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

场景设定

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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、用下面的实例作为测试数据

?
1
2
3
4
5
6
7
8
9
10
11
12
13
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");

常规做法

?
1
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-去掉属性名

?
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-使用更好的序列化工具

?
1
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类型替代字符串,会被序列化工具输出更少的字节。

?
1
2
3
4
5
6
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等工具:

?
1
2
3
4
5
<dependency>   
    <groupId>org.msgpack</groupId>   
    <artifactId>msgpack-core</artifactId>   
    <version>0.9.3</version>
</dependency>
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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%以上的属性为空时,需要进行测试,是否仍然适用)**

Redis缓存空间优化实践详解

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

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

• 使用更好的序列化工具

• 使用更小的数据类型

• 考虑使用ZIP压缩

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

以上就是Redis缓存空间优化实践的详细内容,更多关于Redis缓存空间优化的资料请关注服务器之家其它相关文章!

原文链接:https://juejin.cn/post/7222676935147585596

延伸 · 阅读

精彩推荐
  • RedisRedis配置外网可访问(redis远程连接不上)的方法

    Redis配置外网可访问(redis远程连接不上)的方法

    默认情况下,当我们在部署了redis服务之后,redis本身默认只允许本地访问。Redis服务端只允许它所在服务器上的客户端访问,如果Redis服务端和Redis客户端不...

    yin11302022-12-25
  • RedisSpringBoot整合Redis实现序列化存储Java对象的操作方法

    SpringBoot整合Redis实现序列化存储Java对象的操作方法

    这篇文章主要介绍了SpringBoot整合Redis实现序列化存储Java对象,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以...

    Asurplus6652023-03-24
  • RedisRedis Sentinel的使用方法

    Redis Sentinel的使用方法

    这篇文章主要介绍了Redis Sentinel的使用方法,帮助大家更好的理解和学习使用Redis数据库,感兴趣的朋友可以了解下...

    AsiaYe12082021-07-29
  • RedisRedis,就是这么朴实无华

    Redis,就是这么朴实无华

    Redis是2009年发布的,到今天已经超过10岁了。作为必备技能之一,关于它也有聊不完的话题。本文中的任何一个点,都可以展开,完成一篇中等规模的文章...

    小姐姐味道3482020-11-30
  • Redisredis实现共同好友的思路详解

    redis实现共同好友的思路详解

    微信朋友圈大家都玩过吧,那么朋友圈的点赞、评论只能看到自己好友的信息是怎么操作的呢?下面通过本文给大家分享下此功能的实现流程,对redis实现...

    叁滴水3902021-08-06
  • RedisRedis教程(七):Key操作命令详解

    Redis教程(七):Key操作命令详解

    这篇文章主要介绍了Redis教程(七):Key操作命令详解,本文讲解了Key操作命令概述、相关命令列表、命令使用示例等内容,需要的朋友可以参考下 ...

    Redis教程网2552019-10-24
  • Redis使用SpringBoot + Redis 实现接口限流的方式

    使用SpringBoot + Redis 实现接口限流的方式

    这篇文章主要介绍了SpringBoot + Redis 实现接口限流,Redis 除了做缓存,还能干很多很多事情:分布式锁、限流、处理请求接口幂等,文中给大家提到了限流注...

    MrDong先生5792022-10-19
  • RedisRedis Cluster添加、删除的完整操作步骤

    Redis Cluster添加、删除的完整操作步骤

    这篇文章主要给大家介绍了关于Redis Cluster添加、删除的完整操作步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值...

    hsbxxl4052019-11-08