redis深度历险:核心原理与应用全书总结

1.redis的类型有string,list,hash,set,zset

2.list类似java的linkedlist,内部是链表不是数组,这意味着插入删除操作非常快,查询相对比较慢

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

hash (字典) 
Redis 的字典相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同 
Java 的 HashMap 也是一致的,同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞
时,就会将碰撞的元素使用链表串接起来。 
 不同的是,Redis 的字典的值只能是字符串,

zset (有序列表)  
zset 可能是 Redis 提供的最为特色的数据结构,它也是在面试中面试官最爱问的数据结
构。它类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 
value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权
重。它的内部实现用的是一种叫着「跳跃列表」的数据结构。
zset 还可以用来存储学生的成绩,value 值是学生的 ID,score 是他的考试成绩。我们
可以对成绩按分数进行排序就可以得到他的名次

跳跃列表(类似java的跳表Skiplist)

 

过期时间

Redis 所有的数据结构都可以设置过期时间,时间到了,Redis 会自动删除相应的对象。
需要注意的是过期是以对象为单位,比如一个 hash 结构的过期是整个 hash 对象的过期,
而不是其中的某个子 key。  
还有一个需要特别注意的地方是如果一个字符串已经设置了过期时间,然后你调用了 
set 方法修改了它,它的过期时间会消失

 

应用一千帆竞发:分布式锁

分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占
时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。 
占坑一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占坑。先来先占, 用
完了,再调用 del 指令释放茅坑
但是有个问题,如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样
就会陷入死锁,锁永远得不到释放。

于是我们在拿到锁之后,再给锁加上一个过期时间,比如 5s,这样即使中间出现异常也
可以保证 5 秒之后锁会自动释放

Redis 2.8 版本中作者加入了 set 指令的扩展参数,使得 setnx 和 
expire 指令可以一起执行,彻底解决了分布式锁的乱象。从此以后所有的第三方分布式锁 
library 可以休息了。 > set lock:codehole true ex 5 nx OK ... do something critical ... > del 
lock:codehole 上面这个指令就是 setnx 和 expire 组合在一起的原子指令,它就是分布式锁的
奥义所在

可重入性  

可重入性是指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程的多次加
锁,那么这个锁就是可重入的。比如 Java 语言里有个 ReentrantLock 就是可重入锁。Redis 分
布式锁如果要支持可重入,需要对客户端的 set 方法进行包装,使用线程的 Threadlocal 变量
存储当前持有锁的计数。 
 

应用 2:缓兵之计 —— 延时队列

Redis 的消息队列不是专业的消息队列,它没有非常多的高级特性,
没有 ack 保证,如果对消息的可靠性有着极致的追求,那么它就不适合使用 

适合用于哪些只有很少的消费者或一组消费者

异步消息队列 (服务器指定key,往list里面插入消息,客户端通过key去消费,消费一个值,list的那个值就消失了)

Redis 的 list(列表) 数据结构常用来作为异步消息队列使用,使用rpush/lpush操作入队列,
使用 lpop 和 rpop 来出队列。

队列空了怎么办?

客户端是通过队列的 pop 操作来获取消息,然后进行处理。处理完了再接着获取消息,
再进行处理。如此循环往复,这便是作为队列消费者的客户端的生命周期

可是如果队列空了,客户端就会陷入 pop 的死循环,不停地 pop,没有数据,接着再 pop,
又没有数据。这就是浪费生命的空轮询。空轮询不但拉高了客户端的 CPU,redis 的 QPS 也
会被拉高,如果这样空轮询的客户端有几十来个,Redis 的慢查询可能会显著增多。 
通常我们使用 sleep 来解决这个问题,让线程睡一会,睡个 1s 钟就可以了。不但客户端
的 CPU 能降下来,Redis 的 QPS 也降下来了
 

有没有什么办法能显著降低延迟呢?你当然可以很快想到:那就把睡觉的时间缩短点。这
种方式当然可以,不过有没有更好的解决方案呢?当然也有,那就是 blpop/brpop。 
这两个指令的前缀字符 b 代表的是 blocking,也就是阻塞读。 
阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。消
息的延迟几乎为零。用 blpop/brpop 替代前面的 lpop/rpop,就完美解决了上面的问题。
 

锁冲突处理 

分布式锁在客户端在处理请求时加锁没加成功怎么办。
一般有 3 种策略来处理加锁失败

1、直接抛出异常,通知用户稍后重试; 
2、sleep 一会再重试; 
3、将请求转移至延时队列,过一会再试

 

