Redis原理扫盲(长篇)

简介

总结redis常用的知识点,及其应用。

数据结构


Redis对象通用对象
// 所有redis结构都有这个头
struct RedisObject {
    int4 type; // 4bits
    int4 encoding; // 4bits
    int24 lru; // 24bits
    int32 refcount; // 4bytes
    void *ptr; // 8bytes,64-bit system
} robj;

字符串string

动态字符串,可修改,使用预分配空间方式减少内存频繁分配。

常用操作

set、mset、get、mget、setex(设置过期)、expire(指定过期)

incr(针对数字递增1)、incrby(递增N)

数据结构
struct SDS<T> {
  T capacity; // 数组容量
  T len; // 数组长度
  byte flags; // 特殊标识位,不理睬它
  byte[] content; // 数组内容
}
存储方式

长度小于44字节使用embstr方式存储,否则使用raw方式存储,查看命令如下:

127.0.0.1:6379> debug object g1
Value at:0x7f4cd72a43c8 refcount:1 encoding:embstr serializedlength:4 lru:15045668 lru_seconds_idle:2
为什么是44字节
  1. jemalloc/tcmalloc可以分配32、64字节大小的内存,超过64字节则被认为是大内存。
RedisObject + SDS = 19字节
64字节 - 19字节 = 45字节(其中\0为redis字符串的结尾)
  1. CPU缓存行为64字节,embstr长度和cpu cache line保持一致可以更快加载、重用缓存行
扩容

小于1M加倍扩容,超过1M按照1M的扩容至512M。


列表list

非数组所以插入、删除很快(O1),但是定位(On)常用在异步队列操作。

常用操作

rpush、lpop、rpop、llen、lrange、ltrim(修剪列表)等等


快速列表quicklist

在元素较少时使用压缩链表ziplist,元素较多使用quicklist因为指针占用内存较多所以不用双向链表实现

数据结构
struct ziplist {
    ...
}
struct ziplist_compressed {
    int32 size;
    byte[] compressed_data;
}
struct quicklistNode {
    quicklistNode* prev;
    quicklistNode* next;
    ziplist* zl; // 指向压缩列表
    int32 size; // ziplist 的字节总数
    int16 count; // ziplist 中的元素数量
    int2 encoding; // 存储形式 2bit,原生字节数组还是 LZF 压缩存储
    ...
}
struct quicklist {
    quicklistNode* head;
    quicklistNode* tail;
    long count; // 元素总数
    int nodes; // ziplist 节点的个数
    int compressDepth; // LZF 算法压缩深度
    ...
}

支持LZF算法压缩,以及设定压缩深度。

存储方式

查看数据结构信息

127.0.0.1:6379> debug object g2
Value at:0x7f4cd7228710 refcount:1 encoding:quicklist serializedlength:18 lru:15046649 lru_seconds_idle:270 ql_nodes:1 ql_avg_node:2.00 ql_ziplist_max:-2 ql_compressed:0 ql_uncompressed_size:16

在这里插入图片描述

特点
  1. 顺序存储
  2. 不需要指针(针对ziplist)
  3. 高速缓存(cpu cache line)
  4. 直接用偏移访问很快
  5. O1的头尾插入

但是缺点是:

  1. 删除ziplist中节点需要数据搬移

整数集合intset

如果存储整数集合,为了减少存储空间redis使用inset存储以便节省空间

例如使用uint16可以存储全部整数类型,如果超过了uint16范围则使用uint32存储


字典
常用操作

hset key field value

hget key field

hgetall key

hmset key field value …

hincrby key field 1 (某个字段单独自增) 等等…

使用场景

字典结构、redis的全部key存储、zset集合存储value和score的映射关系。

数据结构
typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx;
    unsigned long iterators;
} dict;

包含了2个字典结构其中一个在扩容时会用到

