Redis 系列文章(一)

Redis数据结构底层实现

Redis作为一个c语言实现的高效率内存数据库,底层使用了很多高效的数据结构,平常我们会使用到它那些结构应用到业务场景呢,下面给些回答:

最普通kv set key value 字符串对象,底层编码实现 sds int embwar raw,Redis设计和实现上比较好的一点是 数据类型的支持和底层编码分开,redis支持的数据类型有 string hash set zset list等,某个数据类型都有多种编码方式,比如 string 就有 int embstr raw 三种方式 hash有ziplist和hashtable等方式,这样的设计使得数据类型和底层编码分开,后续拓展编码方式很容易,具体如下

  • List 数据类型底层数据结构由「双向链表」或「压缩表列表」实现;

  • Hash 数据类型底层数据结构由「压缩列表」或「哈希表」实现;

  • Set 数据类型底层数据结构由「哈希表」或「整数集合」实现;

  • Zset 数据类型底层数据结构由「压缩列表」或「跳表」实现;

string 在redis中的机构 string存储最大512M 是二进制安全的 也就是说可以存储图片等二进制数据

String 底层数据结构

Redis 5.0 的 SDS(simple dynamic string) 的数据结构

截屏2021-12-03 下午2.38.54

具体详细介绍如下:

  • len,SDS 所保存的字符串长度。这样获取字符串长度的时候,只需要返回这个变量值就行,时间复杂度只需要 O(1)。c语言原生char类型为O(n)

  • lloc,分配给字符数组的空间长度。这样在修改字符串的时候,可以通过 alloc - len 计算 出剩余的空间大小,然后用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区益处的问题。

  • flags,SDS 类型,用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5(貌似已废弃,待确认)、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64,后面在说明区别之处。

  • buf[],字节数组,用来保存实际数据。不需要用 “\0” 字符来标识字符串结尾了,而是直接将其作为二进制数据处理,可以用来保存图片等二进制数据。它即可以保存文本数据,也可以保存二进制数据,所以叫字节数组会更好点。

    总的来说,Redis 的 SDS 结构在原本字符数组之上,增加了三个元数据:len、alloc、flags,用来解决 C 语言字符串的缺陷。

    节省内存空间

    SDS 结构中有个 flags 成员变量,表示的是 SDS 类型。

    Redos 一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。

    这 5 种类型的主要区别就在于,它们数据结构中的 len 和 alloc 成员变量的数据类型不同

    比如sdshdr16 和 sdshdr32

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len;
    uint16_t alloc;
    unsigned char flags;
    char buf[];
    };


    struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len;
    uint32_t alloc;
    unsigned char flags;
    char buf[];
    };
    • sdshdr16 类型的 len 和 alloc 的数据类型都是 uint16_t,表示字符数组长度和分配空间大小不能超过 2 的 16 次方。
    • sdshdr32 则都是 uint32_t,表示表示字符数组长度和分配空间大小不能超过 2 的 32 次方。
    • 之所以 SDS 设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间。比如,在保存小字符串时,结构头占用空间也比较少。
    • 除了设计不同类型的结构体,Redis 在编程上还使用了专门的编译优化来节省内存空间,即在 struct 声明了 __attribute__ ((packed)) ,它的作用是:告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐

压缩列表

压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组。

截屏2021-12-03 下午5.34.04

压缩列表在表头有三个字段:

  • zlbytes,记录整个压缩列表占用对内存字节数;

  • zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;

  • zllen,记录压缩列表包含的节点数量;

  • zlend,标记压缩列表的结束点,特殊值 OxFF(十进制255)。

    在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。

压缩列表节点包含三部分内容:

  • prevlen,记录了前一个节点的长度;
  • encoding,记录了当前节点实际数据的类型以及长度;
  • data,记录了当前节点的实际数据;

连锁更新

压缩列表除了查找复杂度高的问题,压缩列表在插入元素时,如果内存空间不够了,压缩列表还需要重新分配一块连续的内存空间,而这可能会引发连锁更新的问题。

压缩列表里的每个节点中的 prevlen 属性都记录了「前一个节点的长度」,而且 prevlen 属性的空间大小跟前一个节点长度值有关,比如:

  • 如果前一个节点的长度小于 254 字节,那么 prevlen 属性需要用 1 字节的空间来保存这个长度值;
  • 如果前一个节点的长度大于等于 254 字节,那么 prevlen 属性需要用 5 字节的空间来保存这个长度值;