应用 3:节衣缩食 —— 位图 

redis提供 setBit,getBit,bitCount统计指令和bitpos查找指令和bitFiled管道指令

 

应用 4:四两拨千斤 —HyperLogLog

Redis 提供了 HyperLogLog 数据结构就是用来解决
这种统计问题的。HyperLogLog 提供不精确的去重计数方案,虽然不精确但是也不是非常不
精确,标准误差是 0.81%,这样的精确度已经可以满足上面的 UV 统计需求了。 
HyperLogLog 数据结构是 Redis 的高级数据结构,它非常有用,但是令人感到意外的
是,使用过它的人非常少。 
比如统计去重ip访问页面量(当有很大量的几百万的访问量)可以粗略的统计

应用 5:层峦叠嶂 —— 布隆过滤器  

讲个使用场景,比如我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内
容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何
实现推送去重的

你会想到服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户
的历史记录里进行筛选,过滤掉那些已经存在的记录。问题是当用户量很大,每个用户看过
的新闻又很多的情况下,这种方式,推荐系统的去重工作在性能上跟的上么
 

实际上,如果历史记录存储在关系数据库里,去重就需要频繁地对数据库进行 exists 查
询,当系统并发量很高时,数据库是很难扛住压力的。 你可能又想到了缓存,但是如此多的历史记录全部缓存起来,那得浪费多大存储空间
啊?而且这个存储空间是随着时间线性增长,你撑得住一个月,你能撑得住几年么?但是不
缓存的话,性能又跟不上,这该怎么办?

这时,布隆过滤器 (Bloom Filter) 闪亮登场了,它就是专门用来解决这种去重问题的。
它在起到去重的同时,在空间上还能节省 90% 以上,只是稍微有那么点不精确,也就是有
一定的误判概率

布隆过滤器是什么?

布隆过滤器可以理解为一个不怎么精确的 set 结构,当你使用它的 contains 方法判断某
个对象是否存在时,它可能会误判。但是布隆过滤器也不是特别不精确,只要参数设置的合
理,它的精确度可以控制的相对足够精确,只会有小小的误判概率。

布隆过滤器有二个基本指令,bf.add 添加元素,bf.exists 查询元素是否存在,它的用法
和 set 集合的 sadd 和 sismember 差不多。注意 bf.add 只能一次添加一个元素,如果想要
一次添加多个,就需要用到 bf.madd 指令。同样如果需要一次查询多个元素是否存在,就需
要用到 bf.mexists 指令
 

我们上面使用的布隆过滤器只是默认参数的布隆过滤器,它在我们第一次 add 的时候自
动创建。Redis 其实还提供了自定义参数的布隆过滤器,需要我们在 add 之前使用 bf.reserve
指令显式创建。如果对应的 key 已经存在,bf.reserve 会报错。bf.reserve 有三个参数,分别 是 key, error_rate 和 initial_size。错误率越低,需要的空间越大。initial_size 参数表示预计放
入的元素数量,当实际数量超出这个数值时,误判率会上升。 

所以需要提前设置一个较大的数值避免超出导致误判率升高。如果不使用 bf.reserve,默
认的 error_rate 是 0.01,默认的 initial_size 是 100

注意事项:

布隆过滤器的 initial_size 估计的过大,会浪费存储空间,估计的过小,就会影响准确
率,用户在使用之前一定要尽可能地精确估计好元素数量,还需要加上一定的冗余空间以避
免实际元素可能会意外高出估计值很多。 
布隆过滤器的 error_rate 越小,需要的存储空间就越大,对于不需要过于精确的场合,
error_rate 设置稍大一点也无伤大雅。比如在新闻去重上而言,误判率高一点只会让小部分文
章不能让合适的人看到,文章的整体阅读量不会因为这点误判率就带来巨大的改变
 

布隆过滤器的其他应用:

邮箱系统的垃圾邮件过滤功能也普遍用到了布隆过滤器,因为用了这个过滤器,所以平
时也会遇到某些正常的邮件被放进了垃圾邮件目录中,这个就是误判所致,概率很低

在爬虫系统中,我们需要对 URL 进行去重,已经爬过的网页就可以不用爬了。但是 
URL 太多了,几千万几个亿,如果用一个集合装下这些 URL 地址那是非常浪费空间的。这
时候就可以考虑使用布隆过滤器。它可以大幅降低去重存储消耗,只不过也会使得爬虫系统
错过少量的页面。 


 