struct dictEntry {
    void* key;
    void* val;
    dictEntry* next; // 链接下一个 entry
}
struct dictht {
    dictEntry** table; // 二维
    long size; // 第一维数组的长度
    long used; // hash 表中的元素个数
    ...
}

默认的hash函数是:siphash,性能好、生成随机数效果好。

用分桶的方式解决hash冲突。

扩容

字典如果元素很多扩容时需要重新申请内存,然后迁移数据这是一个十分耗时的动作,所以redis使用渐进式方式rehash。

如果使用了hset、hdel等指令在处于迁移过程中,将新元素挂接到新数组(ht[1])上,旧数组(ht[0])不会写入数据。另外有定时任务定期异步线程搬移数据。

关于扩容的问题

扩容条件:当元素和容量比例1:1时进行扩容,如果处于bgsave、bgaof过程则不会只想扩容,但是当容量快速增加到1:5时强制开始扩容操作

触发扩容的指令:新增、更新、删除操作。会查询ht[0],若存在key则迁移这个key所在的桶并返回元素,如果不存在则ht[1]查找

扩容针对某个db还是全部:针对单个db

高位进位法扩容

字典使用分桶的方式解决hash冲突,查找hashkey时先找到位置,然后往下找item。

看一下高位进位法扩容。例如槽号为3,此时发送了扩容:

在这里插入图片描述

可以看下高位进位结果:例如槽号3扩容后的槽3和11在遍历上是相邻的,当扩容时当前槽位从011扩容为0011和1011,则我们直接可以从0011槽位开始遍历避免之前的数据重新遍历。缩容同理,但是由于数据合并可能会有对当前操作数据有些重复遍历。

在这里插入图片描述

字典遍历

redis提供2种迭代器,分别是安全、非安全迭代器。

typedef struct dictIterator {
    dict *d; // 目标字典对象
    long index; // 当前遍历的槽位置,初始化为-1
    int table; // ht[0] or ht[1]
    int safe; // 这个属性非常关键,它表示迭代器是否安全
    dictEntry *entry; // 迭代器当前指向的对象
    dictEntry *nextEntry; // 迭代器下一个指向的对象
    long long fingerprint; // 迭代器指纹,放置迭代过程中字典被修改
} dictIterator;

非安全迭代器是只读操作,不影响rehash过程,但是可能遍历一些重复元素。另外迭代前和迭代后指纹fingerprint(MD5码)不可以被修改,否则服务会异常。这里元素修改不会有影响,但是结构变化例如size变化则指纹会修改。

安全迭代器(dict结构中的iterators表示安全迭代器数量)

迭代器的使用场景:keys使用安全迭代器保证结果不重复、bgaof和bgsave需要遍历对象持久化也是使用安全迭代器、如果需要处理过期操作同理。

其它情况下,允许遍历过程出现元素重复,不修改字典的场景使用非安全迭代器。

hash攻击

利用key生成比较集中,导致字典单桶数据太多,访问时间复杂度为On降低系统性能。


集合set

底层结构使用字段,只不过value为null

常用操作

sadd key value

smember key(可能无序)

sismember key value(是否存在)

scard key(获取长度)

spop key(弹出一个)


跳表skiplist
常用操作

zadd key score value

zrange key 0 1(也可以0到-1)

zrevrange key 0 -1(逆序输出)
zrem key value (删除)
zcard key(相当于count)
zrank key field(获取排名)
zrangebyscore students 0 80.5 (根据分值区间遍历 zset)
zrangebyscore students -inf 80.5 withscores (根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 代表 infinite,无穷大的意思。)

使用场景

数据具有唯一性。使用场景很多例如:value为粉丝ID,score为关注时间,则可以根据关注时间排序粉丝。又或者value为学生ID,score为考试成绩,则可以快速排行。

数据结构
struct zslforward {
  zslnode* item;
  long span;  // 跨度为了快速计算排名
}

