Redis之数据结构和底层编码

一 字符串(string)

在Redis的世界里,数据类型没有什么整数、浮点数、布尔值等,只有一种类型来代替他们,那就是字符串。字符串无论是否加引号,redis可以自动识别它是否是整数或者浮点数,亦或是普通的字符串。如果是整数,那我们还可以进行自增自减等操作,如果是浮点数,则会被检查出来不是整数,所以不能自增,只能够增加指定的浮点类型的数据。

#1 Redis的字符串是动态的可以修改的字符串,内部结构类似于Java的ArrayList,采用预分配内存来减少频繁的内存分配,如果内存小于1M的,则成倍扩容;

#2 如果超过1M则每次只增加1M。另外就是字符串最大只允许512M。

1.1 设置

1.1.1 设置单个值

set key value [ex seconds] [px milliseconds] [nx|xx]

ex: 设置key的到期时间,以秒为单位 可以通过setex key second value代替

px: 设置key的到期时间,以毫秒为单位

nx: 如果key不存在,则执行操作,可以通过setnx key value代替

xx: 如果key存在,则执行操作

如果没有设置到期时间,则就没有到期时间

set shape round

set shape squre xx

set color red nx

set color blue ex 3600 xx

set gender women px 360000 nx

1.1.2 获取单个字符串

get key

get shape

1.1.3 批量获取和设置

1.1.3.1 批量设置

mset key value [ex seconds] [px milliseconds] [nx|xx] key value [ex seconds] [px milliseconds] [nx|xx] .......

mset a 15 ex 2000 xx b 20 ex 2000 nx

1.1.3.2批量获取

mget a b

mget color shape gender

mget 和 get区别

批量操作命令可以有效提高效率,因为N次get时间 = N次网络时间 + N次命令执行时间。所以mget只需要一次网络时间即可。但是也不能数量过多。

1.1.4 整数增加或者减少以及浮点数增加

1.1.4.1 incr 整数自增(可以用来实现计数的功能)

set count 100 xx

incr count

1.1.4.2 decr 正数自减

decr count

1.1.4.3 incrby 增加多少

incrby count 200

1.1.4.4 decrby 减少多少

decrby count 50

1.1.4.5 incrybyfloat 浮点数增加

set a 1.5 xx

incrbyfloat a 100

注意没有decrbyfloat

1.1.5 getrange截取字符串

getrange key start end

start: 从什么位置开始截取

end: 截取到神呢么位置结束

从0开始截取5个字符串

getrange word 0 4

1.1.6 setrange替换字符串

setrange key offset value

offset: 从什么位置开始替换

value: 要替换的值

setrange word 1

1.1.7设置完某一个key返回之前的值

getset key value

getset a 15

1.1.8字符串长度

strlen key

strlen color

二 列表(list)

2.1 列表介绍

#1 列表相当于Java的LinkedList,是基于链表的数据结构,所以插入和删除很快,但是查询较慢。

#2 当列表弹出最后一个元素的时候,该数据结构自动被删除,内存被回收。

#3 列表常用来做异步队列队列使用,将需要延后处理的任务结构序列化成字符串,然后塞进Redis的列表,另外一个线程从列表中轮询数据。

#4 一个列表最多可以存储2^32-1个元素

#5 列表可以从两边push和top,所以既可以充当栈,也可以充当队列

2.2 存储数据

我们可以从右边或者左边进行push

rpush key value [value ......]

rpush books "think in java" "spring in action" "design pattern" "hadoop in action"

lpush key value [value ......]

lpush movies "seventeen years" "ashes of time" "fream factory" "third sister liu" "titanlic"

2.3 弹出数据(删除) 和带阻塞机制的弹出

2.3.1 非阻塞弹出

我们可以从右边或者左边进行pop

rpop books

lpop movies

2.3.2 阻塞式弹出

当列表list没有数据的时候,就会阻塞等待

blpop key [key……] timeout

brpop key [key……] timeout

2.4 队列

2.4.1 左进右出

