Redis 4.x系列(十二):Redis 使用合适数据类型(优化)

  Redis 为满足业务需求提供了丰富的数据类型,在使用时需要注意它们在不同业务场景中的优缺点,还需要考虑选中的数据类型在性能和内存消耗上是否还有优化的空间。

  Redis 对小的聚合类型数据进行了特殊的编码处理。Redis2.2版本及以后,存储集合数据的时候会采用内存压缩技术,以使用更少的内存存储更多的数据。当集合中的元素小于给定的个数,或元素小于给定的最大值时,Redis 会以一种非常有效的内存方式进行编码,最多可节省10倍的内存(平均至少节省5倍)。

  Redis Memory Optimization译文:内存优化

这种编码技术对用户和 Redis API 是透明的,是一种对 CPU/内存 的权衡(CPU 换内存/时间换空间),可在redis.conf配置文件中调修改最大元素个数最大元素值。如果超过了设置的最大值,Redis 将自动把它(集合)转换为正常的散列表。这种操作对于较小的值是非常快的。

1
2
3
4
5
6
7
hash-max-zipmap-entries 512 (hash-max-ziplist-entries for Redis >= 2.6)
hash-max-zipmap-value 64 (hash-max-ziplist-value for Redis >= 2.6)
list-max-ziplist-entries 512
list-max-ziplist-value 64
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
set-max-intset-entries 512

内存优化方式

使用 32位 Redis

如果使用 32位 的 Redis, 每个键占用的内存会少得多,因为指针很小,但能使用的最大内存被限制为 4 GB。Redis 的持久化策略RDB文件和AOF兼容 32 位和 64位, 也就是可以用 64 位 Redis 恢复 32位的 RDBAOF 文件 。

位和字节级别的操作

Redis 2.2引入了新的位和字节级操作:GETRANGE,SETRANGE,GETBITSETBIT。 使用这些命令可以将Redis字符串类型视为随机访问数组。

例如:一个应用程序,其中用户ID 是一个连续的数字,可以使用位图来保存有关用户性别的信息,使用 1 表示男性,0 表示女性,或者反过来(位图只能用 0 或 1 来表示)。 1 亿个用户时也只占用 12M 的内存。 还可以使用 GETRANGESETRANGE 执行相同操作,为每个用户存储一个字节的信息。 这只是一个例子,实际上可以使用这些原始数据类型在较小的内存中模拟许多问题。(100000000 / 1024 /1024 /8 = 11.92M)

尽可能使用 hashes

小散列表(是说散列表里面存储的数少)占用非常少的内存,应该尽可能的使用 hashes 来存储数据。

例如:将多个字段的用户对象数据使用哈希来存储(存储对象),而不是存储每一个字段。

使用 hashes 高效存储抽象的键值对

一般而言,把一个模型(model)表示为 key-value 的形式存储在redis中非常容易,当然value必须为字符串,这样存储不仅比一般的key value存储高效,并且比 memcached 存储还高效。

一些 key 存储了一个对象的多个字段要比一个散列表存储对象的多个字段占用更多的内存。当散列表非常小时,Redis 采用的编码为一个 O(N) 的数据结构,在使用HGETHSET命令的复杂度仍然为O(1), 当散列表包含的元素增长太多时,散列表转被转换为正常的散列表。散列表的极限值可在redis.conf文件中配置,如下。

1
2
3
4
//元数个数
hash-max-zipmap-entries 256
//key和value 长度
hash-max-zipmap-value 1024

HASH 列表的field 和 value并不是一个具有完整功能的Redis 对象,所以 HASH 的field不能像普通的 KEY 一样设置过期时间,但不影响对散列表的使用。

所以散列表使用内存的效率很高的,当使用散列表来存储对象,或对多个字段进行抽象建模,这非常有用。

例如数据 object:1234,可将 key 拆成两部分,第一部分当作一个key,第二部分当前散列表字段,这样就组合成了散列表的数据结构(key field value)

1
2
3
4
key:object12
field:34

HSET object12 34 value

每次哈希超过指定的元数数量或元素大小时,它将被转换为常规的哈希表,此时就会丢失节约内存的特性。

备注:这小节看文字不太容易理解,简单的理解就是把一组普通的对象或数据,拆成 HASH 结构(key field value),高大上的描述叫抽象出数据模型

内存分配

当设置了maxmemory后,Redis 会分配几乎和 maxmemory一样大的内存(然而可能还有少量的其它内存分本)。