struct zslnode {
  String value;
  double score;
  zslforward*[] forwards;  // 多层连接指针
  zslnode* backward;  // 回溯指针
}
struct zsl {
  zslnode* header; // 跳跃列表头指针
  int maxLevel; // 跳跃列表当前的最高层
  map<string, zslnode*> ht; // hash 结构的所有键值对
}

在这里插入图片描述
元素少于128+member长度小于64字节用ziplist否则skiplist

使用ziplist存储时:

​ ziplist第一个节点保存member、第二个节点保存score,score从小到大排序。

使用skiplist储存时:

​ score从小到大保存,字典保存从value到score的映射,这样可以快速从value查询到score。

跳表的构建

跳跃列表采取一个随机策略来决定新元素可以跨越到第几层。这里随机概率为25%,且redis最大层高为64。

插入过程

搜索路径、创建节点、分配层数(随机)、修改向前向后指针、修改最大高度

删除过程

同插入类型

更新

这里redis比较粗暴,如果是score修改则先删除后插入的策略更新节点

快速获取排名

forwards中的span字段表示从当前节点沿着zslforward指针跳到下一个节点中间跳过了多少节点,在节点插入、删除时会同时更新span的值,这样在计算rank值时,只需要将所有节点跨度span值进行叠加即可。

特点
  1. 较耗内存,因为需要重复分层存节点,但是也可以通过调参降低内存消耗(节点升级的概率)
  2. 双向链表连接节点,方便范围操作,另外操作更区域化更好的利用缓存
  3. zrank操作O(log(N))的时间复杂度

对比平衡树:

1. 插入时修改前后指针,平衡树可能需要做一些`rebalance`操作涉及到树的其他部分
2. 平衡树的顺序输出需要中序遍历,而skiplist使用双向链表快速输出
3. 内存上平衡树需要2个指针节点,跳表(redis)只需要平均1.33个指针即可

压缩列表ziplist
数据结构
struct ziplist<T> {
    int32 zlbytes; // 整个压缩列表占用字节数
    int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
    int16 zllength; // 元素个数
    T[] entries; // 元素内容列表,挨个挨个紧凑存储
    int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
}

struct entry {
    int<var> prevlen; // 前一个 entry 的字节长度(倒叙遍历)
    int<var> encoding; // 元素类型编码
    optional byte[] content; // 元素内容
}

在这里插入图片描述
压缩列表的每个元素紧凑一起,随着容纳元素不同结构也不一样。

当元素小于254时,prevlen为1字节,否则为5字节(其中开头为0xFE)。

另外对于很小的整数,数值内联到了encoding中以便节省空间(设计比较复杂)。

级联更新问题

由于ziplist的后一个entry都会记录前一个entry的字节长度,所以如果entry的长度从253变成254则下一个entry的pervlen字段就需要从1字节更新到5字节,如果再后面的entry也是253字节那么也会更新


紧凑列表listpack

Redis5.0中引入了紧凑列表listpack,对ziplist进行了改进,另外在存储空间也会更加节省。

数据结构
struct listpack<T> {
    int32 total_bytes; // 占用的总字节数
    int16 size; // 元素个数
    T[] entries; // 紧凑排列的元素列表
    int8 end; // 同 zlend 一样,恒为 0xFF
}

struct lpentry {
    int<var> encoding;
    optional byte[] content;
    int<var> length;
}

在这里插入图片描述
​ 这里没有使用zltail_offset字段来定位最后一个元素,因为length在entry的最后面,通过total_byteszlend则可以得到最后一个元素位置,通过尾部length可以计算出元素大小。

​ 另外长度字段使用varint进行编码,不同于 skiplist 元素长度的编码为 1 个字节或者 5 个字节,listpack 元素长度的编码可以是1、2、3、4、5个字节。同 UTF8 编码一样,它通过字节的最高为是否为 1 来决定编码的长度(同样设计复杂,不做简介)。

解决级联更新问题