lpush movies "seventeen years" "ashes of time" "fream factory" "third sister liu" "titanlic"

rpop movies

2.4.2 右进左出

rpush books "think in java" "spring in action" "design pattern" "hadoop in action"

lpop books

2.5 栈

2.5.1 左进左出

lpush movies "seventeen years" "ashes of time" "fream factory" "third sister liu" "titanlic"

lpop movies

2.5.2 右进右出

rpush books "think in java" "spring in action" "design pattern" "hadoop in action"

rpop books

2.6 查找(不会删除)

2.6.1 范围查找

lrange key start end

lrange key 0 -1: 表示全部查询(慎用)

lrange books 1 2   

2.6.2 索引查找

lindex key index

lindex movies 0

2.7 删除元素

2.7.1 获取元素之后删除

lpop/rpop key 弹出元素之后,从列表中删除

2.7.2 直接删除指定数量的元素,不获取元素

lrem key count value 从列表中查找出所有和指定value相同的值,删除count个。

count: 代表要删除的个数,可以是正数、负数和0

value: 代表要删除的元素

#1 如果是lpush

其中count > 0,表示从右边到左边删除count个;count < 0表示从左边到右边删除count个元素;count=0删除所有相同的值。

rpush counts 1 2 1 2 1 2 1 2 1 2

lrem nums 2 1 : 从右往左删除2个1, 则剩余元素:1 2 1 2 1 2 2 2

lrem nums -2 1: 从左往右删除2个1,则剩余元素: 2 2 1 2 1 2 1 2

lrem counts 0 1: 删除所有1元素,剩余元素: 2 2 2 2 2

#2如果是rpush

其中count > 0,表示从左边到右边删除count个;count < 0表示从右边到左边删除count个元素;count=0删除所有相同的值。

lpush counts 1 2 1 2 1 2 1 2 1 2

lrem counts 2 1 : 从左往右删除2个1, 则剩余元素:2 2 1 2 1 2 1 2

lrem counts -2 1: 从右往左删除2个1,则剩余元素: 1 2 1 2 1 2 2 2

lrem counts 0 1: 删除所有1元素,剩余元素: 2 2 2 2 2

2.7.3 按照指定索引范围修剪列表,删除其他索引范围之外的元素

ltrim key start stop

start:从哪个位置开始裁剪

stop: 裁剪到什么位置结束

ltrim key 1 0 删除整个列表

ltrim counts 3 4

2.8 修改

lset key index new_value

index: 要修改哪一个位置元素

new_value: 要修改的值

lset counts 0 5

三 哈希(hash)

我们在Redis中要存储用户信息,如果不考虑哈希的情况下有2种办法,都是基于字符串的。

第一: 用户ID为key,用户对象序列化成JSON字符串作为value,比如 user:1={'name':'nicky','age':'20'}

优点:简单

缺点:需要序列化和反序列化;有并发问题,影响性能

第二: 将用户ID和用户属性作为key,属性值作为value,比如user:1:name='nicky',user:1:age=20

优点:无需序列化

缺点:需要存储很多key,对于内存来说也不是好事;另外也并未消除并发问题,只是降低了并发的粒度

hash数据结构,类似于java中的HashMap数据结构,也是采用数组+链表的结构作为底层数据结构,当key的hash值发生冲突的时候就放入链表中。但是他们的rehash的方式不一样

HashMap的rehash是对所有的数据全部进行rehash,这是一个耗时的操作;但是redis采用渐进式rehash,即在rehash的时候,保留新旧2个hash结构,查询的时候会同时查询2个结构,然后在后续的任务中将旧hash里面的数据一点一点迁移到新的哈希结构,当迁移完成了,就会使用新的hash结构取代它

哈希结构和列表一样,当移除最后一个元素的时候,数据结构自动删除,内存被回收。

3.1 设置值 hset

hset key field value 成功返回1,错误返回0

field: 字段

value: 值

hset user:1 name admin

hset user:1 age 45

hset user:1 gender men

3.2 获取

hget key field

hget user:1 name

3.3 删除