具体的值可以在配置文件中设置,或者在启动后通过CONFIG SET命令设置(see Using memory as an LRU cache for more info)

Redis 在内存管理方面,注意以下事项:

  1. 当删除 KEY 后,内存并不会总是释放回给操作系统,这是由操作系统(Linux)的**malloc()**函数的特性,redis 使用的底层内存分配不会简单的就把内存归还给操作系统,很大部分是因为删除的 KEY 和 其它未删除的 KEY 在同一个内存页上,这样就无法把整个内存页归还给操作系统。
  2. 根据峰值内存(可能会用到的最大内存)使用情况来配置内存,如果你的工作负载可能用到 10GB,即使通常情况下只用 5GB ,也需要配置 10GB。
  3. 内存分配器是智能的,能够复用释放的内存块。例如,当你从 5GB 的数据集中释放 2GB的数据(删除),当再次添加 KEYS 时,可以看到RSS保持稳定并且不会增加更多,因为添加 2GB 的 KEYS 时,会复用之前(逻辑上)释放的内存。
  4. 因为这些,当可能达到的峰值内存远远大于通常使用的内存时,内存的碎片率就变的不稳定。内存碎片计算为当前使用的内存(used_memory)除以实际分配的内存(used_memory_rss)就是 fragmentation 。因为RSS反映了峰值了内存(peak),当释放了很多键/值使用的内存很少,但RSS很高,此时 mem_used/ RSS 就很高。 备注:官方英文说明的计算难以理解。个人理解:Redis 的内存有三个值,分别是当前使用的内存(used_memory),分配的总内存(used_memory_rss),峰值使用的内存(used_memory_peak),三个值的排序是:used_memory_rss > used_memory_peak > used_memory; 要计算内存碎片应该是(used_memory_peak - used_memory)。

如果没有设置maxmemory,Redis 将持续向操作系统申请内存直到占用所有可用内存。因此,通常建议配置以作限制,可能会因为内存不足返回错误导致程序出错,但不会由于内存不足导致整个机器宕机。你可能还想将maxmemory-policy设置为noeviction(这不是某些旧版本的默认值)。

HASH 节省内存示例

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
public class ActorServiceImpl implements ActorService {

private static final Logger logger = LogManager.getLogger(ActorServiceImpl.class);

@Autowired
private RedisTemplate redisTemplate;

public void saveActorToHash() {
Actor actor = new Actor();
for (Long i = 0l; i < 10000; i++) {
actor.setActorId(i).setFirstName("first_" + i)
.setLastName("last_" + i).setLastUpdate(new Date());
/*redisTemplate.opsForHash().put("actor" + i, "lastName",actor.getLastName());
redisTemplate.opsForHash().put("actor" + i, "firstName",actor.getFirstName());
redisTemplate.opsForHash().put("actor" + i, "lastUpdate",actor.getLastUpdate());*/

//每个KEY 存1000个字段; 10000条数据占内存 1.77M
Long a = i/1000;
redisTemplate.opsForHash().put("actor:" + a, String.valueOf(i), actor);
}
}

public void saveToHashTest() {
String nickName1 = null;
String nickName2 = null;
String nickName3 = null;
int profile_id = 0;

for (int i = 0; i < 50000; i++) {
nickName1 = "Tom";
nickName2 = "Kitty";
nickName3 = "Andy";

nickName1 = nickName1 + i;
nickName2 = nickName2 + i;
nickName3 = nickName3 + i;
profile_id = i;

//1.4章节:对 hashs 进行抽像建模(数据拆分)。根据ID来分区
String digest = DigestUtils.md5DigestAsHex((nickName1 + nickName2 + nickName3).getBytes());
String key = digest.substring(0, 2);

redisTemplate.opsForHash().put(key, nickName1, String.valueOf(profile_id));
redisTemplate.opsForHash().put(key, nickName2, String.valueOf(profile_id));
redisTemplate.opsForHash().put(key, nickName3, String.valueOf(profile_id));

/*
设置:hash-max-zipmap-entries 1000
5万条数据占内存3.90M
取三个呢称的md5的前两位作为KEY,就有256个key(16*16)
将每个呢称作为key 的 field,profile_id作为 value
*/
}
}
}

相关参考

Optimising session key storage in Redis
Storing hundreds of millions of simple key-value pairs in Redis

Redis 4.x系列(十二):Redis 使用合适数据类型(优化)

http://blog.gxitsky.com/2018/11/03/Redis-12-use-correct-data-type/

作者

光星

发布于

2018-11-03

更新于

2022-08-14

许可协议

评论