listpack的设计彻底消灭了ziplist 存在的级联更新行为,元素与元素之间完全独立,不会因为一个元素的长度变长就导致后续的元素内容会受到影响。

为什么listpack比ziplist更好
  1. 解决了级联更新问题,元素间相互独立
  2. 反向遍历无须维护tail偏移
  3. 占用空间少,listpack基础结构少了4byte
取代ziplist

listpack 的设计的目的是用来取代 ziplist,不过当下还没有做好替换 ziplist 的准备,因为有很多兼容性的问题需要考虑,ziplist 在 Redis 数据结构中使用太广泛了,替换起来复杂度会非常之高。它目前只使用在了新增加的 Stream 数据结构中。


基数树

Rax(基数树Radix Tree)属于字典树,按照key的字典序排序,支持快速定位、插入、删除。

struct raxNode {
    int<1> isKey; // 是否没有 key,没有 key 的是根节点
    int<1> isNull; // 是否没有对应的 value,无意义的中间节点
    int<1> isCompressed; // 是否压缩存储,这个压缩的概念比较特别
    int<29> size; // 子节点的数量或者是压缩字符串的长度 (isCompressed)
    byte[] data; // 路由键、子节点指针、value 都在这里
}

在这里插入图片描述

压缩存储

如果只有一个子节点,那么路由键就是一个字符串,例如上图的“许多”路由键就是any。这就是压缩形式的存储。

应用

Rax被用在Redis Stream(见下文)结构中用于存储消息队列,在Stream中消息ID是:时间戳+序号,Rax可以快速根据消息ID定位到具体的消息,然后继续遍历后续的消息。


位图

和普通的位图结构、使用上都一样,这里列出使用指令

setbit x 1 1 (第一位为1)
getbit x 2 (获取第二位)
bitcount x 0 10(位图统计)
bitpos b 1 0 10 (从0-10查找第一个1的位置)

bitfield 一次操作多个指令


布隆过滤器

​ 例如在推荐场景,看新闻不停推荐新内容,这些内容必须是不同的,所以我们需要记录全部用户的记录,所以这里要考虑如何缓存、如何快速查询。

​ 布隆过滤器 (Bloom Filter) 就是专门解决这种去重问题,在去重同时还能节省90%以上的空间,只是稍微没有那么精确,存在误判率。

​ 如果一个元素被判断存在时,这个值可能不存在,但是如果判断不存在,则该元素肯定不存在。所以我们在使用的时候如果判断存在,则需要继续查询缓存(或者其他存储)来判断元素是否真的存在。

常用操作

bf.add (添加元素)

bf.exists(查询元素是否存在)

bf.madd (批量添加元素)

bf.mexists(批量查询元素是否存在)

使用场景

邮件过滤、推荐内容过滤、爬虫URL、DB查询磁盘。

原理

布隆过滤器对应在redis中就是一个大的位数组,以及多个hash函数。

添加

向布隆过滤器中添加key时,会使用多个hash函数对key进行hash计算得到一个整数数值,然后对数组长度取模得到多个index,这样将多个index值都置位1即可。

查询

同理会使用多个hash函数对key进行hash计算得到一个整数数值,然后对数组长度取模得到多个index,然后查询数组这些位置是否都为1即可。

如果这个位数组比较稀疏,判断正确的概率就会很大,如果这个位数组比较拥挤,判断正确的概率就会降低。

在这里插入图片描述

空间占用预估

这里是布隆过滤器空间占用计算器

这是公式推导过程