hdel key field

hdel user:1 e-mail

3.4 计算field个数和 value长度

3.4.1 hlen计算字典中field个数

hlen key

hlen user:1

3.4.2 hstrlen 计算value的长度

hstrlen user:2 name

hstrlen user:2 gender

3.5 批量设置和批量获取

hmset key field value [field value......]

hmset user:2 name "nicky" age 32 gender "men"

hmget key field field .......

hmget user:2 name age gender

3.6 判断key是否存在

hexists key

hexists user:1

3.7 获取字典中所有的field和value

类似于java中获取字典中所有的键和值

3.7.1 获取所有的field(键)

hkeys key

hkeys user:1

1) "name"

2) "age"

3) "gender"

3.7.2 获取所有的value(值)

hvals key

hvals user:1

1) "nicky"

2) "32"

3) "men"

3.7.3 获取所有键值

hgetall key

hgetall user:1

1) "name"

2) "nicky"

3) "age"

4) "32"

5) "gender"

6) "men"

3.8 可以增加value的值

hincrby/hincrbyfloat key field increment

hincrbyfloat user:1 age 10

四 集合(set)

#1 和Java中HashSet相似,HashSet底层是基于HashMap实现的,所有的value字段都是null; Redis也一样,基于hash实现的,所有的value都是nil

#2 集合是不能重复的,且没有顺序(所以有去重的功能)

#3 当集合中最后一个元素被移除,集合数据结构被删除,内存被回收

4.1 集合内部的操作

4.1.1 添加集合

sadd key member [member ......]

添加一个

sadd animals tiger

添加多个

sadd animals lions cat panda

4.1.2 获取集合元素

4.1.2.1 随机获取N个元素

srandmember key count

srandmember animals 2

4.1.2.2 随机获取N个元素(删除)

spop key count

spop animals 2 会从集合返回2个元素,然后删除

4.1.2.3 获取所有元素

smember key

smembers animals

4.1.3 删除集合元素

4.1.3.1 删除指定元素

srem key member [member......]

srem animals buff cat

4.1.3.2 删除指定数量元素,并且返回删除的元素

spop key count

spop animals 2 会从集合返回2个元素,然后删除

4.1.4 是否存在某个元素

sismember key member 如果存在返回1否则返回0

sismember animals buff

4.1.5 计算集合元素个数

scard key

scard animals

4.2 集合之间的操作

sadd a 10 20 30 40 50

sadd b 40 50 60 70 80

4.2.1 交集

sinter key1 key2 ......

sinter a b

1) "40"

2) "50"

4.2.2 并集

sunion key1 key2 ......

sunion a b

1) "10"

2) "20"

3) "30"

4) "40"

5) "50"

6) "60"

7) "70"

8) "80"

4.2.3 差集

sdiff key1 key2 ......

sdiff a b

1) "10"

2) "20"

3) "30"

4.2.4 将交集保存为新集合

sinterstore destination key [key......]

destination: 要保存的新的集合名字

sinterstore abinter a b

4.2.5 将并集保存为新集合

sunionstore destination key [key......]

destination: 要保存的新的集合名字

sunionstore abunion a b

4.2.6 将差集保存为新集合

sdiffstore destination key [key......]

destination: 要保存的新的集合名字

sdiffstore abdiff a b

五 有序集合(zset)

#1 类似于Java中SortedSet和HashMap的结合体:一方面他是一个set,保证了元素value的唯一性;另一方面给每一个元素赋予了一个分数,代表这个value的排序权重。

#2 内部实现用的是跳跃列表的数据结构

#3 虽然集合不能重复,但是集合里面元素的分数是可以重复的

#4 zset中数据全部被移除之后,数据结构会被删除,然后内存被回收

#5 比较适合存储需要按照权重来排序的数据,比如考试成绩等等

5.1 集合内部操作

5.1.1 添加元素

zadd key score member [score member ......]

zadd books_1 6.5 "apache kafka in action" 8.2 "apache kylin" 4.5 "hadoop in action" 9.0 "spark in action"

