常见问题
持久化机制
实现
单独 fork() 一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程写入到临时文件,等持久化过程结束了,再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放rdb
redis 的默认持久方式,按照一定的时间周期策略,把内存的数据以快照的形式保存到硬盘 rdb 文件;
通过配置文件的 save 参数可以定义快照的周期;
好处是恢复方便,启动快,转移方便
坏处是因为按时间间隔来做持久化,数据安全不能达到最好,总会丢失一个时间间隔内的数据aof
redis 会将每一个收到的写命令都通过 write 函数 append 到文件最后,类似于 mysql 的 binlog;
当 redis 重启,会通过重新执行文件中保存的写命令来在内存中重建整个数据库内容;
好处是数据安全性高
坏处是启动慢,转移麻烦同时开启 rdb 和 aof,redis 会优先选择 aof 恢复
相关缓存问题
缓存雪崩
引发原因
我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期,所有原本应该访问缓存的请求都去查询数据库了,而对数据库 cpu 和内存造成巨大压力,严重的会造成数据库宕机,从而造成一系列连锁反应,造成整个系统崩溃;最可怕的是,某台缓存节点突然宕机了,更会造成大量从数据库取数据的问题解决方案
解决方案:
1、尽可能将缓存过期时间分散开
2、设置热点数据永远不过期,异步更新
3、如果缓存数据库是分布式部署,将热点数据均匀分布在不同缓存数据库中
4、业务代码上加锁,保证并发情况下,最终只有一个请求是落到底层数据库的,当这个请求处理成功后,缓存便已经重建,其他并发请求最终命中的都是缓存层
5、数据库连接池(稍微有点优化效果,避免数据库崩溃)- 加锁方案,体验不好,逻辑复杂,还依赖分布式锁,其实缓存预热就能解决雪崩问题;另外,如果 qps ok 的话,就算不处理也不会引起缓存雪崩问题
- 节点如果挂了,这种问题应该在运维层做多节点、多机房、异地多活,原则上业务无感知
缓存穿透
引发原因
模拟一个根本不可能存在的值,不断请求,则正常的业务代码会不断的去缓存层查找,再去底层数据库查找,其实这个操作是无效操作,浪费性能解决方案
布隆过滤器(BOOLMFILTER)布隆过滤器
redis 支持 setbit 和 getbit,天然支持布隆过滤器的结构
解决缓存穿透,就是将底层数据库中实际存在的数据,都在 redis 维护一个布隆过滤器,当 redis 无法命中相关数据时,先检查布隆过滤器,事实存在,再去找底层数据库
但是布隆过滤器存在误判率,并不能完全杜绝缓存穿透
还可以去重
其他方案
- 缓存预热,写入、更新时 delete 该 key
- 加空缓存,避免回源,请求一个不存在的 key 时,回源不存在,默认缓存个空值;写入、更新时 delete 该 key
缓存击穿
引发原因
当某一个 key ,在缓存过期的那一刻,同时有大量的请求,这样会击穿到底层数据库;
和缓存雪崩引发原因类似,但它的范围较小解决方案
缓存雪崩的解决方案,能解决此问题
缓存预热
系统上线后,将相关数据直接加载到缓存系统
解决方案
- 写个缓存刷新功能,上线时手工操作下
- 数据量不大时,可以在项目启动的时候自动加载
- 定时刷新缓存
- 在写入和更新时维护 key
缓存更新
除了缓存服务器自带的缓存失效策略外,redis 默认还有六种策略可供选择,其中常见有两种
定时清理过期的缓存,并重新生成
- 集中式的重新生成,还需要维护大量的 key
当用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层数据库取
- 逻辑代码编写相对复杂
缓存降级
降级的目的是保证核心服务可用,即使是有损的,而且有些服务是无法降级的,如加入购物车,结算等;防止 redis 服务故障,导致数据库跟着一起发生雪崩问题,因此对于不重要的缓存数据,可以采取服务降级策略;比如,redis 出现问题,不去数据库查询,而是直接返回默认值给用户
以参考日志级别设置预案
一般
- 有些服务偶尔因为网络抖动或服务正在上线而超时,可以自动降级
警告
- 有些服务在一段时间内的成功率有波动,可以自动降级,或人工降级,并报警
错误
- 比如可用率低于 90%,或者数据库连接池爆了,或者访问量突然猛增到系统能够承受的最大阀值,此时可以根据情况自动降级或人工降级
严重错误
- 因为特殊原因数据错误了,此时需紧急人工降级
热点数据和冷数据
- 热点数据,缓存才有价值,主要是读取频次较高的数据,尽可能让他进入缓存
- 冷数据,大部分数据可能还没有再次访问到就已经被挤出内存了,不仅占用内存,而且价值不大;频繁修改的数据,也要看情况考虑使用缓存
- 另外,更新前,至少读取两次,缓存才有意义;这个是最基本的策略,如果缓存还没起到作用就失效了,就没有什么价值了
- 但是也存在修改频率很高,但又不得不考虑缓存的场景。如点赞数、收藏数、分享数,都是典型的热点数据,但修改频次又很高
与 Memcache 的区别
- memcached 不支持持久化,数据不能超过内存大小,而 redis 支持持久化,并存在 swap 区
- memcached 只支持简单的字符串类型
- 底层模型不同,通信应用协议不同,redis 自己构建了 vm 机制
- redis 最大支持 1g value,而 mem 只支持 1mb
- redis 速度比 memcached 快很多
单线程为什么这么快
- 纯内存操作
- 单线程操作,避免频繁的上下文切换
- 采用非阻塞 IO 多路复用机制
为什么是单线程
- 因为 redis 基于内存操作,cpu 不是瓶颈,而内存大小和网络带宽才是瓶颈。既然单线程容易实现,且 cpu 不会成为瓶颈,就顺理成章采用单线程了
- redis 利用队列技术将并发访问变为串行访问
- 绝大部分请求时纯粹的内存操作
- 采用单线程,避免了上下文切换和资源竞争
数据类型及各自适用的场景
string
- 如 bitmap、复杂的计数功能等
hash
- 存放结构化的对象,可以模拟存储用户信息、session 等
list
- 可以简单做消息队列得到功能,还可以使用 lrange 命令做基于 redis 的分页功能
set
- 因为 set 对方不重复值的集合,所以可以做全局去重功能,还可以做交集、并集、差集的运算;
- 业务代码层的 set,如 go 的 map 也支持去重,但业务代码总是会存储在集群的,其内存变量并不共享,而缓存层对外是统一的
zset
- 排行榜等
过期策略及内存淘汰机制
采用定期删除 + 惰性删除策略
定时删除,用一个定时器来负责监视 key ,过期则自动删除,虽然内存及时释放,但十分消耗 cpu 资源,并发大的情况下,cpu 要将时间应用在处理请求上,而不是监视 key 上,因此没有采用此策略
而定期删除 + 惰性删除呢?
定期删除,redis 每 100 ms 检查,是否有过期的 key,有则删除
redis 不是每 100ms 就检查所有 key ,而是随机抽取进行检查,因此,采用定期删除策略,会导致很多到期的 key 没有删除。
但是惰性删除可以解决这个问题,比如每次获取 key 的时候,redis 会检查一下这个 key 是否过期了,如果过期了,此时就会删除。
这种策略其实仍然存在问题,比如,如果定期删除没有删除 key,然后也没有及时请求 key,那么惰性删除也没有生效,这样 redis 的内存占用得会越来越多
此时,针对这个问题,就要采用内存淘汰机制内存淘汰机制
redis.conf 中有一行配置
maxmemory-policy volatile-lru
该配置就是内存淘汰策略,选项有:
volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据进行淘汰
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据进行淘汰
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
allkeys-lru:从数据集中挑选最近最少用的数据进行淘汰
allkeys-random:从数据集中任意选择数据淘汰
no-enviction:禁止驱逐数据,这样一来,内存满了,新写入会报错
volatile 指定了数据集的范围,如果实际运行中,不存在设置有过期时间的 keys,则效果和 no-envicion 基本一致
并发竞争 key 问题
集群方案
twemproxy
- 通过代理的方式,使用时在本需要连接 redis 的地方改为连接 twemproxy,它会以一个代理的身份接收请求,并使用一致性hash算法,将请求转接到具体的redis,然后redis将结果返回到twemproxy
- 缺点:twemproxy自身单端口实例的压力,使用一致性 hash 后,对 redis 节点数量改变时,数据无法自动移动到新的节点
codis
- 目前用的最多的集群方案,基本和 twemproxy 效果差不多,但它支持节点数量改变情况下,旧节点数据可恢复到新 hash 节点
redis cluster3.0
- redis 自带的集群,特点在于它的分布式算法不是一致性 hash,而是 hash 槽的概念,以及自身支持节点设置从节点
多机部署如何保证数据一致
- 主从复制,读写分离
- 一类是主数据库,一类是从数据库,主数据库负责写操作,发生写操作时,自动将数据同步到从数据库,而从数据库只负责读,并接收主数据库同步过来的数据,一个主数据库可有多个从库,一个从库只能有一个主库
redis 如何处理并发
- redis 是单线程程序,也就代表着只支持并发,不支持并行;
- 但它通过io多路复用(select、epoll、kqueue,依据不同平台采取不同的实现)来处理多个客户端请求
常见性能问题和解决方案
- master 最好不要做持久化工作
- 如果数据比较重要,某个 slave 开启 aof,策略设置为每秒同步一次
- 为了主从复制的速度和连接的稳定性,主从尽量在同一局域网内
- 尽量避免在压力很大的主库上增加从库
- 主从复制不要用图状结构,用单向链表结构更为稳定,如 master<-slave1<-slave2…
线程模型
redis 基于 reactor 模式开发了自己的网络事件处理器,被称为文件事件处理器(file event handler)
文件事件处理器使用 IO多路复用程序(multiplexing)来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器
当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生。这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件
虽然文件事件处理器以单线程方式运行,但通过io多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地为redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 redis 内部单线程设计的简单性
文件事件处理器的构成
套接字
io 多路复用程序
文件事件分派器(dispatcher)
事件处理器
- 命令请求处理器
- 命令回复处理器
- 连接应答处理器
- ……
所谓的文件事件,是对套接字操作的抽象,每当一个套接字准备好执行连接应答、写入等操作时,就会产生一个文件事件。因为一个服务器通常会连接多个套接字,所以多个文件事件有可能会并发的出现
IO 多路复用程序负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字
尽管多个事件可能并发出现,但IO多路复用程序总是会将所有产生事件的套接字都入队到一个队列里,然后通过这个队列,以有序(sequentinally)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字:当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕),IO 多路复用程序才会继续向文件事件分派器传送下一个套接字
文件事件分派器接收IO多路复用程序传来的套接字,并根据套接字产生的事件类型,调用对应的处理器
服务器会为执行不同任务的套接字关联不同的事件处理器,这些处理器是一个个函数,它们定义了某个事件发生时,服务器应该执行的动作
为什么 redis 操作是原子性的,怎么保证原子性
- 命令原子性是指一个操作不可再分,操作要么执行,要么不执行
- 操作之所以是原子性的,是因为 redis 是单线程的
- redis 本身提供的所有 api 都是原子操作,但是 redis 的事务并非原子的,只是一个批处理
事务
- exec 命令,执行事务块内所有命令,并返回所有命令的返回值,按命令执行的先后顺序排列;当操作被打断时,返回 nil
- watch 命令,可以为 redis 事务提供 check-and-set (CAS)行为,可以监控一个或多个键,一旦其中有一个键被修改或删除,之后的事务就不会执行,监控一直持续到 exec 命令为止
分布式锁
为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度,该技术的核心就是分布式锁
条件
- 分布式环境下,一个方法在同一时间只能被一个机器的一个线程执行
- 高可用的获取锁和释放锁
- 高性能的获取锁和释放锁
- 具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
- 具备锁失效机制,防止死锁
- 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败
- 利用 setnx 命令,实现分布式锁的创建,此命令是原子性操作的,只有key 不存在的情况下,才能 set 成功
核心要素
加锁
- 最简单的方法是使用 setnx ,key 是锁的唯一标识,由业务层来指定;
- 一个线程执行 setnx,如果返回设定的 value ,则说明抢锁成功;若返回 0 ,则抢锁失败
解锁
- 通过 del 命令删除锁
锁超时
- 如果一个得到锁的线程在执行任务过程中挂掉,来不及释放锁,则该锁会一直存在,就形成了死锁;所以 setnx 的 key 必须设置一个超时时间,以保证不出现死锁
几个问题
setnx 和 expire 不是原子性的
- 但是如果通过 expire 来指定 key 的过期时间,由于 setnx 和 expire 两个命令不是原子性的,还是存在出现死锁的几率,所以,可以用 set 来替代,同时增加可选参数 NX
del 导致误删
- 线程 1 得到锁,执行了很久,期间该 key 过期了,被线程 2 得到,然后线程 1 执行到了 del 命令,将线程 2 得到的锁给删了
- 可以为锁的 value 加上当前线程的标志,如线程 id,每个线程在执行 del 时,要判断是不是自己的锁,是的话再删
- 但这样因为判断和删除仍然不是原子性的
实际案例
热点数据缓存
限时业务
- 基于 expire 命令,设置一个键的生存时间
- 可以用于限时优惠活动信息、手机验证码等
计数器
- incrby 命令可以实现原子性递增,所以运用于高并发的秒杀活动、分布式序列号的生出、具体业务还体现在比如限制一个手机号发多少短信、一个接口一分钟限制请求多少次等
- 商品紧俏、秒杀类的,应该使用下单减库存。然后设置超时时间,并提醒用户,N 分钟后可能还会再有;时间过去之后,库存变回来
- incr 命令,在执行增加的同时,返回增加后的数字
排行榜
- zset 可以维护热点数据的排序
分布式锁
延时操作
- 订单生产后占用了库存,10 分钟后去检验用户是否真实购买,如果没有真实购买,就关闭该订单,还原库存
分页、模糊搜索
- zrangebylex key min max [limit offset count] 支持分页和模糊查询
点赞、好友等相互关系存储
- set 实现共同好友
简单队列
- list
布隆过滤器
XMind: ZEN - Trial Version