k=0.7*(l/n)  # 约等于
f=0.6185^(l/n)  # ^ 表示次方计算,也就是 math.pow
  1. 位数组相对越长 (l/n),错误率 f 越低,这个和直观上理解是一致的

  2. 位数组相对越长 (l/n),hash 函数需要的最佳数量也越多,影响计算效率

  3. 当一个元素平均需要 1 个字节 (8bit) 的指纹空间时 (l/n=8),错误率大约为 2%

  4. 错误率为 10%,一个元素需要的平均指纹空间为 4.792 个 bit,大约为 5bit

  5. 错误率为 1%,一个元素需要的平均指纹空间为 9.585 个 bit,大约为 10bit

  6. 错误率为 0.1%,一个元素需要的平均指纹空间为 14.377 个 bit,大约为 15bit


模糊统计HyperLogLog

​ HyperLogLog算法是一种非常巧妙的近似统计海量去重元素数量的算法。例如我们想统计统计网站主页uv(一个用户1天统计1次,但是不要求完全准确)这时候我们使用其他组件实现起来会比较麻烦,在访问量很大的时候也可能比较占内存。

​ 此时使用HyperLogLog十分合适,误差0只有.81%可能更低,但是存储只占用12K,计数小时存储使用稀疏矩阵存储空间占用小。

实现

​ 一系列整数0位的最大连续长度(代表数量级),这一系列值均分到16384(2^14)个桶中,取调和平均数作为整体的预估值。

​ 占用空间:每个桶的maxbits需要6bits存储*(记录对数),最大maxbits=63,2^14 * 6 / 8 = 12k字节。


PubSub发布订阅

支持消息多播,即消息生产一次中间件将消息复制到多个消息队列。

在这里插入图片描述

缺点

消息丢失问题

​ Redis 会直接找到相应的消费者传递过去。如果一个消费者都没有,那么消息直接丢弃。如果开始有三个消费者,一个消费者突然挂掉了,生产者会继续发送消息,另外两个消费者可以持续收到消息。但是挂掉的消费者重新连上的时候,这断连期间生产者发送的消息,对于这个消费者来说就是彻底丢失了。

高可用问题

​ 如果 Redis 停机重启,PubSub 的消息是不会持久化的,毕竟 Redis 宕机就相当于一个消费者都没有,所有的消息直接被丢弃。


Stream

​ Redis5.0新增,支持多播的可持久化消息队列,多个消费组独立游标,同组竞争消费。

​ 解决了Redis Pub/Sub不能持久化消息的问题,但是不同于kafka这些消息队列,Stream不支持分区。

常用操作

xadd(增加消息)
xdel(删除消息)
xrange(获取消息列表,过滤已删除的)
xlen(消息长度)
del(删除stream)

xread(独立消费,阻塞消费)
xread block 0 count 1 streams key(阻塞从尾部读取1个数据)

详细全部指令还是看下官方文档,或者找一些示例再看下。

结构图

在这里插入图片描述

几个问题分析

消息太多怎么处理

因为xdel其实不会删除指令只是做了一个标记位,只有总数据长度超过maxlen时才会触发删除操作。

消息没有ACK怎么处理

每个消费者中都保存了正在处理中的消息ID列表PEL,如果消费者受到消息处理完成但是没有ACK会导致PEL列表不断增长,如果消费者很多这个列表就会很长。

PEL如何避免消息丢失

服务端保存了ID列表,如果客户端突然断开,等待下一次连接后,可以再次拿到PEL的全部消息ID。

高可用

在主从赋值的基础上,和其他数据结构复制机制一样,因为Redis指令是异步复制的所以在极端情况下还是可能会丢失数据。


限流模块Cell

Redis4.0中新增的漏桶限流组件

使用方式
cl.throttle allen:reply 15 30 60 1
# key allen
# 15 capacity 这是漏斗容量
# 30 operations / 60 seconds 这是漏水速率
# need 1 quota (可选参数,默认值也是1)

上面这个指令的意思是允许「用户Allen助力行为」的频率为每 60s 最多 30 次(漏水速率),漏斗的初始容量为 15,也就是说一开始可以连续助力15个人,然后才开始受漏水速率的影响。