zadd books_2 9.0 "spark in action" 6.8 "think in java" 7.9 "Java Virtual Machine" 8.3 "spring in action"

5.1.2 获取某一个成员分数

zscore key member

zscore books_1 "apache kylin"

5.1.3 返回指定排名的成员

zrange books_1 start stop [withscores]

start: 从哪一个排名开始

stop: 到哪一个排名结束

withscores: 是否返回分数

zrange books_1 1 2  withscores

1) "apache kafka in action"

2) "6.5"

3) "apache kylin"

4) "8.2"

5.1.4 返回指定分数范围的排序成员

zrangebyscore key min max [withscores] [limit offset cout]

#1 获取指定分数范围的有成员

zrangebyscore books_1 7 9

1) "apache kylin"

2) "spark in action"

#2 获取指定分数范围的有成员,并且携带分数

zrangebyscore books_1 7 9 withscores

1) "apache kylin"

2) "8.2"

3) "spark in action"

4) "9"

#3 获取指定分数范围的有成员,但是只从结果某位置(offset)开始,连续取N(count)个

zrangebyscore books_1 4 9 limit 1 2

1) "apache kafka in action"

2) "apache kylin"

5.1.5 获取成员个数分区区间成员个数

zcard key 返回成员个数

zcard books_1

zcount key min max 分数区区间[min,max]成员个数

zcount books_1 7 9

5.1.5 获取成员排名

5.1.5.1 由低到高排序

zrank key member

zrank books_2 "spark in action"

5.1.5.2 由高到低排序

zrevrank key member

zrevrank books_2 "spark in action"

5.1.7 删除成员

5.1.7.1 删除指定成员

zrem key member

zrem books_1 "spark in action"

5.1.7.2 删除指定排名内的升序成员

zremrangebyrank key start end

zrmrangebyrank animals 0 2 删除正序排序为0 1 2的成员

5.1.7.3 删除制指定分数范围的成员

zremrangebyscore key min max

zremrangebyscore animals 5 7 删除5-7分的成员

5.1.7 元素自增

zincrby key increment member

zincrby A 10 1 获取A集合中元素1,如果存在在1的基础上添加10

5.2 集合之间的操作

5.2.1 将交集保存为新集合

zinterstore destination key [key......] [weights weight ] [aggregate sum|min|max]

destination: 要保存的新的集合名字

zinterstore abinter a b

5.2.2 将并集保存为新集合

zunionstore destination key [key......] [weights weight ] [aggregate sum|min|max]

destination: 要保存的新的集合名字

zunionstore abunion a b

六 geo

Geo是基于zset实现的

6.1 添加

geoadd 集合 经度 纬度 名称

geoadd geo:city 118.8921 31.32751 nanjing

6.2 获取地理位置的坐标

geppos 集合 名称

geopos geo:city Nanjing

6.3返回两个给定位置之间的距离

Geodist 集合 member1 member2 距离单位

geodist geo:city nanjing hangzhou km

6.4以给定的经纬度为中心,返回键包含的位置元素当中,与中心的距离不超过

过给定最大距离的所有位置元素

georadius 集合 经度 纬度 范围 单位

georadius geo:city 120 30 100 km withcoord

6.5 删除

zrem 集合 名称

zrem geo:city suzhou

六 redis对key的管理

6.1 查看所有key(慎用),支持正则

keys *

keys an*

6.2 删除某个key

del key

3.6.3 获取key的数据类型

type key

type user:1

6.3 是否存在某个key

exists key [key .....]

只返回存在个数

6.4 设置key的到期时间和检查到期时间

设置到期时间

expire key second(秒级)

pexpire key millionseconds(毫秒级)

ttl key 检查到期时间(秒级)

pttl key 检查到期时间(毫秒级)

6.6 key重命名

rename key newkey

重命名期间,会删除旧的键,如果键对应的值比较大,会存在阻塞

6.7 key迁移

从一个Redis 数据库迁移到另一个Redis 数据库

move animals 1

