redis 优化编码之字符串
字符串优化
字符串对象是redis内部最常用的数据类型。
- 所有的键是字符串对象
- 值对象除了整数之外都是使用字符串存储
lpush cache:type "redis" "tair" "memcache" "leveldb"
创建如上一个链表 需要创建一个链表对象和四个字符串对象。至少需要五个字符串对象。
字符串结构
redis为采用原生C语言类型的字符串结构,拥有自己的字符串结构 ----内部简单动态字符串(simple dynamic string , SDS)
graph LR
subgraph SDS字符串结构体
c[char buf ] -->cexp[字节数组]
end
subgraph SDS字符串结构体
b[int free ] --> bexp[未用字节长度]
end
subgraph SDS字符串结构体
a[int len ] --> aexp[已用字节长度]
end
style aexp fill:#f9f,stroke:#333,stroke-width:2px
style bexp fill:#f9f,stroke:#333,stroke-width:2px
style cexp fill:#f9f,stroke:#333,stroke-width:2px
特点如下:
- [ ] O(1) 时间复杂度获取:字符串长度、已用长度、未用长度
- [ ] 内部实现空间预分配机制 降低再分配
- [ ] 惰性删除机制,字符串缩减后的空间不释放,作为预分配空间保留
预分配机制
字符串存在预分配机制,需要注意预分配带来的内存浪费。
阶段一
graph LR
subgraph SDS字符串结构体
c[char buf ]
end
subgraph SDS字符串结构体
b[free = 0 ]
end
subgraph SDS字符串结构体
a[len = 60 ]
end
c -->cexp[used+free+\O=61字节]
style cexp fill:#f9f,stroke:#333,stroke-width:2px
这里忽略int类型和free字段消耗的8字节
阶段二
graph LR
subgraph SDS字符串结构体
c[char buf ]
end
subgraph SDS字符串结构体
b[free = 120 ]
end
subgraph SDS字符串结构体
a[len = 120 ]
end
c -->cexp[used+free+\O=241字节]
style cexp fill:#f9f,stroke:#333,stroke-width:2px
追加操作后字符串对象预分配一倍容量作为预留空间-大量追加操作内存重分配造成内存碎片
阶段三
graph LR
subgraph SDS字符串结构体
c[char buf ]
end
subgraph SDS字符串结构体
b[free = 0 ]
end
subgraph SDS字符串结构体
a[len = 120 ]
end
c -->cexp[used+free+\O=121字节]
style cexp fill:#f9f,stroke:#333,stroke-width:2px
降低一倍的空间同时碎片率也降低了
测试用例环境:
- redis 3.2.8
- mem_allocator libc
- maxmemory 5G
- model standalone
数据量阶段操作说明命令内存内存碎片率
200W阶段1新插入200W数据set367.33M1.79
阶段2在阶段1的基础上追加1倍数据append760.21M1.62
阶段3重新插入200W数据 value是之前的1倍set500.84M1.72
数据量阶段操作说明命令内存内存碎片率
400W阶段1新插入400W数据set690.91M1.8
阶段2在阶段1的基础上追加1倍数据append1.45G1.63
阶段3重新插入400W数据 value是之前的1倍set961.76M2.48
数据量阶段操作说明命令内存内存碎片率
600W阶段1新插入600W数据set1.00G1.8
阶段2在阶段1的基础上追加1倍数据append2.16G1.63
阶段3重新插入600W数据 value是之前的1倍set1.40G2.48
数据量阶段操作说明命令内存内存碎片率
800W阶段1新插入800W数据set1.3G1.31
阶段2在阶段1的基础上追加1倍数据append2.85G1.65
阶段3重新插入800W数据 value是之前的1倍set1.831.19
数据量阶段操作说明命令内存内存碎片率
1000W阶段1新插入1000W数据set1.66G1.8
阶段2在阶段1的基础上追加1倍数据append3.60G1.63
阶段3重新插入1000W数据 value是之前的1倍set2.84G1.46
上述进行了200W、400W、600W、800W、1000W 数据量的 测试,内存以及内存碎片率
同样的数据追加后内存的消耗非常严重。
字符串预分配的方式 是为防止修改数据操作不断重分配内存和字节数据
造成内存浪费,并不是每次都是翻倍
- 第一次创建len 等于数据实际大小 free等于0, 不做预分配
- 修改后 free空间不足,且数据小于1M,则分配1倍(len=60bytes,free=0,再追加60bytes 预分配120bytes 。总空间就是60+60+120+ 1 bytes)
- 修改后free空间不足,且数据大约1M 则每次预分配1M (len=30MB ,free=0,追加100bytes 预分配1MB 总空间 30MB+100bytes+1MB + 1bytes)
总结
尽量减少字符串频繁修改操作 如append,setrange
改为直接使用set修改字符串
- 降低预分配带来的内存浪费和碎片