cl.throttle laoqian:reply 15 30 60
1) (integer) 0   # 0 表示允许,1表示拒绝
2) (integer) 15  # 漏斗容量capacity
3) (integer) 14  # 漏斗剩余空间left_quota
4) (integer) -1  # 如果拒绝了,需要多长时间后再试(漏斗有空间了,单位秒)
5) (integer) 2   # 多长时间后,漏斗完全空出来(left_quota==capacity,单位秒)

集群与高可用


数据持久化
快照

数据二进制序列化存储,结构紧凑。基于COW(copy on write)实现。

COW:父进程创建子进程时,两个进程共享代码、数据段以便节约内存,只有数据发送变更才会触发内存拷贝。所以子进程等同于快照,直接写入数据到磁盘即可。

AOF
  1. AOF日志,连续增量备份,记录执行指令,但是需要定期合并重写。对一个空Redis执行AOF则包含全部数据。
  2. 先执行指令才日志存盘。
  3. 重写:开启一个新AOF文件,然后瘦身,随后将增量记录追加进去
  4. fsync:文件写操作都是到内存缓冲,然后内核异步刷盘,fsync同步一次
  5. 持久化:一般是从节点进行,因为没有请求不会有数据不一致问题
  6. 如果出现了网络分区,从节点了长期连不上主,如果主挂了就丢失数据,此时只能靠监控来防止
混合持久化

​ 重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。

​ Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。


主从同步

增量同步

主写入记录到buffer(环形数组),然后从机去同步,并反馈同步到了哪里。

快照同步

主bgsave到磁盘(遍历数据),然后发送给从机,从机执行全量加载(加载前清空数据)。

存在的问题

如果环形buffer太小会触发全量同步。

新节点加入

从节点第一次加入,会触发一次快照同步,随后增量同步。

无盘复制

因为每次落盘太消耗资源,所以主节点可以直接发送数据到从节点,从节点落盘数据后加载数据。

主机不会发生写IO操作。

wait指令

wait指令:wait 1 0 同步N(1)个从库等待(0为无限等待)数据同步完成


哨兵机制

​ Redis Sentinel保证高可用,实现自动切换主,无须防止运维接入。类似zookeeper,使用3-5个节点组成一个集群。
​ 负责监控主从节点健康,主挂了选优从变成主。客户端连接时也是先连接Sentinel询问主的地址,主故障,需要重新找Sentinel要地址,程序无需重启。 即使原来的主恢复也是从新主重新复制数据。

存在的问题

但是主从异步复制还是会丢数据,Sentinel尽量保证少丢数据。

涉及的配置文件

min-slaves-to-write 1 至少一个从节点正在复制,否则停止写
min-slaves-max-lag 10 10秒都没有收到从节点反馈表示从节点同步异常

流程图

在这里插入图片描述

Cluster集群
结构图

​ 它是去中心化的,如图所示,该集群有三个 Redis 节点组成,每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。这三个节点相互连接组成一个对等的集群,它们之间通过 Gossip协议相互交互集群信息。

在这里插入图片描述

​ 将所有数据划分为16384个槽,key对crc16然后hash得到一个整数对16384取模。由于节点心跳要带上槽信息,这里槽信息使用bitmap存储16384个bit只占用2K空间,理论上集群不会超过1000,所以不需要更大的槽。

​ 如果某个节点错误的槽号对应key,将会回复客户端跳转指令让其链接指定正确的槽。

迁移

​ Redis迁移的单位是槽,一个槽一个槽的迁移。迁移过程:从源节点获取内容=>存到目标节点=>从源节点删除内容,这里是同步迁移,主线程会处理阻塞状态,直到key被成功删除。

网络故障

​ 如果迁移过程中突然出现网络故障,整个slot的迁移只进行了一半。这时两个节点依旧处于中间过渡状态。待下次迁移工具重新连上时,会提示用户继续进行迁移。

大key问题