应用 6:断尾求生 —— 简单限流 

令牌桶算法:

应用 7:一毛不拔 —— 漏斗限流 

漏斗的剩余空间就代表着当前行为可以持续进行的数量,漏嘴的流水速率代表着
系统允许该行为的最大频率

应用 8:近水楼台 —— GeoHash

Redis 在 3.2 版本以后增加了地理位置 GEO 模块,意味着我们可以使用 Redis 来实现
摩拜单车「附近的 Mobike」、美团和饿了么「附近的餐馆」这样的功能了

GeoHash 算法

业界比较通用的地理位置距离排序算法是 GeoHash 算法,Redis 也使用 GeoHash 算
法。GeoHash 算法将二维的经纬度数据映射到一维的整数,这样所有的元素都将在挂载到一
条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。当我们想要计算「附
近的人时」,首先将目标位置映射到这条线上,然后在这个一维的线上获取附近的点就行

应用 9:大海捞针 —— Scan 

在平时线上 Redis 维护工作中,有时候需要从 Redis 实例成千上万的 key 中找出特定
前缀的 key 列表来手动处理数据,可能是修改它的值,也可能是删除 key。这里就有一个问
题,如何从海量的 key 中找出满足特定前缀的 key 列表来

Redis 提供了一个简单暴力的指令 keys 用来列出所有满足特定正则字符串规则的 key

Redis 为了解决这个问题,它在 2.8 版本中加入了大海捞针的指令——scan。scan 相比 
keys 具备有以下特点: 

1、复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程; 
2、提供 limit 参数,可以控制每次返回结果的最大条数,limit 只是一个 hint,返回的
结果可多可少; 
3、同 keys 一样,它也提供模式匹配功能; 
4、服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数; 
5、返回的结果可能会有重复,需要客户端去重复,这点非常重要; 
6、遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的; 
7、单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零; 

 

从上面的过程可以看到虽然提供的 limit 是 1000,但是返回的结果只有 10 个左右。因
为这个 limit 不是限定返回结果的数量,而是限定服务器单次遍历的字典槽位数量(约等于)。

如果将 limit 设置为 10,你会发现返回结果是空的,但是游标值不为零,意味着遍历还没结
束。

那如何定位大 key 呢?
为了避免对线上 Redis 带来卡顿,这就要用到 scan 指令,对于扫描出来的每一个 
key,使用 type 指令获得 key 的类型,然后使用相应数据结构的 size 或者 len 方法来得到
它的大小,对于每一种类型,保留大小的前 N 名作为扫描结果展示出来。 
上面这样的过程需要编写脚本,比较繁琐,不过 Redis 官方已经在 redis-cli 指令中提供
了这样的扫描功能,我们可以直接拿来即用。 
redis-cli -h 127.0.0.1 -p 7001 –-bigkeys 
如果你担心这个指令会大幅抬升 Redis 的 ops 导致线上报警,还可以增加一个休眠参
数。 
redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1 
上面这个指令每隔 100 条 scan 指令就会休眠 0.1s,ops 就不会剧烈抬升,但是扫描的
时间会变长
 

原理 1:鞭辟入里 —— 线程 IO 模型 

Redis 是个单线程程序!这点必须铭记。 
也许你会怀疑高并发的 Redis 中间件怎么可能是单线程。很抱歉,它就是单线程,你的
怀疑暴露了你基础知识的不足。莫要瞧不起单线程,除了 Redis 之外,Node.js 也是单线
程,Nginx 也是单线程,但是它们都是服务器高性能的典范
 

原理 3:未雨绸缪 —— 持久化 

 

Redis 的持久化机制有两种,第一种是快照,第二种是 AOF 日志。快照是一次全量备
份,AOF 日志是连续的增量备份。快照是内存数据的二进制序列化形式,在存储上非常

凑,而 AOF 日志记录的是内存数据修改的指令记录文本。AOF 日志在长期的运行过程中会
变的无比庞大,数据库重启时需要加载 AOF 日志进行指令重放,这个时间就会无比漫长。
所以需要定期进行 AOF 重写,给 AOF 日志进行瘦身。

快照原理