所以一般ziplist保存元素不会很多

Redis 实际项目中应用场景

(1)排行榜

ziplist zrank zreverange

1
2
3
4
5
6
7
8
9
 zadd ranklist 300 apple 500 banana //添加排行榜内容
zrevrange ranklist 0 10 withscores //返回排行榜前10名,带上分数

# 添加元素的分数,如果member不存在就会自动创建
ZINCRBY key increment member
# 按分数从大到小进行读取
zrevrange key
# 根据分数从大到小获取member排名
zrevrank key member

(2)月打卡签到

bitmap 实现底层

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
 # 2021年3月1号签到
127.0.0.1:6379> SETBIT user:sign:5:202103 0 1
(integer) 0

# 2021年3月2号签到
127.0.0.1:6379> SETBIT user:sign:5:202103 1 1
(integer) 0

# 2021年3月3号签到
127.0.0.1:6379> SETBIT user:sign:5:202103 2 1
(integer) 0

# 获取2021年3月3号签到情况
127.0.0.1:6379> GETBIT user:sign:5:202103 2
(integer) 1

# 获取2021年3月4号签到情况
127.0.0.1:6379> GETBIT user:sign:5:202103 3
(integer) 0

# 统计2021年3月签到次数
127.0.0.1:6379> BITCOUNT user:sign:5:202103
(integer) 3

# 获取2021年3月首次签到(返回索引)
127.0.0.1:6379> BITPOS user:sign:5:202103 1
(integer) 0

# 获取2021年3月前3天签到情况,返回7,二进制111,意味着前三天都签到了
127.0.0.1:6379> BITFIELD user:sign:5:202103 get u3 0
(integer) 7

# 获取1月份前31天的签到数据
BITFIELD u:sign:1225:202101 get u31 0

(3)地理位置 geo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# geoadd location_set longitude latitude name
geoadd guangdong-cities 113.2099647 23.593675 Qingyuan --添加清远市的坐标
geoadd guangdong-cities 113.2278442 23.1255978 Guangzhou --添加广州市的坐标

# geopos location_set name 返回设置的经纬度坐标

# geodist location_set name1 name2
> geodist guandong-cities qingyuan guanzhou
"52094.433" --清远和广州之间的直线距离 52094m

# georadius location_set longtitude latitude radius unit --返回某个地理位置附件多少km等单位的城市或地理位置信息

# georadiusbymember location_set name radius unit [WITHDIST] [WITHCHOORD] [ASC|DESC] [COUNT n]

(4) 月pv uv统计

1
2
3
4
# hyperlog 月pv uv统计
pfadd hyperloglog element [element ...]
pfcount hyperloglog [hyperloglog ...] 返回集合的近似基数
pfmerge destination hyperloglog [hyperloglog ...] 计算多个hyperlog的并集

Linux环境上的redis优化

1 最大打开文件句柄数

1
ulimit -Sn {max-open-files}

2 Tcp backlog

1
2
3
cat /proc/net/sys/core/somaxconn
128
echo 511 > /proc/net/sys/core/somaxconn
  1. vm.overcommit_memory建议为1

  2. Linux > 3.5 vm.swappiness建议为1 否则建议为0

  3. Transparent Huge Pages (THP)建议关闭 但注意linux发行版本改变了THP位置

  4. 可以为Redis设置 oom_adj 减少redis被OOM killer概率 但不能过渡依赖

  5. 建议对Redis所在节点 使用NTP服务,同步时钟

  6. 设置合理ulimit保证网络连接正常

  7. 设置合理的tcp-backlog参数

Redis 安全

1 设置密码 requirepass 设置密码长度为64个字节以上

2.伪装危险命令

引入renamecommand 比如

1
rename-command flushall 12312312fascsfsfsfsfs

3.防火墙

4.bind

bind指定了 redis 绑定那块网卡可以进行访问

5bigkeys 阻塞问题

redis-cli –bigkeys 可以查找占比比较大的key

另外可以通过 debug object key 查看一个key是否是bigkey 可以查看这个命令返回的 serializedlength属性 返回序列号字节数

其他待深入学习

Redis运维平台(CacheCloud)

github redis-migarate-tool 唯品会开源迁移工具

Redis bitmap 实现布隆过滤器 https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter

redis-faina

paketbeat ELK 抓取日志收集器

RedisLive工具

参考资料

《Redis设计和实现》

《Redis开发和运维》