Redis 小对象压缩

297 阅读3分钟

概述

Redis 数据是保存再数据库,是非常耗内存的。所以,内存对于 Redis 来说极为重要。Redis 有很多内存的优化点,据说这些优化是牺牲代码可读性为代价。

32bit VS 64bit

如果你的Redis使用内存很小,不超过4G,可以考虑使用 32bit 进行编译,32bit 编译内部所有的数据结构使用的指针空间占用会少一半,如果内存不足可以通过增加实例来实现。

小对象压缩存储(ziplist)

如果 Redis 内部管理的集合数据结构很小,它会使用紧凑存储形式压缩存储。

这就好比 HashMap 本来是二维结构,但是如果内部元素比较少,使用二维结构反而浪费空间,还不如使用一维数组进行存储,需要查找时,因为元素少进行遍历也很快,甚至可以比 HashMap 本身的查找还要快。比如下面我们可以使用数组来模拟 HashMap 的增删改操作。

public class ArrayMap<K, V> {

  private List<K> keys = new ArrayList<>();
  private List<V> values = new ArrayList<>();

  public V put(K k, V v) {
    for (int i = 0; i < keys.size(); i++) {
      if (keys.get(i).equals(k)) {
        V oldv = values.get(i);
        values.set(i, v);
        return oldv;
      }
    }
    keys.add(k);
    values.add(v);
    return null;
  }

  public V get(K k) {
    for (int i = 0; i < keys.size(); i++) {
      if (keys.get(i).equals(k)) {
        return values.get(i);
      }
    }
    return null;
  }

  public V delete(K k) {
    for (int i = 0; i < keys.size(); i++) {
      if (keys.get(i).equals(k)) {
        keys.remove(i);
        return values.remove(i);
      }
    }
    return null;
  }
}

Redis 的 ziplist 是一个紧凑的字节数组结构,如下图所示,每个元素之间都是紧挨着的。

image.png

如果它存储的是 hash 结构,那么 key 和 value 会作为两个 entry 相邻存在一起。

127.0.0.1:6379> hset score math 99
(integer) 1
127.0.0.1:6379> hset score english 80
(integer) 1
127.0.0.1:6379> object encoding score
"ziplist"

如果它存储的是 zset,那么 value 和 score 会作为两个 entry 相邻存在一起。

127.0.0.1:6379> zadd code 1 A
(integer) 1
127.0.0.1:6379> zadd code 2 B
(integer) 1
127.0.0.1:6379> object encoding code
"ziplist"

intset

Redis 的 intset 是一个紧凑的整数数组结构,它用于存放元素都是整数的并且元素个数较少的 set 集合。

如果整数可以用 uint16 表示,那么 intset 的元素就是 16 位的数组,如果新加入的整数超过了 uint16 的表示范围,那么就使用 uint32 表示,如果新加入的元素超过了 uint32 的表示范围,那么就使用 uint64 表示,Redis 支持 set 集合动态从 uint16 升级到 uint32,再升级到 uint64。

image.png

127.0.0.1:6379> sadd number 1 2 3 4
(integer) 4
127.0.0.1:6379> object encoding number
"intset"

hashtable

如果 set 里存储的是字符串,那么 sadd 立即升级为 hashtable 结构。还记得 Java 的 HashSet 么,它内部是使用 HashMap 实现的。

127.0.0.1:6379> sadd str a b c d
(integer) 4
127.0.0.1:6379> object encoding str
"hashtable"

存储界限 当集合对象的元素不断增加,或者某个 value 值过大,这种小对象存储也会被升级为标准结构。Redis 规定在小对象存储结构的限制条件如下:

hash-max-ziplist-entries 512  # hash 的元素个数超过 512 就必须用标准结构存储
hash-max-ziplist-value 64  # hash 的任意元素的 key/value 的长度超过 64 就必须用标准结构存储
list-max-ziplist-entries 512  # list 的元素个数超过 512 就必须用标准结构存储
list-max-ziplist-value 64  # list 的任意元素的长度超过 64 就必须用标准结构存储
zset-max-ziplist-entries 128  # zset 的元素个数超过 128 就必须用标准结构存储
zset-max-ziplist-value 64  # zset 的任意元素的长度超过 64 就必须用标准结构存储
set-max-intset-entries 512  # set 的整数元素个数超过 512 就必须用标准结构存储

比如:

  • hash 的元素个数超过 512 就必须用标准结构存储
public class ZipListTest {

    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        jedis.del("numKey");
        // 第一次插入512
        for (int i = 0; i < 512; i++) {
            jedis.hset("numKey", String.valueOf(i), String.valueOf(i));
        }
        System.out.println(jedis.objectEncoding("numKey"));
        // 再插入一个元素
        jedis.hset("numKey", "512", "512");
        System.out.println(jedis.objectEncoding("numKey"));
    }
}

输出:

ziplist
hashtable
  • hash 的任意元素的 key/value 的长度超过 64 就必须用标准结构存储
public static void main(String[] args) {
    Jedis jedis = new Jedis("localhost", 6379);
    jedis.del("numKey");
    // 第一次插入
    String value = "0";
    for (int i = 0; i < 64; i++) {
        jedis.hset("numKey", String.valueOf(i), value);
        value = value.concat("0");
    }

    System.out.println(jedis.objectEncoding("numKey"));
    System.out.println(value.length());
    // 再插入一个元素 65 位
    jedis.hset("numKey", "64", value);
    System.out.println(jedis.objectEncoding("numKey"));
}

输出:

ziplist
65
hashtable

内存回收机制

  • Redis 并不总是可以将空闲内存立即归还给操作系统。
  • 操作系统回收内存是以页为单位,如果这个页上只要有一个 key 还在使用,那么它就不能被回收。
  • flushdb:所有的 key 都干掉了,大部分之前使用的页面都完全干净了,会立即被操作系统回收。
  • Redis 虽然无法保证立即回收已经删除的 key 的内存,但是它会重用那些尚未回收的空闲内存。这就好比电影院里虽然人走了,但是座位还在,下一波观众来了,直接坐就行。而操作系统回收内存就好比把座位都给搬走了。

内存分配算法

127.0.0.1:6379> info memory
# Memory
mem_allocator:jemalloc-5.2.1-redis

内存分配是一个非常复杂的课题,需要适当的算法划分内存页,需要考虑内存碎片,需要平衡性能和效率。

Redis 为了保持自身结构的简单性,在内存分配这里直接做了甩手掌柜,将内存分配的细节丢给了第三方内存分配库去实现。目前 Redis 可以使用 jemalloc(facebook) 库来管理内存,也可以切换到tcmalloc(google)。因为 jemalloc 相比 tcmalloc的性能要稍好一些,所以Redis默认使用了jemalloc。