系统城装机大师 - 固镇县祥瑞电脑科技销售部宣传站!

当前位置:首页 > 数据库 > Redis > 详细页面

Redis缓存空间优化实践详解

时间:2023-11-01来源:系统城装机大师作者:佚名

导读

缓存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%以上的属性为空时,需要进行测试,是否仍然适用)**

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

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

• 使用更好的序列化工具

• 使用更小的数据类型

• 考虑使用ZIP压缩

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

以上就是Redis缓存空间优化实践的详细内容,

分享到:

相关信息

系统教程栏目

栏目热门教程

人气教程排行

站长推荐

热门系统下载