migrate 也适用于Redis实例间进行数据迁移

migrate host port key| destination-db timeout [COPY] [REPLACE] [KEYS key]

host:目标数据库的IP地址

port: 目标数据库端口

key|"": 如果迁移一个key,则用key,如果迁移多个则使用“”

destination-db: 目标数据库

timeout: 迁移的超时时间

copy: 如果添加此选项,并不删除原来key

replace: 不管是否目标数据库存在这个key,给替换掉

keys: 表示迁移多个key

migrate 192.168.3.201 6379 "" 1 5 copy KEYS fruits plants

6.8 key遍历

3.6.8.1 keys 全量遍历

3.6.8.2 scan渐进式遍历

SCAN 命令及其相关的 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令都用于增量地迭代(incrementally iterate)一集元素(a collection of elements):

SCAN 命令用于迭代当前数据库中的数据库键。

如果不指定游标,默认返回10个,然后返回下一次获取需要从哪一个游标开始。他迭代的是整个数据库的key.

 scan 0

1) "5" // 下一次获取key的游标位置

2)  1) "AB"

    2) "ab"

    3) "b"

    4) "books_2"

    5) "user:2"

    6) "A"

    7) "abunion"

    8) "user:4"

    9) "user:3"

   10) "a"

   11) "animals"

   12) "abinter"

scan 5

1) "0" // 游标位置为0,说明已经遍历完了

2) 1) "B"

   2) "animal"

   3) "user:1"

   4) "books_1"

SSCAN 命令用于迭代集合键中的元素。

HSCAN 命令用于迭代哈希键中的键值对。

ZSCAN 命令用于迭代有序集合中的元素(包括元素成员和元素分值)。

6.9 键值序列化

dump key 会将键值序列化,格式采用rdb

restore key ttl value # 将序列化的值复原,其中ttl参数代表过期时间,ttl=0表示没有过期时间

七 基础数据结构

7.1 字符串

字符串是redis中基础的数据结构, 在C语言中字符串用char[]表示,并且末尾用\0结尾,表示字符串结束。比如 char[] data ="content",  C语言表示为"content\0"。

如果字符串内有多个\0的字符, 那么有可能字符串就会被截断,比如"content\0123545\0Bee\0"。所以为了解决这个问题, Redis引入了SDS(simple dynamic string)结构,简单动态对象,核心思想,就是字符串长度不再因\0表示结尾,而是取决于数据本身的长度,所以每一个字符串有一个int类型的len属性表示字符串长度,然后char[] 字符数组表示字符串。其实这样也基本解决问题了,但是一个int最多可以表示8字节,普通int也是4字节,可以表示20多亿的长度,一般情况下应该没有这么长的字符串,所以为了避免不必要的空间浪费,redis又进行优化,根据字符串长度分为sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。

7.1.1 sdshdr5

struct __attribute__ ((__packed__)) sdshdr5 {

    unsigned char flags; // 前3位表示类型,后5位表示数据长度

    char buf[]; // 字符串

};

flags: 字符串类型,占用1字节,其中前3位表示字符串类型,后5位表示字符串长度

buf: 表示字符串

当字符串长度小于2^5-1,也就是1 <= len <= 31,则使用这个类型

7.1.2 sdshdr8

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; // 字符串已占用多少字节
    uint8_t alloc; // 剩余多少可用字节
    unsigned char flags; // 前3位表示类型,后5位没有使用
    char buf[];// 字符串
};


len: 字符串占用多少字节

alloc: 剩余多少可用字节

flags: 字符串类型,占用1字节,其中前3位表示字符串类型,后5位表示字符串长度

buf: 表示字符串

 

7.1.3 sdshdr16

struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; // 字符串已占用多少字节
    uint16_t alloc; // 剩余多少可用字节
    unsigned char flags; // 前3位表示类型,后5位没有使用
    char buf[];// 字符串
};


len: 字符串占用多少字节

alloc: 剩余多少可用字节

flags: 字符串类型,占用1字节,其中前3位表示字符串类型,后5位表示字符串长度