​ 如果key都比较小,迁移可以很快执行,不会有卡顿现象,但是如果key很大,因为迁移是阻塞指令会同时导致原节点和目标节点卡顿,影响集群的稳定型。所以在集群环境下业务逻辑要尽可能避免大key的产生。

容错

​ Redis Cluster 可以为每个主节点设置若干个从节点,单主节点故障时,集群会自动将其中某个从节点提升为主节点。如果某个主节点没有从节点,那么当它发生故障时,集群将完全处于不可用状态。不过 Redis 也提供了一个参数cluster-require-full-coverage可以允许部分节点故障,其它节点还可以继续提供对外访问。

网络抖动

​ 为了防止网络抖动造成的机器下线(可能波动后网络很快恢复),这里支持某个节点持续timeout时间才会认为故障,如果是主节点则会执行主从切换。

关于Slave节点是否分担读压力

​ Redis中Slave节点主要是做高可用,不分担查询压力。redis 定位就是支持高性能的查询的,所有主节点就够了。


Redis多线程
Redis4.0多线程

数据支持后台删除,避免主线程卡顿。

Redis6.0多线程

​ 因为Redis使用IO多路复用非阻塞IO,但是IO读写本身是阻塞的,数据从内核态空间拷贝到用户态空间给Redis调用,拷贝过程是阻塞的。

​ 这里监控文件读写事件,通知线程执行去执行IO操作,保证主线程非阻塞。

​ 即使socket读写并行化,但是Redis命令依旧主线程串行执行,这里Redis默认没有开启这个选项,有大佬实际测试性能提升了1倍!

数据过期与删除


key过期

Redis将全量设置过期的key存储到字典。

过期策略

定期过期、惰性过期(客户端访问时)

定期过期

每秒10次扫描,使用贪心策略过期key

  1. 从过期字段随机选取20key、删除20key中过期的,如果过期比例超过1/4则重复执行。
  2. 扫描时间不会大于25ms,并且此时是阻塞的,所以可能客户端访问超时但是slowlog查不到慢查询日志,因为这里是等待而不是逻辑处理慢导致的耗时。

大key问题

这里如果定期过去遇到大key,此时删除key将会导致卡顿,并且redis内存回收时也会造成cpu飙高。


分布式锁
setnx

​ setnx(set if not exists)允许一个客户端占用,用完了在调用del指令释放。

​ 这里的问题就是如果del没有被调用,这样就会进入死锁。

解决办法

​ 我们在加到锁后增加过期时间,例如:

setnx lock:allen true
expire lock:allen 5

但是这里的问题是如果setnx和expire之间服务器出现问题,导致expire不能正确执行导致死锁。

set扩展指令

Redis2.8增加了扩展指令将setnx和expire一起执行。

set lock:allen true ex 5 nx

使用lua脚本

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

超时问题

​ Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候第一个线程持有的锁过期了,临界区的逻辑还没有执行完,这个时候第二个线程就提前重新持有了这把锁,导致临界区代码不能得到严格的串行执行。

Redlock算法

获取锁执行步骤如下:

  1. 获取当前时间(毫秒数)。
  2. 按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过期时间(比如PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。
  3. 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。
  4. 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。
  5. 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的Redis Lua脚本)。

释放锁的过程比较简单:客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否。


分布式锁
近似LRU

​ Redis如果改造成完全LRU,对数据结构设计影响较大,且消耗额外内存,所以Redis使用了近似LRU算法。

实现

​ 每个key增加时间戳记录最后一次访问时间,Redis写操作时,如果内存超了maxmemory,随机取5个淘汰最旧的元素,直到内存正常。

改进

​ Redis3.0增加淘汰池,进一步优化数据过期的策略。随机待过期的key和淘汰池的多个key对比时间戳,淘汰最旧的key,剩余的保留的等待下次触发。

下面是随机 LRU 算法和严格 LRU 算法的效果对比图:

在这里插入图片描述


LFU