我们知道 Redis 是单线程程序,这个线程要同时负责多个客户端套接字的并发读写操作
和内存数据结构的逻辑读写。 
在服务线上请求的同时,Redis 还需要进行内存快照,内存快照要求 Redis 必须进行文
件 IO 操作,可文件 IO 操作是不能使用多路复用 API。 
这意味着单线程同时在服务线上的请求还要进行文件 IO 操作,文件 IO 操作会严重拖
垮服务器请求的性能。还有个重要的问题是为了不阻塞线上的业务,就需要边持久化边响应
客户端请求。持久化的同时,内存数据结构还在改变,比如一个大型的 hash 字典正在持久
化,结果一个请求过来把它给删掉了,还没持久化完呢,这尼玛要怎么搞? 
那该怎么办呢? 
Redis 使用操作系统的多进程 COW(Copy On Write) 机制来实现快照持久化,这个机制
很有意思,也很少人知道。多进程 COW 也是鉴定程序员知识广度的一个重要指标
 

fork(多进程)  

Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,快照持久化完全交给子进
程来处理,父进程继续处理客户端请求。子进程刚刚产生时,它和父进程共享内存里面的代
码段和数据段。这时你可以将父子进程想像成一个连体婴儿,共享身体。这是 Linux 操作系
统的机制,为了节约内存资源,所以尽可能让它们共享起来。在进程分离的一瞬间,内存的
增长几乎没有明显变化

子进程做数据持久化,它不会修改现有的内存数据结构,它只是对数据结构进行遍历读
取,然后序列化写到磁盘中。但是父进程不一样,它必须持续服务客户端请求,然后对内存
数据结构进行不间断的修改。 
这个时候就会使用操作系统的 COW 机制来进行数据段页面的分离。数据段是由很多操
作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复
制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的,
还是进程产生时那一瞬间的数据子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再
也不会改变,这也是为什么 Redis 的持久化叫「快照」的原因。接下来子进程就可以非常安
心的遍历数据了进行序列化写磁盘了。 

AOF 原理

AOF 日志存储的是 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的
指令记录

通常 Redis 的主节点是不会进行持久化操作,持久化操作主要在从节点进行。从节
点是备份节点,没有来自客户端请求的压力,它的操作系统资源往往比较充沛 ,

 原理 4:雷厉风行 —— 管道 

两个连续的写操作和两个连续的读操作总共只会花费一次网络来回,就好比连续的 write 
操作合并了,连续的 read 操作也合并了一样
这便是管道操作的本质,服务器根本没有任何区别对待,还是收到一条消息,执行一条
消息,回复一条消息的正常的流程。客户端通过对管道中的指令列表改变读写顺序就可以大
幅节省 IO 时间。管道中指令越多,效果越好
因为大多的消耗是在网络的通信中,所有尽量减少通信

 

前面我们讲了 Redis 消息队列的使用方法,但是没有提到 Redis 消息队列的不足之
处,那就是它不支持消息的多播机制

 

原理 8:有备无患 —— 主从同步 

很多企业都没有使用到 Redis 的集群,但是至少都做了主从。有了主从,当 master 挂
掉的时候,运维让从库过来接管,服务就可以继续,否则 master 需要经过数据恢复和重启
的过程,这就可能会拖很长的时间,影响线上业务的持续服务

在了解 Redis 的主从复制之前,让我们先来理解一下现代分布式系统的理论基石——
CAP 原理

 最终一致

Redis 的主从数据是异步同步的,所以分布式的 Redis 系统并不满足「一致性」要求。
当客户端在 Redis 的主节点修改了数据后,立即返回,即使在主从网络断开的情况下,主节
点依旧可以正常对外提供修改服务,所以 Redis 满足「可用性」。 
Redis 保证「最终一致性」,从节点会努力追赶主节点,最终从节点的状态会和主节点
的状态将保持一致。如果网络断开了,主从节点的数据将会出现大量不一致,一旦网络恢
复,从节点会采用多种策略努力追赶上落后的数据,继续尽力保持和主节点一致

主从同步 

Redis 同步支持主从同步和从从同步,从从同步功能是 Redis 后续版本增加的功能,为
了减轻主库的同步负担

Redis 同步的是指令流,主节点会将那些对自己的状态产生修改性影响的指令记录在本
地的内存 buffer 中,然后异步将 buffer 中的指令同步到从节点,从节点一边执行同步的指
令流来达到和主节点一样的状态,一遍向主节点反馈自己同步到哪里了 (偏移量)。Redis 的复制内存 buffer 是一个定长的环形数组,如果数组内容满了,就会从头开始覆
盖前面的内容。 

 

快照同步 