buf: 表示字符串

7.1.4 sdshdr32

struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; // 字符串已占用多少字节
    uint32_t alloc; // 剩余多少可用字节
    unsigned char flags;// 前3位表示类型,后5位没有使用
    char buf[];// 字符串
};


len: 字符串占用多少字节

alloc: 剩余多少可用字节

flags: 字符串类型,占用1字节,其中前3位表示字符串类型,后5位表示字符串长度

buf: 表示字符串

 

7.1.5 sdshdr64

struct __attribute__ ((__packed__)) sdshdr64 {

    uint64_t len; // 字符串已占用多少字节

    uint64_t alloc; // 剩余多少可用字节

    unsigned char flags; // 前3位表示类型,后5位没有使用

    char buf[];// 字符串

};

len: 字符串占用多少字节

alloc: 剩余多少可用字节

flags: 字符串类型,占用1字节,其中前3位表示字符串类型,后5位表示字符串长度

buf: 表示字符串

7.2 字典(dict)

dict 字典是redis中基础的数据结构,RedisDb的key和value,以及hash或者zset都是基于dict字典实现的。

7.2.1 字典的数据结构

typedef struct dictht {

    dictEntry **table; // 数据

    unsigned long size; // 表示这个dictht已经分配空间的大小,大小总是2^n

    unsigned long sizemask;  //sizemask = size - 1; 是用来求hash值的掩码,为2^n-1

    unsigned long used; //目前已有的元素数量

} dictht;


typedef struct dictEntry {

    void *key; // 指向key的指针

    union {

        void *val; // 指向value的指针

        uint64_t u64;

        int64_t s64;

        double d;

    } v;

    struct dictEntry *next; // 后继节点指针

} dictEntry;

7.2.2 渐进式哈希

一般字典扩容,需要重新申请新的数组,然后将老字典中数据拷贝过去,这是一个O(N)级别的事情,有的可能是阻塞然后rehash,有的可能是多线程rehash。如果是大字典的话,作为单线程的Redis,肯定是不可能多线程来进行rehash的。Redis采用是一种渐进式rehash。

7.2.2.1 字典中字段

typedef struct dict {

    dictType *type; // 字典类型

    void *privdata;

    dictht ht[2]; // 哈希表,ht[0]对外提供访问,ht[1]用于rehash

    long rehashidx; // rehash索引,没有发生rehash,这个字段就是-1,否则代表dict[0]哪一个索引正在进行rehash

    int iterators; // 当前正在运行的迭代器数量

} dict;

哈希字典有两个哈希表ht[0]和ht[1], ht[0]用于对外访问,ht[1]用于rehash。并且有一个rehashidx用于表示当前哪一个索引正在进行rehash, 如果没有发生rehash,那么rehashidx=-1。

7.2.2.2 rehash扩容和缩容时机

第一:哈希表扩容和缩容都会触发rehash

第二: 每次插入键值对的时候会检查哈希表中元素数量或者redis数据库定时任务会自动检测当前哈希表元素数量

第三: 负载因子 = 哈希表已保存节点数量 / 哈希表大小

load_factor = ht[0].used / ht[0].size

第四: 当前没有子进程执行bgsave或者bgwriteaof命令,即没有进行任何持久化的动作,并且哈希表负载因子大于1,即大于当前哈希表的size,则触发扩容操作

第五: 当前有子进程执行bgsave或者bgwriteaof命令,即正在进行持久化操作,并且哈希表负载因子大于5,则也会触发扩容操作

第六:当数据库定时任务检测到哈希表元素或者节点数量只有哈希表size的1/10,也就是加载因子小于0.1的时候,这时候会触发缩容操作

7.2.2.3 rehash是如何扩容

Redis扩容是按照2的倍数来扩容

7.2.2.4 redis是如何渐进式rehash的?

第一:检查到哈希表需要扩容,则会将ht[1]分配空间,, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。

第二:在字典中维持的rehashidx字段, 将它的值设置为 0 , 表示 rehash已经开始

