Redis相关

2020-05-12  本文已影响0人  万福来

Redis相关

Reids没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。

SDS的类型定义

 struct sdshdr{
 //记录buf数组中已使用字节的数量,等于SDS所保存字符串的长度
 int len;
 //记录buf数组中位数字字节的数量
 int free;
 // 字节数组,用于保存字符串
 char buf[];
 }

其中SDS遵循C字符串以空字符串结尾的管理,保存空字符的1字节不计算在SDS的len属性里边

SDS与C字符串的区别

常数复杂度获取字符串长度

C字符串并不记录自身的长度信息,所以获取C字符串长度,需要遍历数组,所以操作复杂度为O(N),而SDS在len属性中记录了SDS本身的长度获取复杂度为O(1)

避免缓冲区溢出

当SDS API需要对SDS进行修改时,API会先根据free属性来检查SDS的剩余空间十分满足,
如果不满足,API会自动将SDS的空间扩展至执行修改所需要的大小,然后才执行实际的修改操作,所以使用SDS不需要手动修改SDS的空间大小,可以避免缓冲区溢出问题。

减少修改字符串时带来的内存重分配次数

1.空间预分配策略
空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改,并且需要对SDS
进行空间扩展的时候,程序不仅会为SDS分配修改所需要的空间,还会为SDS分配额外的未使用空间。
空间预分配公式
SDS的len< 1MB ,则设置free和len属性值相同,表示预分配一倍的空间数组;
SDS的len>= 1MB,则预分配1MB的未使用空间。
2.惰性空间释放策略
惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序
并不立即使用内存重新分配来回收缩短多出来的字节,而是使用free属性,将这些字节的
数量记录下来,并等待将来使用。与此同时,SDS也提供了相应的API,让我可以在有需要时,
真正的释放SDS的使用空间,可以不用担心惰性空间释放策略会造成内存浪费。

二进制安全

C字符串中的字符必须符合某种编码,并且除了字符串的末尾之外,字符串里边不能包含空字符,
否则最先被程序读入的空字符串被误认为是字符串结尾,这些限制使的C字符串只能保存文本数据,不能保存图片、音频、视频、压缩文件等二进制数据。
而SDS使用len属性来判断字符串是否结束,不会对buf数组里的数据进行任何限制过滤等操作。
使得SDS可以保存任意格式的二进制数据。

兼容部分C字符串函数

虽然SDS的API是二进制安全的,但是一样遵循了C字符串以空字符串结尾的惯例,使得
SDS可以重用一部分C字符串库定义的函数。

C字符串和SDS之间的区别

C字符串 SDS
获取字符串长度的复杂度为O(N) 获取字符串长度的复杂度为O(1)
API不安全,可能会造成缓冲区溢出 API安全,不会造成缓冲区溢出
修改字符串长度N次需要执行N次内存重分配 空间预分配,避免每次都进行内存重分配
只能保存文本数据 可以保存文本或者二进制数据
可以使用所有字符库中函数 可以使用部分字符库中的函数

Redis-链表

链表作为一种常用的数据结构,但是在C语言中并没有内置这种数据结构,所以Redis构建了自己的链表实现。Redis中链表应用非常广泛,主要有列表键,发布与订阅,慢查询,监视器还有使用链表保存多个客户端的状态信息,构建客户端输出缓冲区等。

链表和链表节点实现伪代码

typedef strcut listNode{
    //前置节点
    struct listNode *prev;
    //后置节点
    struct listNode *next;
    //节点的值
    void *value;
}

typedef struct list{
    //链表头节点
    listNode *head;
    //链表尾部节点
    listNode *tail;
    //链表包含的节点数量
    unsigned long len;
    //节点值复制函数
    void *(*dup)(void *ptr);
    //节点值释放函数
    void (*free)(void *ptr);
    //节点值对比函数
    int (*match)(void *ptr,void *key);
}

通过使用多个listNode结构可以组成链表,然后在使用list结构来持有链表,内置部分函数,方便对链表进行操作处理。

Redis链表实现的特性

Redis-字典

Redis - 跳跃表

Redis - 整数集合

Redis - 压缩列表

对象类型与编码

Redis使用对象来表示数据库汇总的键和值,每次在redis数据库中新建一个键值对时,至少会创建两个对象,一个对象用作键值对的键对象,另一个对象用作键值对的值对象。
示例命令

 SET msg ‘hello world’

键值对的键是一个包含了字符串值“msg”的对象,而键值对的值则是一个包含了字符串值“hello world"的对象。
Redis 对象结构

    typedef strcut redisObject{
        // 类型
        unsigned type:4;
        // 编码
        unsigned encoding:4;
        // 指向底层实现数据结构的指针
        void *ptr;
    }