快照同步是一个非常耗费资源的操作,它首先需要在主库上进行一次 bgsave 将当前内
存的数据全部快照到磁盘文件中,然后再将快照文件的内容全部传送到从节点。从节点将快
照文件接受完毕后,立即执行一次全量加载,加载之前先要将当前内存的数据清空。加载完
毕后通知主节点继续进行增量同步

 

Sentinel 基本使用 集群 1:李代桃僵 —— Sentinel 哨兵

如果主节点凌晨3 点突发宕机怎么办?就坐等运维从床上爬起来,然后手工进行从主切换,再通知所有的程
序把地址统统改一遍重新上线么?毫无疑问,这样的人工运维效率太低,事故发生时估计得
至少 1 个小时才能缓过来。如果是一个大型公司,这样的事故足以上新闻了.

所以我们必须有一个高可用方案来抵抗节点故障,

当故障发生时可以自动进行从主切
换,程序可以不用重启,运维可以继续睡大觉,仿佛什么事也没发生一样。Redis 官方提供
了这样一种方案 —— Redis Sentinel(哨兵)。

 

它负责持续监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换为
主节点。客户端来连接集群时,会首先连接 sentinel,通过 sentinel 来查询主节点的地址,
然后再去连接主节点进行数据交互。当主节点发生故障时,客户端会重新向 sentinel 要地
址,sentinel 会将最新的主节点地址告诉客户端。如此应用程序将无需重启即可自动完成节
点切换。如下图:

从这张图中我们能看到主节点挂掉了,原先的主从复制也断开了,客户端和损坏的主节
点也断开了。从节点被提升为新的主节点,其它从节点开始和新的主节点建立复制关系。客
户端通过新的主节点继续进行交互。Sentinel 会持续监控已经挂掉了主节点,待它恢复后,
集群会调整为下面这张图。 

此时原先挂掉的主节点现在变成了从节点,从新的主节点那里建立复制关系

消息丢失

Redis 主从采用异步复制,意味着当主节点挂掉时,从节点可能没有收到全部的同步消
息,这部分未同步的消息就丢失了。如果主从延迟特别大,那么丢失的数据就可能会特别
多。Sentinel 无法保证消息完全不丢失,但是也尽可能保证消息少丢失。它有两个选项可以
限制主从延迟过大。
 

min-slaves-to-write 1

min-slaves-max-lag 10 

第一个参数表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服
务,丧失可用性。 
何为正常复制,何为异常复制?这个就是由第二个参数控制的,它的单位是秒,表示如
果 10s 没有收到从节点的反馈,就意味着从节点同步不正常,要么网络断开了,要么一直没
有给反馈。
 

集群 2:分而治之 —— Codis 

Codis 是 Redis 集群方案之一

拓展 2:无所不知 —— Info 指令

在使用 Redis 时,时常会遇到很多问题需要诊断,在诊断之前需要了解 Redis 的运行状
态,通过强大的 Info 指令,你可以清晰地知道 Redis 内部一系列运行参数。 
Info 指令显示的信息非常繁多,分为 9 大块,每个块都有非常多的参数,这 9 个块分
别是: 

   1、Server 服务器运行的环境参数 
    2、Clients 客户端相关信息 
    3、Memory 服务器运行内存统计数据 
    4、Persistence 持久化信息 
    5、Stats 通用统计数据 
    6、Replication 主从复制相关信息 
    7、CPU CPU 使用情况 
    8、Cluster 集群信息 
    9、KeySpace 键值对统计数量信息 

 

拓展 4:朝生暮死 —— 过期策略


Redis 所有的数据结构都可以设置过期时间,时间一到,就会自动删除。你可以想象 
Redis 内部有一个死神,时刻盯着所有设置了过期时间的 key,寿命一到就会立即收割。 

过期的 key 集合 
redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历这个
字典来删除到期的 key。除了定时遍历之外,它还会使用惰性策略来删除过期的 key,所谓
惰性策略就是在客户端访问这个 key 的时候,redis 对 key 的过期时间进行检查,如果过期
了就立即删除。定时删除是集中处理,惰性删除是零散处理

  1、从过期字典中随机 20 个 key; 
    2、删除这 20 个 key 中已经过期的 key; 
    3、如果过期的 key 比率超过 1/4,那就重复步骤 1; 

同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时
间的上限,默认不会超过 25ms

业务开发人员一定要注意过期时间,如果有大批量的 key 过期,要给过期时间设置
一个随机范围,而不能全部在同一时间过期。

从库的过期策略 

从库不会进行过期扫描,从库对过期的处理是被动的。主库在 key 到期时,会在 AOF 
文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 
key

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值