第三:客户端每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。这个过程也叫操作辅助rehash

第四:可能客户端长时间没有操作,此时不会有增删改查,服务器比较空闲,redis数据库将很长时间内都一直使用两个哈希表。这样的话,长时间长时间两个哈希表都占用内存,肯定不好。所以,Redis数据库有定时任务,如果发现有字典正在进行渐进式rehash操作,则会花费1毫秒的时间,帮助一起进行渐进式rehash操作。这个操作也叫定时辅助rehash

第五:当ht[0]哈希表中所有桶内的数据已经rehash到ht[1]中,此时会将ht[1]重新赋给ht[0], 然后ht[1]置为null,并且rehashidx重置为-1,此时渐进式rehash结束。

7.3 ziplist(压缩列表)

ziplist是一块连续的内存空间,元素紧凑的存储,没有任何冗余空隙。如图所示:

zlbytes: ziplist总的字节数

zltail_offset: 最后一个元素距离ziplist起始位置的偏移量

zllen: ziplist中元素个数

entry数组: 存储的内容
zlend: 表示压缩列表ziplist的结束,值恒为0XFF

Entry字段信息:

prevlen: 前一个entry的长度

encoding: 当前entry使用的编码

data: entry中的数据

优点:元素存储紧凑,节约空间

缺点:每次增加元素需要重新分配内存,然后将数据拷贝到新的分配区域上,如果数据量较大,而且redis又是一个单线程,所以将会影响性能。

7.4 quicklist(快速列表)

因为ziplist每次增加元素需要重新分配内存,然后将数据拷贝到新的分配区域上,如果数据量较大,而且redis又是一个单线程,所以将会影响性能。所以Redis又又有了quicklist, quicklist是一个双向链表,每一个节点维护一部分元素到一个ziplist,当ziplist达到阈值则quicklist开始进行分裂,这样可以将以前存在一个ziplist中元素分成多个段,那么再进行增加元素的时候,拷贝的ziplist也只是某一个段上数据,而不是全部ziplist中全部数据,这样提升了性能。

7.5 skiplist(跳跃链表)

struct zslnode {

    string value; // 数据

    double score; // 分数

    zslnode*[] forwards;  // 多层连接指针

    zslnode* backward;  // 回溯指针

}



struct zsl {

    zslnode* header; // 跳跃列表头指针

    int maxLevel; // 跳跃列表当前的最高层

    map<string, zslnode*> ht; // hash 结构的所有键值对

}

八  数据结构的编码

数据结构的编码指的是各种数据结构在底层是如何存储数据的, 我们可以通过过object encoding + key来查看key的编码。对于不同的数据结构,底层可能有不同的编码,即不同的底层存储实现。

8.1 RedisObject是什么

Redis的数据最后会通过RedisObject将数据进行一个封装,说白了,RedisObject就是存储对象,除了包括存储的数据(指向数据的地址),RedisObject还有数据的类型、数据的编码、引用计数次数等元数据或者头信息,如下所示:

typedef struct redisObject {
    unsigned type:4; /* 对象类型: string?list?zset? hash?等*/
    unsigned encoding:4; // 编码:int?row ?embstr?ziplist?
    unsigned lru:LRU_BITS; /* 内存淘汰相关的LRU信息*/
    int refcount;// 引用次数,引用计数法,管理内存
    void *ptr; // 指向数据存储的内存地址
} robj;

8.2 string类型的编码

8.2.1 int

8.2.1.1 int的满足条件

如果string类型的编码是int,需要满足什么条件?

第一:该字符串必须是数字

如果是包含有其他字母或者特殊符号的,那么编码一定不是int

第二:该字符串长度必须小于20

如果字符串长度大于或者等于20,那么编码一定不是int。 因为int类型最大长度一般就是8字节,即64位,可以表示最大数据长度不会超过20. 所以即便字符串全是数字,但是长度超过了20,那么底层也不是int。

8.2.1.2 为什么设计int这样的编码