类型常量

类型常量 对象名称
REDIS_STRING 字符串对象
REDIS_LIST 列表对象
REDIS_HASH 哈希对象
REDIS_SET 集合对象
REDIS_ZSET 有序集合对象

编码

对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定。
encoding属性记录了对象所使用的编码,这个对象使用了什么数据结构作为对象的底层实现,这个属性的值可以如下常量中选择。
不同类型和编码对象映射关系

类型 编码 对象
REDIS_STRING REDIS_ENCODING_INT 使用整数值实现的字符串对象
REDIS_STRING REDIS_ENCODING_EMBSTR 使用embstr编码的简单动态字符串实现的字符串对象
REDIS_STRING REDIS_ENCODING_RAW 使用简单动态字符串实现的字符串对象
REDIS_LIST REDIS_ENCODING_ZIPLIST 使用压缩列表实现的列表对象
REDIS_LIST REDIS_ENCODING_LINKEDLIST 实现双端列表实现的列表对象
REDIS_HASH REDIS_ENCODING_ZIPLIST 使用压缩列表实现的哈希对象
REDIS_HASH REDIS_ENCODING_HT 使用字典实现的哈希对象
REDIS_SET REDIS_ENCODING_INTSET 使用整数集合实现的集合对象
REDIS_SET REDIS_ENCODING_HT 使用字典实现的集合对象
REDIS_ZSET REDIS_ENCODING_ZIPLIST 使用压缩列表实现的有序集合对象
REDIS_ZSET REDIS_ENCODING_SKIPLIST 使用跳跃表和字典实现的有序集合对象

字符串对象

编码 适用场景 优势
int 当字符串对象保存的是整数值,并且这个整数值可以用long类型 来表示,则采用int编码类型 可以进行数字自增运算
embstr 如果保存的字符串值的长度小于等于32字节,则采用该编码方式 保存这个字符串值 embstr编码创建和释放字符串对象所需要的内存分配和释放次数为一次; 字符串对象的所有数据都保存在一块连续的内存中。
raw 字符串值得长度大于32字节时,将使用SDS来保存这个字符串值 raw编码创建和释放字符串对象所需要内存分配和释放次数为两次。

列表对象

编码 适用场景 优势
ziplist 列表对象保存的所有字符串元素的长度都小于64字节 并且对象保存的元素数量小于512个。 可以修改配置文件 List-max-ziplist-value和list-max-ziplist-entries选项修改阈值
linkedlist 列表对象中的字符串长度超过64字节或者列表元素数量大于512个 使用双端列表数据结构存储

哈希对象

编码 适用场景 优势
ziplist 键值对的字符串长度都小于64字节并且键值对的数量小于512个 先将key节点压缩列表表尾,再将value节点推入表尾,键值对节点总数紧挨在一起
hashtable 键值对的字符串长度有一个超过64字节或数量超过512个 使用字典数据结构存储,键和值都是字符串对象,保存了键值对的值

集合对象

编码 适用场景 优势
intset 集合对象保存的所有元素都是整数值并且元素数量不超过512个 配置文件set-max-intset-entries选项修改阈值
hashtable 集合对象中添加一个字符串对象或元素数量超过512 转换为字典结构,每个键都是字符串对象,键对应的值设置为NULL

有序集合对象

编码 适用场景 优势
ziplist 有序集合的元素数量小于128个并且元素成员的长度都小于64字节 每个集合元素使用两个紧挨在一起的压缩列表节点保存,第一个节点保存元素成员,第二个节点保存元素分值,集合元素按照分值从小到大排序
skiplist 元素数量超过128个或元素成员长度超过64字节 通过跳跃表可以对有序集合进行范围操作,比如zrank、zrange命令,zset结构同时使用跳跃表和字典保存有序集合元素,通过指针来共享相同元素的成员和分值。

对象类型检查与命令多态

内存回收

    typedef struct redisObject{
        // ...
        // 引用计数
        int refcount;
        / ...
    }

创建新对象是,引用计数的值初始化为1,当有方法使用时,计数值加1,使用完毕后减1
当对象的引用计数值为0时,改对象占用的内存会被释放。
生命周期:创建对象 -> 操作对象 -> 释放对象

对象共享

多个键共享同一个值对象需要执行两个步骤:
1、将数据库键的值指向一个现有的值对象;
2、将被共享的值对象的引用计数加1;
Redis初始化服务器器,默认创建一万一个字符串对象,从0到9999。
当服务器用到值为0到9999的字符串对象时,服务器会使用这些共享对象。
创建共享字符串对象的数量可以通过修改redis.h/REDIS_SHARED_INTEGERS常量配置
当一个共享对象保存的值越复杂,验证共享对象和目标对象是否相同所需要的复杂度就会越高,需要消耗更多地CPU,所以只针对整数值的字符串对象进行共享。