Redis中增加LFU算法按照访问频率进行淘汰,它比 LRU 更加精准地表示了一个 key 被访问的热度。

数据结构

每个对象都会保存热度信息。因为存储的是对数值,所以更新字段时使用概率方法递增。

// redis 的对象头
typedef struct redisObject {
    unsigned type:4; // 对象类型如 zset/set/hash 等等
    unsigned encoding:4; // 对象编码如 ziplist/intset/skiplist 等等
    unsigned lru:24; // 对象的「热度」
    int refcount; // 引用计数
    void *ptr; // 对象的 body
} robj;

惰性删除

Redis维护了一个队列专门交给异步线程消费任务执行。

  1. Redis4.0 unlink key交给后台线程异步删除(key过期、LRU淘汰等)
  2. flushdb flushall缓慢清空数据库,增加async指令,扔到异步任务队列后台线程执行
  3. AOF也是异步队列执行
具体步骤

​ 执行懒惰删除时,Redis 将删除操作的相关参数封装成一个bio_job结构,然后追加到链表尾部。异步线程通过遍历链表摘取 job 元素来挨个执行异步任务。

struct bio_job {
    time_t time;  // 时间字段暂时没有使用,应该是预留的
    void *arg1, *arg2, *arg3; // 分别是普通对象、字典、基数树对象
};

其他


Gen地理位置模块

将二维坐标映射到一维平面中,二维越接近,一维越接近。
具体做法:二维地图二分切割,一个元素在一个小正方体,例如分4块,则元素可以用00-01-10-11的4bit表示,正方形越小,二进制越大,精度越高。

常用操作

新增:geoadd company 116.48105 39.996794 juejin
删除:直接用zset的zrem指令
距离:geodist company juejin ireader km
位置:geopos company juejin
Hash值(base32):geohash company ireader
附近其他元素: georadiusbymember company ireader 20 km count 3 asc

实现原理

Redis将经纬度用52位整数编码放入zset,value为元素的key,查询时因为跳表连续,很方便查询附近的元素。

使用技巧
  1. 建议单Redis实例部署,否则集群迁移数据卡顿
  2. 数据量过大应该根据国家、地区、市单独划分出来key

Scan扫描数据

获取全部key:keys c(c开头的key)但是复杂度O(n)主线程扫描较慢影响线上业务。

Redis2.8增加Scan,通过游标渐进式进行,提供limit获取数量,需但是要客户端去重(扩容时)

常用操作

scan 0 match key* count 1000 (开始扫匹配1000个,返回匹配的数据)

定位大key

定位大key:scan命令逐步拿到全部key使用对应接口的size、len获取大小
redis的方法:redis-cli -h 127.0.0.1 -p 7001 –-bigkeys

具体如下:

redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1扫描100次休眠0.1秒


定时任务

Redis维护了一个任务小顶堆,记录下次执行的timeout时间,每个循环周期查询堆顶即可(Nginx同理)。


管道

​ Redis管道(流水线),正常的 req => rsp,req => rsp,我们可以合并先req => req,然后收到2个rsp,这样称为管道,合并请求,减少了网络调用极大的提高吞吐能力。

​ 正常write很快,但是read要等待对端返回。但是并行之后,收到第一个read说明其他的响应也在内核缓冲区了,可以快速读取。

压测试验证
redis-benchmark -t set -q      # 测试10w/s
redis-benchmark -t set -P 2 -q  # 测试20w/s,增加-P参数最多到100w/s。

事务

multi事务开始、exec事务执行、discard事务丢弃。redis服务器缓存执行,收到exec执行。

事务的问题

非原子性,失败了不回滚,仅仅是满足隔离性。


内存管理
内存管理

使用facebook的jemalloc做内存管理(info命令可以查看)

参考

  1. 掘金《Redis深度历险:核心原理与应用实践》
  2. 基于Redis的分布式锁到底安全吗?
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值