我们知道RedisObject中的ptr指针会指向数据存储的内存地址,也就是说ptr存储的并不是数据,而是数据的地址,这样的话,获取数据的时候,每次是先要拿到ptr保存的数据地址,然后再根据内存地址从内存获取数据。

ptr是一个指针,指针会占用8字节的内存空间,而如果存储的值如果很明确知道不会超过8字节,那我们可以直接用指针存储数据,而不是地址,这样客户端获取数据的时候,直接读取RedisObject的ptr值就可以,无需再拿着ptr指向的地址去内存再读一次数据。

而int类型的数据,我们知道最大也就8字节,是肯定不会超过8字节的,所以用8字节的ptr指针直接存储8字节的int数据,节省了内存开销和Redis的性能

8.2.2.embstr

8.2.2.1 满足embstr条件

如果是数字还需要长度大于20且字符串长度小于或者等于44;如果不是数字,则只需要字符串长度小于或者等于44的字符串则是使用的embstr编码

8.2.2.2 为什么设计embstr这样的编码

如果不这样设计,那Redis可能就会为RedisObject和SDS单独分配内存空间,而且在内存上可能不是连续的。这样的话每次都需要先从RedisObject获取对应ptr指向的地址,然后再将数据读取出来。

我们知道,CPU从内存读取数据,是根据缓存行读取,默认缓存行大小是64字节,所以如果要读取的数据小于64,那么读取缓存行的时候就会把连续存储的64字节的数据读取出来到CPU的缓存中,这样提升了读取效率。

所以Redis它将 RedisObject 对象头和 SDS 对象连续存在一起,这样的好处就是避免在创建的时候多分配1次空间,在删除的时候少释放1次空间,而且连续存储还可以提升查找效率。

8.2.2.3 为什么少于44字节的字符串才会使用embstr?

第一:缓存行是64字节

第二:RedisObject占用16字节(type: 4bit, encoding: 4bit, LRU: 24bit, refcount: 4byte, ptr: 8byte)

typedef struct redisObject {
    unsigned type:4; /* 对象类型: string?list?zset? hash?等*/
    unsigned encoding:4; // 编码:int?row ?embstr?ziplist?
    unsigned lru:LRU_BITS; /* 内存淘汰相关的LRU信息*/
    int refcount;// 引用次数,引用计数法,管理内存
    void *ptr; // 指向数据存储的内存地址
} robj;

第三:SDS对象头占用3字节(len: 1byte, alloc: 1byte,flags:1byte),如下所示:

struct __attribute__ ((__packed__)) sdshdr8 {

    uint8_t len; // 1byte,表示已使用多少字节
uint8_t alloc; // 1byte,表示有多少剩余字节数可以分配,如果为0,表示没有字节可以分配了
unsigned char flags; //1byte,前3字节表示类型,后5字节没用
char buf[];

};

第四:字符串末尾还有一个\0,需占用一个字节

因此:剩余的字节数量=64-16-3-1 = 44

8.2.3 raw

只要大于44长度的字符串,无论是纯数字还是其他字母或者特殊字符都使用这种编码存储数据。存储特点,就是RedisObject和SDS是分开存储的,在内存上不是连续的,需要进行两次2分配内存,释放也需要两次。

8.2.4 int、embstr和raw的比较

8.3 hash的编码

Hash数据结构默认使用ziplist,当ziplist中元素个数超过512个或者最大元素超过64字节则切换为hashtable数据结构。

8.4 list的编码

列表采用quicklist编码来存储数据,quicklist是将ziplist分成多个段,每一个段中ziplist元素数量是有限制的,采用quicklist的好处就是再大数据量的时候对性能友好

8.5 set的编码

对于set集合,当元素全是数字且小于阈值,默认512 则使用intset(数组)作为底层数据存储结构;如果某一个元素不是纯数字或者数字或者列表中全是数字,但是元素个数超过阈值512,则会使用value等于null的hashtable

8.6 zset的编码

当数据元素较少的时候,使用ziplist来存储,当数据元素较多的时候使用skiplist来存储。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

莫言静好、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值