对象空转时长

    typedef struct redisObject{
        // ...
        // 记录了对象最后一次被命令程序访问的时间
        unsigned lru:22;
        / ...
    }

如果服务器配置了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者
allkeys-lru,当服务器占用内存超过了maxmemory配置阈值,空转时长较高的部分键会有些被服务器释放,从而回收内存。

Redis - 数据库

Redis - 持久化

RDB持久化

AOF持久化

Redis - 事件

Redis - 客户端

Redis - 服务器

服务器读取命令请求

当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器来执行以下操作:

  1. 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面。
  2. 对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数,以及命令参数的个数,分别将参数和参数个数保存到客户端状态argv属性和argc属性里面。
  3. 调用命令执行器,执行客户端指定的命令。

服务器执行命令过程

  1. 命令执行器:查找命令实现
  2. 命令执行器:执行预报操作
  3. 命令执行器:调用命令的实现函数
  4. 命令执行器:执行后续操作
  5. 将命令回复发送给客户端

命令请求从发送到完成主要步骤

  1. 客户端将命令请求发送给服务器;
  2. 服务器读取命令请求,并分析出命令参数;
  3. 命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复;
  4. 服务器将命令回复返回给客户端。

serverCron函数默认每隔100毫秒执行一次,它的工作主要包括更新服务器状态信息,处理服务器接收的SIGTERM信号,管理客户端资源和数据库状态,检查并执行持久化操作。

服务器启动到处理客户端命令过程

  1. 初始化服务器状态;
  2. 载入服务器配置;
  3. 初始化服务器数据结构;
  4. 还原数据库状态;
  5. 执行事件循环。

Redis - 主从复制

用户可以通过执行slaveof命令或者设置slaveof选项,让一个服务器去复制另一个服务器。
被复制的服务器为主服务器(master);对主服务器进行复制的服务器则被称为从服务器(slave)。

旧版主从同步过程 (只支持完整重同步)

  1. 从服务器向主服务器发送SYNC命令;
  2. 收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令;
  3. 当主服务器的BGSAVE命令执行完毕时,主服务器会将BGSAVE命令生成的RDB文件发送给从服务器,从服务器接收并载入这个RDB文件,将自己的数据库状态更新至主服务器执行BGSAVE命令时的数据库状态;
  4. 主服务器将记录在缓冲区里边的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器数据库当前所处的状态。

主从命令传播

主服务器会将自己执行的写命令,发送给从服务器执行。

新版主从同步 (从redis2.8版本开始支持)

部分重同步实现

心跳检测

Redis - 哨兵(Sentinel)

哨兵是Reids的高可用性解决方案:由一个或多个哨兵实例组成的哨兵系统可以监视任意多个主服务器,以及这些主服务器属下的从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。

Redis - 集群

为什么是16384(2^14)个?

在redis节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,16384=16k,在发送心跳包时使用char进行bitmap压缩后是2k(2 * 8 (8 bit) * 1024(1k) = 2K),也就是说使用2k的空间创建了16k的槽数。
虽然使用CRC16算法最多可以分配65535(2^16-1)个槽位,65535=65k,压缩后就是8k(8 * 8 (8 bit) * 1024(1k) = 8K),也就是说需要需要8k的心跳包,作者认为这样做不太值得;并且一般情况下一个redis集群不会有超过1000个master节点,所以16k的槽位是个比较合适的选择。

Redis集群脑裂

redis集群脑裂是指因为网络问题,导致redis master节点和redis slave节点和哨兵集群处于不同的网络分区,此时哨兵集群无法感知到master的存在,所以将salve节点提升为master节点,此时存在两个不同的master节点,就像一个大脑分裂成了两个。
如果此时客户端还在基于原来的master节点继续写入数据,那么新的master节点将无法同步这些数据,当网络问题解决之后,哨兵集群将原来的master节点降为slave节点,此时再从新的master中同步数据,将会造成大量的数据丢失。

解决方案

修改redis配置文件两个参数

min-slaves-to-write 3 # 表示控制连接到master的最少slave数量,否则拒绝写入
min-slaves-max-lag 10 # 表示slave连接到master得最大延迟时间,数据复制和同步延迟不能超过10秒,否则master拒绝写入

控制master写入时的slave数量,必须超过所有slave数量的一半以上时才允许写入,防止同时写入两个master,
如果发生集群脑裂,原先的master节点则会拒绝写入请求,可以减少数据同步之后的数据丢失。

Redis - 事务

Redis - Lua脚本

Redis - 排序

Redis - 二进制位数组

二级制位数组使用

Redis提供了setbit、getbit、bitcount、bitop四个命令用于处理二进制位数组
setbit命令用户为位数组指定偏移量上的二进制位设置值,位数组的偏移量从0开始计算,而二进制位的值则可以是0或者1;

    setbit bit 0 1 // 设置 0000 0001
    setbit bit 3 1 // 设置 0000 1001
    setbit bit 0 0 // 设置 0000 1000
    
    getbit bit 0 // get 0000 1000
    (integer) 0
    getbit bit 3 // get 0000 1000
    (integer) 1
    
    bitcount bit // 0000 1000 统计位数组中,值为1的二进制位的数量
    (interger) 1
    bitcount bit // 0000 1001 统计位数组中,值为1的二进制位的数量
    (interger) 2
    
    //bittop命令可以对多个位数组进行按位与运算、按位或运算、按位异或运算
    bittop and and_result_key bit1 bit2 bit3
    bittop or or_result_key bit1 bit2 bit3
    

Redis使用SDS来保存位数组,SDS使用逆序来保存位数组,这种保存顺序简化了SETBIT命令实现

二进制位数组应用场景

用户签到场景

每天的日期字符串作为key,用户ID作为offset,统计每天用户签到情况,总的用户签到数量。

活跃用户统计

用户日活、月活、留存率等均可以用位数组存储,以每天的日期作为key,用户活跃了就写入osffset用用户id的位值为1。

用户是否在线以及在线人数统计

使用一个位数组,用户ID映射偏移量,在线标识为1,下线标识为0,即可实现用户上下线查询和总得在线人数统计。

APP内用户全局消息提示小红点

App站内信功能,当有消息的时候,提示一个小红点,代表用户有新的消息。

Redis 缓存问题

缓存穿透

缓存穿透问题描述

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从DB查不到数据则不写入缓存,这将导致这个不存在的数据每次都要到DB去查询,失去了缓存的意义。在流量大时,DB可能会挂掉,黑客可以利用不存在的key频繁攻击我们的应用。

缓存穿透解决方案

缓存雪崩

缓存雪崩问题描述

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致大量缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重发生雪崩效应。

缓存雪崩解决方案

缓存击穿

缓存击穿问题描述

对于一些设置了过期时间的缓存key,如果这些key在某个时间点被超高并发访问,是一种非常热点的数据。和缓存雪崩的区别是针对某一个key缓存,缓存雪崩是多个key。
热点缓存key在某个时间点过期的时候,恰好这个时间点有大量并发请求这个缓存key,这些请求发现缓存key已经过期会从后端DB加载数据并回填缓存 ,这个大并发请求可能会瞬间把DB压垮。

缓存击穿解决方案

Redis - 热点key

热key问题描述

热key问题就是突然有几十万的请求去访问redis上的某个特定key,那么这样会造成流量过于集中,达到物理网卡上限,从而导致这台redis服务器直接宕机。

如何发现热点key

如何解决热key

有赞透明多级缓存解决方案(TMC)

TCM对原生jedis包得jedisPool和jedis类做改造,在JedisPoll初始化过程中集成了TCM“热点发现”+“本地缓存"功能Hermes-sdk包得初始化逻辑。

Redis - 过期策略和内存淘汰策略

Redis内存过期策略

Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
过期策略通常有以下三种:

(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)
Redis中同时使用了惰性过期和定期过期两种过期策略。

Redis的内存淘汰策略

Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

总结

Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。

Redis - 高性能原因

Redis分布式锁

Redis分布式锁实现

Redis 锁主要利用 Redis 的 setnx 命令。

存在的问题

if (redis.call('setnx', KEYS[1], ARGV[1]) < 1)
then return 0;
end;
redis.call('expire', KEYS[1], tonumber(ARGV[2]));
return 1;
// 解锁
if (redis.call('get', KEYS[1]) == ARGV[1])
    then return redis.call('del', KEYS[1])
else return 0
end
// 如果 lock_key 不存在
if (redis.call('exists', KEYS[1]) == 0)
then
    // 设置 lock_key 线程标识 1 进行加锁
    redis.call('hset', KEYS[1], ARGV[2], 1);
    // 设置过期时间
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
    end;
// 如果 lock_key 存在且线程标识是当前欲加锁的线程标识
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)
    // 自增
    then redis.call('hincrby', KEYS[1], ARGV[2], 1);
    // 重置过期时间
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
    end;
// 如果加锁失败,返回锁剩余时间
return redis.call('pttl', KEYS[1]);
上一篇 下一篇

猜你喜欢

热点阅读