PART1:缓存异常,有四种类型
- 缓存和数据库的数据不一致
- 使用到缓存,无论是本地内存做缓存还是使用 Redis 做缓存,那么就会存在数据同步的问题,因为配置信息缓存在内存中,而
内存是无法感知到数据在数据库的修改
。这样就会造成数据库中的数据与缓存中数据不一致的问题 如何保证缓存与数据库双写时的数据一致性
?【目前主要用“先删除缓存,后更新数据库”和“先更新数据库,后删除缓存”方案。】- 先更新数据库,后更新缓存
- 并发更新数据库场景下,
会将脏数据刷到缓存
。所以不能用
- 并发更新数据库场景下,
- 先更新缓存,后更新数据库
- 如果先更新缓存成功,
但是数据库更新失败,则肯定会造成数据不一致
,所以不能用
- 如果先更新缓存成功,
先删除缓存,后更新数据库
:框图补充如下:
- 延时双删:
- 更新与读取操作进行异步串行化:
- 延时双删:
先更新数据库,后删除缓存
- 这一种情况也会出现问题,比如更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功,那么此时再读取缓存的时候每次都是错误的数据了
- 这一种情况也会出现问题,比如更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功,那么此时再读取缓存的时候每次都是错误的数据了
- 先更新数据库,后更新缓存
- 使用到缓存,无论是本地内存做缓存还是使用 Redis 做缓存,那么就会存在数据同步的问题,因为配置信息缓存在内存中,而
(redis做缓存发生的三种问题,redis做数据库肯定没这些事,但是redis经常被用来做缓存,所以咱们就得考虑这三种异常情况),先总览一下:
- 缓存击穿、缓存穿透、缓存雪崩
- redis缓存击穿:一个key数据刚一过期,结果对这个key数据的查询就来了【
一个key上的很多请求来了
】,造成大量并发直接到数据库
,产生缓存击穿redis雪崩和缓存击穿的区别是,缓存雪崩虽然也是大量请求到DB,但是redis雪崩中大量请求是针对多个key的,而缓存击穿是针对单个key的
- redis雪崩:大量的key同时失效(一批key同时到期),间接造成对多个key的大量访问到达DB
- redis穿透:从业务接收查询的是你系统根本不存在的数据,缓存跟DB都没有数据
- redis缓存击穿:一个key数据刚一过期,结果对这个key数据的查询就来了【
- 缓存雪崩:缓存中不同的数据大批量在
某一个时刻出现大规模的key失效【会在同一时间失效就是因为,通常我们为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存。】
,那么就会导致大量的请求打在了数据库上面
,因为此时Redis中无法处理这么多用户请求,导致数据库压力巨大,如果在高并发的情况下,可能瞬间就会导致数据库宕机【Redis 故障宕机或者说大量缓存数据在同一时间过期(失效)】。这时候如果运维马上又重启数据库,马上又会有新的流量把数据库打死。这就是缓存雪崩- 造成缓存雪崩的
关键在于同一时间的大规模的key失效
,主要有两种可能:- 第一种是Redis宕机(缓存服务器某个节点宕机或者断网),
- 第二种可能就是采用了相同的过期时间(某一个时间段内缓存集中过期失效),导致大量数据同时过期。有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上
- 有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上
- 解决方案:
- 针对大量数据同时过期而引发的缓存雪崩问题,常见的应对方法有下面这几种:
- 均匀设置过期时间
- 如果要给缓存数据设置过期时间,应该避免将大量的数据设置成同一个过期时间。我们可以在
对缓存数据设置过期时间时,给这些数据的过期时间加上一个随机数,这样就保证数据不会在同一时间过期
。
- 如果要给缓存数据设置过期时间,应该避免将大量的数据设置成同一个过期时间。我们可以在
- 加互斥锁
- 当业务线程在处理用户请求时,
如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存
(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。【实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象
。】
- 当业务线程在处理用户请求时,
- 缓存永不过期【后台更新缓存】
- 业务线程不再负责更新缓存,缓存也不设置有效期,
而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新
。缓存数据不设置有效期,并不是意味着数据一直能在内存里,因为当系统内存紧张的时候,有些缓存数据会被“淘汰”,而在缓存被“淘汰”到下一次后台定时更新缓存的这段时间内,业务线程读取缓存失败就返回空值,业务的视角就以为是数据丢失了。
- 业务线程不再负责更新缓存,缓存也不设置有效期,
- 双层缓存策略【双 key 策略】
- 我们对缓存数据可以使用两个 key,一个是主 key,会设置过期时间,一个是备 key,不会设置过期,它们只是 key 不一样,但是 value 值是一样的,
相当于给缓存数据做了个副本
。 - 当业务线程访问不到主 key 的缓存数据时,就直接返回备 key 的缓存数据,然后在更新缓存的时候,
同时更新主 key 和备 key 的数据
。
- 我们对缓存数据可以使用两个 key,一个是主 key,会设置过期时间,一个是备 key,不会设置过期,它们只是 key 不一样,但是 value 值是一样的,
- 均匀设置过期时间
- 针对Redis宕机(缓存服务器某个节点宕机或者断网)而引发的缓存雪崩问题,常见的应对方法有下面这几种:
- 服务熔断或请求限流机制:
避免同时处理大量的请求
- 因为 Redis 故障宕机而导致缓存雪崩问题时,
我们可以启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误,不用再继续访问数据库,从而降低对数据库的访问压力
,保证数据库系统的正常运行,然后等到 Redis 恢复正常后,再允许业务应用访问缓存服务。 服务熔断机制是保护数据库的正常允许,但是暂停了业务应用访问缓存服系统,全部业务都无法正常工作
为了减少对业务的影响,我们可以启用请求限流机制,只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务,等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制
。
- 因为 Redis 故障宕机而导致缓存雪崩问题时,
- 构建 Redis 缓存高可靠集群:
- 服务熔断或请求限流机制是缓存雪崩发生后的应对方案,我们最好
通过主从节点的方式构建 Redis 缓存高可靠集群
。 - 如果 Redis 缓存的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务,避免了由于 Redis 故障宕机而导致的缓存雪崩问题
- 服务熔断或请求限流机制是缓存雪崩发生后的应对方案,我们最好
- 服务熔断或请求限流机制:
- 针对大量数据同时过期而引发的缓存雪崩问题,常见的应对方法有下面这几种:
- 造成缓存雪崩的
- 缓存击穿:某个设置了过期时间的key,缓存在某个时间点过期时,或者说是某个热点的key失效时,恰好大并发集中对其进行请求,就会造成大量请求读缓存没读到数据,从而导致
高并发访问数据库,引起数据库压力剧增
。这种现象就叫做缓存击穿【我们的业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据
。】- 指的是一个key非常热点在不停着扛着大并发请求(相当于请求们大并发的集中对这一个点进行访问),当这个key在失效瞬间,持续的大并发就击穿缓存并直接请求数据库,就相当于在屏障上凿开了一个洞,不就是击穿了吗【
可以发现缓存击穿跟缓存雪崩很相似,你可以认为缓存击穿是缓存雪崩的一个子集
】 - 解决方案:
- 热点key不设置过期时间:不设置过期时间就不会出现热点key过期后产生的比如缓存击穿这种问题【
物理不过期但逻辑过期(后台异步线程去刷新)
】或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间- 物理不过期,针对热点key不设置过期时间
- 逻辑过期,把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建
- 降低打在数据库上的请求数量:
加互斥锁
: 使用分布式锁保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限因此要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。(在缓存失效后,通过互斥锁或者队列来控制读数据写缓存的线程数量,比如某个key只允许一个线程查询数据和写缓存,其他线程等待。这种方式会阻塞其他的线程,此时系统的吞吐量会下降)----把压力转到了分布式锁- 其实解决思路缓存击穿就是阻止高并发去数据库get key,思路就是第一个进到缓存中发现这货key已经过期了,那么赶紧利用setnx()设置一把锁【也就是后续所有人再访问这个不存在或者说过期的数据时要抢这把锁,抢到锁才能去DB访问这个数据】
使用互斥锁指的是当缓存失效时,不立即去load db,先使用比如Redis的setnx去设置一个互斥锁,当操作成功返回时再进行load db的操作并回设缓存,否则重试get缓存的方法。
- 有可能造成死锁,第一个设置锁的哥们挂了,其他人一直在睡觉。解决思路就是设置锁的过期时间,但是有可能锁会超时,所以可以优化为一个线程取DB、一个线程监控是否取回来,并更新锁时间
- 其实解决思路缓存击穿就是阻止高并发去数据库get key,思路就是第一个进到缓存中发现这货key已经过期了,那么赶紧利用setnx()设置一把锁【也就是后续所有人再访问这个不存在或者说过期的数据时要抢这把锁,抢到锁才能去DB访问这个数据】
- 热点key不设置过期时间:不设置过期时间就不会出现热点key过期后产生的比如缓存击穿这种问题【
- 指的是一个key非常热点在不停着扛着大并发请求(相当于请求们大并发的集中对这一个点进行访问),当这个key在失效瞬间,持续的大并发就击穿缓存并直接请求数据库,就相当于在屏障上凿开了一个洞,不就是击穿了吗【
- 缓存穿透(
缓存中查不到某个数据导致的导致短时间大量请求落在数据库上,造成数据库压力过大。或者这样说,你客户端要搜的我缓存里和DB中都没有,结果请求直接到DB中让DB做很多有的没得的事情
。(是指用户大量请求的数据在缓存中不存在即没有命中
,同时在数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍。如果有恶意攻击者不断请求系统中不存在的数据,会导致短时间大量请求落在数据库上,造成数据库压力过大
,甚至导致数据库承受不住而宕机崩溃。))【一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大
,就会对后端系统造成很大的压力。这就叫做缓存穿透。】当发生缓存雪崩或击穿时,数据库中还是保存了应用要访问的数据
,一旦缓存恢复相对应的数据,就可以减轻数据库的压力,而缓存穿透就不一样了。当用户访问的数据,既不在缓存中,也不在数据库中
,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。- 当用户想要查询一个数据但发现redis内存数据库中没有(也就是说缓存没有命中),于是用户会再向持久层数据库查询发现也没有于是此次查询失败。当用户很多时缓存都没有命中,与是查询请求都去请求了持久层数据库,这就给持久层数据库造成很大压力,相当于出现了缓存穿透
- 缓存穿透的发生一般有这两种情况:
- 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据
- 黑客恶意攻击,故意大量访问某些读取不存在数据的业务
- 缓存穿透解决方案:
最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等
。
- 缓存无效 key
如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下: SET key value EX 10086。【一般情况下我们是这样设计 key 的: 表名:列名:主键名:主键值】
。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟
- 布隆过滤器【
布隆过滤器是一个数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中
。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在
】- Redis中文官方文档中的布隆过滤器
- 布隆过滤器缺点:只能增加不能删除
- 解决方案就是换一个比如布谷鸟,支持删除的
- 对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。
- 在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。
即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的
。把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程
。- 布隆过滤器说某个元素存在,小概率会误判。
布隆过滤器说某个元素不在,那么这个元素一定不在。
- 布隆过滤器工作原理:
- 布隆过滤器
由初始值都为 0 的位图数组和 N 个哈希函数两部分组成
。当我们在写入
数据库数据时,在布隆过滤器里做个标记
,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中
。
- 布隆过滤器会通过 3 个操作完成标记:【或者说
当一个元素加入布隆过滤器中的时候,会进行哪些操作
:】- 第一步,使用布隆过滤器中的 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值
- 第二步,将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置或者对应索引。
- 第三步,将每个哈希值在位图数组的对应位置或者索引的值设置为 1
- 布隆过滤器由于是基于哈希函数实现查找的,
高效查找的同时存在哈希冲突的可能性
,比如数据 x 和数据 y 可能都落在第 1、4、6 位置,而事实上,可能数据库中并不存在数据 y,存在误判的情况
。所以,查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据
当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:
- 对给定元素再次进行相同的哈希计算
- 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,
如果存在一个值不为 1,说明该元素不在布隆过滤器中
。
- redis布隆过滤器,先问,你有啥,有先做比对匹配。
布隆过滤器是一种概率的,有可能x这个值通过计算后第1、4、6位置为1,结果y很巧也是1、4、6,那肯定出错了呀或者说误标记了呀,y你用的是不属于你的1呀,也就是数据库中没有y但被x误导了以为DB中有y
。
- 布隆过滤器
- 返回空对象【缓存空值或者默认值】
- 对查询结果为空的情况也当作数据进行缓存,缓存时间设置短一点或者该key对应的数据insert了之后清理缓存【当我们线上业务发现缓存穿透的现象时,
可以针对查询的数据,在缓存中设置一个空值或者默认值
,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库】
- 对查询结果为空的情况也当作数据进行缓存,缓存时间设置短一点或者该key对应的数据insert了之后清理缓存【当我们线上业务发现缓存穿透的现象时,
- 非法请求的限制:
- 当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
- 布隆过滤器的使用场景
判断给定数据是否存在:比如判断一个数字是否存在于包含大量数字的数字集中(数字集很大,5 亿以上!)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤、黑名单功能等等
。- 去重:比如爬给定网址的时候对已经爬取过的 URL 去重
- 编程手动实现布隆过滤器。javaGuide老师的布隆过滤器。实际项目中我们不需要手动实现一个布隆过滤器。可以利用
Google 开源的 Guava 中自带的布隆过滤器
- 实际项目中:【redis官网中,也就是布隆的源码网站中找到源码,下载zip,到redis中https://github.com/RedisBloom/RedisBloom/archive/refs/heads/master.zip下载zip,可以到linux中wget下载,然后yum install unzip,然后解压,然后make编译,然后进入布隆过滤器启动,然后redis-cli,开始你的骚操作即可】
- 布隆过滤器的实现思路:
选择哪种取决于项目的性能要求和成本
- 客户端实现bloom算法,自己承载bitmap
- 客户端实现bloom,redis端实现bitmap
客户端无,bloom和bitmap都加在redis
这种比较好,因为redis是内存级的,对CPU损耗不大,这样可以让客户端更轻量一些,client这边只留一些业务代码
- 使用Google 开源的 Guava 中自带的布隆过滤器步骤:这个布隆过滤器有一个重大的缺陷就是只能单机使用(另外,容量扩展也不容易),而现在互联网一般都是分布式的场景。为了解决这个问题,我们就需要用到 Redis 中的布隆过滤器了
- 首先我们需要在项目中引入 Guava 的依赖:
- 创建了一个最多存放 最多 1500 个整数的布隆过滤器,并且我们可以容忍误判的概率为百分之(0.01)
// 创建布隆过滤器对象 BloomFilter<Integer> filter = BloomFilter.create( Funnels.integerFunnel(), 1500, 0.01); // 判断指定元素是否存在 //当 mightContain() 方法返回 true 时,我们可以 99%确定该元素在过滤器中,当过滤器返回 false 时,我们可以 100%确定该元素不存在于过滤器中。 System.out.println(filter.mightContain(1)); System.out.println(filter.mightContain(2)); // 将元素添加进布隆过滤器 filter.put(1); filter.put(2); System.out.println(filter.mightContain(1)); System.out.println(filter.mightContain(2));
- 首先我们需要在项目中引入 Guava 的依赖:
- Redis 中的布隆过滤器:
- Redis v4.0 之后有了 Module(模块/插件) 功能,Redis Modules 让 Redis 可以使用外部模块扩展其功能 。
布隆过滤器就是其中的 Module
。 - Redis 官方对 Redis Modules 的介绍
- 官网推荐了一个 RedisBloom 作为 Redis 布隆过滤器的 Module
- RedisBloom 提供了多种语言的客户端支持,包括:Python、Java、JavaScript 和 PHP
- Docker中使用RedisBloom
- redis-lua-scaling-bloom-filter(lua 脚本实现)
- pyreBloom(Python 中的快速 Redis 布隆过滤器)
- Redis v4.0 之后有了 Module(模块/插件) 功能,Redis Modules 让 Redis 可以使用外部模块扩展其功能 。
- 布隆过滤器的实现思路:
- 缓存无效 key
PART3:常见问题之三高:
- 如果现在有个读超高并发的系统,用Redis来抗住大部分读请求,你会怎么设计?
- Redis高可用方案具体怎么实施
- 使用官方推荐的哨兵(sentinel)机制就能实现,当主节点出现故障时,由Sentinel自动完成故障发现和转移,并通知应用方,实现高可用性。它有四个主要功能:
- 集群监控,负责监控Redis master和slave进程是否正常工作。
- 消息通知,如果某个Redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
- 故障转移,如果master node挂掉了,会自动转移到slave node上。
- 配置中心,如果故障转移发生了,通知client客户端新的master地址
- 使用官方推荐的哨兵(sentinel)机制就能实现,当主节点出现故障时,由Sentinel自动完成故障发现和转移,并通知应用方,实现高可用性。它有四个主要功能:
PART4.Redis 的热key问题怎么解决?
- 热key就是说,
在某一时刻,有非常多的请求访问某个key,流量过大,导致该 redis服务器宕机
- 解决方案::
- 可以将结果缓存到本地内存中
- 将热 key 分散到不同的服务器中
- 设置永不过期
- 解决方案::
PART5.复制:在Redis中,用户可以通过执行SLAVEOF命令或者设置slaveof选项
,让一个服务器去复制(replicate)另一个服务器,我们称呼被复制的服务器为主服务器(master)
,而对主服务器进行复制的服务器则被称为从服务器(slave)
- 执行这句命令:127.0.0.1:12345> SLAVEOF 127.0.0.1 6379。那么地址为127.0.0.1:12345的服务器将成为地址为127.0.0.1:6379的服务器的从服务器。而服务器127.0.0.1:6379则会成为127.0.0.1:12345的主服务器。
进行复制中的主从服务器双方的数据库将保存相同的数据,概念上将这种现象称作“数据库状态一致”,或者简称“一致”。
- 在Redis中,从服务器对主服务器的复制可以分为以下两种情况:
- 初次复制:从服务器以前没有复制过任何主服务器,或者从服务器当前要复制的主服务器和上一次复制的主服务器不同。
- 断线后重复制:处于命令传播阶段的主从服务器因为网络原因而中断了复制,但从服务器通过自动重连接重新连上了主服务器,并
继续复制
主服务器。
- 旧版复制功能的实现:
Redis的复制功能分为同步(sync)和命令传播(command propagate)两个操作
:- 同步操作用于将从服务器的数据库状态
更新
至主服务器当前所处的数据库状态。- 当客户端向从服务器发送SLAVEOF命令,要求从服务器复制主服务器时,从服务器首先需要执行同步操作,也即是,将从服务器的数据库状态更新至主服务器当前所处的数据库状态。 从服务器对主服务器的同步操作需要通过向主服务器发送SYNC命令来完成,以下是SYNC命令的执行步骤:
- 1)从服务器向主服务器
发送SYNC命令
。 - 2)收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个 RDB文件,
主服务器并将这个RDB文件发给从服务器
,并使用一个缓冲区记录从现在开始执行的所有写命令。 - 3)当主服务器的BGSAVE命令执行完毕时,主服务器会将BGSAVE命令生成的RDB文件发送给从服务器,从服务器接收并载入这个RDB文件,将自己的数据库状态更新至主服务器执行BGSAVE命令时的数据库状态。
- 4)
主服务器将记录在缓冲区里面的所有**写命令**发送给从服务器
, 从服务器执行这些写命令,将自己的数据库状态更新至主服务器数据库当前所处的状态。
- 1)从服务器向主服务器
- 当客户端向从服务器发送SLAVEOF命令,要求从服务器复制主服务器时,从服务器首先需要执行同步操作,也即是,将从服务器的数据库状态更新至主服务器当前所处的数据库状态。 从服务器对主服务器的同步操作需要通过向主服务器发送SYNC命令来完成,以下是SYNC命令的执行步骤:
- 命令传播(传播,感觉就像是发布订阅中的发布,你细品)操作则用于在主服务器的数据库状态被修改,导致主从服务器的数据库状态出现不一致时,
让主从服务器的数据库重新回到一致状态
。- 在同步操作执行完毕之后,主从服务器两者的数据库将达到一致状态,
但这种一致并不是一成不变的
,每当主服务器执行客户端发送的写命令时,主服务器的数据库就有可能会被修改,并导致主从服务器状态不再一致。(因为有个时间差嘛)
主服务器会将自己执行的写命令,也即是造成主从服务器不一致的那条写命令,发送给从服务器执行,当从服务器执行了相同的写命令之后,主从服务器将再次回到一致状态
- 在同步操作执行完毕之后,主从服务器两者的数据库将达到一致状态,
- 同步操作用于将从服务器的数据库状态
- 旧版复制功能的缺陷:
- 对于初次复制来说,旧版复制功能能够很好地完成任务,但**
对于断线后重复制来说,旧版复制功能虽然也能让主从服务器重新回到一致状态,但效率却非常低
(两个服务器保存的数据大部分都是相同的,为了让从服务器补足一小部分缺失的数据,却要让主从服务器重新执行一次SYNC命令
**,这种做法无疑是非常低效的)SYNC命令是一个非常耗费资源的操作
:每次执行SYNC命令,主从服务器需要执行以下动作:- 1)主服务器需要执行BGSAVE命令来生成RDB文件,
这个生成RDB文件操作会耗费主服务器大量的CPU、内存和磁盘I/O资源
。 - 2)主服务器需要将自己生成的RDB文件发送给从服务器,这个发送操作会耗费主从服务器大量的网络资源(带宽和流量),并对主服务器响应命令请求的时间产生影响。
- 3)接收到RDB文件的从服务器需要载入主服务器发来的RDB 文件,并且在载入期间,从服务器会因为阻塞而没办法处理命令请求。因为SYNC命令是一个如此耗费资源的操作,所以Redis有必要保证在真正有需要时才执行SYNC命令。
- 1)主服务器需要执行BGSAVE命令来生成RDB文件,
- 对于初次复制来说,旧版复制功能能够很好地完成任务,但**
- 新版复制功能的实现:为了解决
旧版复制功能在处理断线重复制情况时的低效问题
,Redis从2.8版本开始,使用PSYNC命令代替SYNC命令来执行复制时的同步操作
。- PSYNC命令具有
完整重同步(full resynchronization)和部分重同步 (partial resynchronization)
两种模式:- 完整重同步(full resynchronization):完整重同步用于处理初次复制情况:完整重同步的执行步骤和SYNC命令的执行步骤基本一样,
它们都是通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步
。 - 部分重同步 (partial resynchronization):部分重同步则用于处理断线后重复制情况:当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态。
- 虽然SYNC命令和PSYNC命令都可以**
让断线的主从服务器重新回到一致状态
,但执行部分重同步所需的资源比起执行SYNC命令所需的资源要少得多
,完成同步的速度也快得多
**。执行SYNC命令需要生成、传送和载入整个RDB文件,而部分重同步只需要将从服务器缺少的写命令发送给从服务器执行就可以了。
- 部分重同步的实现:
- 部分重同步功能由以下三个部分构成:
- 主服务器的
复制偏移量
(replication offset)和从服务器的复制偏移量
。- 执行复制的双方——主服务器和从服务器会分别维护一个复制偏移量:
- 主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N。
- 从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量的值加上N。
- 通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致状态:
如果主从服务器处于一致状态,那么主从服务器两者的偏移量总是相同的
。如果主从服务器两者的偏移量并不相同,那么说明主从服务器并未处于一致状态
。
- 执行复制的双方——主服务器和从服务器会分别维护一个复制偏移量:
- 主服务器的
复制积压缓冲区
(replication backlog)。:复制积压缓冲区是由主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列(新元素从一边进入队列,而旧元素从另一边弹出队列),默认大小为1MB
。当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务 器,还会将写命令入队到复制积压缓冲区里面
- 和普通先进先出队列随着元素的增加和减少而动态调整长度不同,
固定长度先进先出队列的长度是固定的,当入队元素的数量大于队列长度时,最先入队的元素会被弹出,而新元素会被放入队列
- 当从服务器重新连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作:
- 如果offset偏移量之后的数据(也即是偏移量offset+1开始的数据)仍然存在于复制积压缓冲区里面,那么主服务器将对从服务器执行部分重同步操作
- 如果offset偏移量之后的数据已经不存在于复制积压缓冲区,那么主服务器将对从服务器执行完整重同步操作。
- 根据需要调整复制积压缓冲区的大小:Redis为复制积压缓冲区设置的默认大小为1MB,如果主服务器需要执行大量写命令,又或者主从服务器断线后重连接所需的时 间比较长,那么这个大小也许并不合适。如果复制积压缓冲区的大小设置得不恰当,那么PSYNC命令的复制重同步模式就不能正常发挥作用,因此,正确估算和设置复制积压缓冲区的大小非常重要。
- 和普通先进先出队列随着元素的增加和减少而动态调整长度不同,
- 服务器的
运行ID(run ID)
:每个Redis服务器,不论主服务器还是从服务器,都会有自己的运行ID。运行ID在服务器启动时自动生成,由40个随机的十六进制字符组成,例如53b9b28df8042fdc9ab5e3fcbbbabff1d5dce2b3- 当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,
而从服务器则会将这个运行ID保存起来
。当从服务器断线并重新连上一个主服务器时,从服务器将向当前连接的主服务器发送之前保存的运行ID
:- 如果从服务器保存的运行ID和当前连接的主服务器的运行ID相同,那么说明从服务器断线之前复制的就是当前连接的这个主服务器, 主服务器可以继续尝试执行部分重同步操作。
- 相反地,如果从服务器保存的运行ID和当前连接的主服务器的运行ID并不相同,那么说明从服务器断线之前复制的主服务器并不是当前连接的这个主服务器,主服务器将对从服务器执行完整重同步操作。
- 当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,
- 主服务器的
- 部分重同步功能由以下三个部分构成:
- 虽然SYNC命令和PSYNC命令都可以**
- 完整重同步(full resynchronization):完整重同步用于处理初次复制情况:完整重同步的执行步骤和SYNC命令的执行步骤基本一样,
- PSYNC命令具有
- PSYNC命令的实现:
- PSYNC命令的调用方法有两种:
- 如果从服务器以前没有复制过任何主服务器,或者之前执行过SLAVEOF no one命令,那么从服务器在开始一次新的复制时将向主服务器发送PSYNC ? -1命令,主动请求主服务器进行完整重同步(因为这时不可能执行部分重同步)。
- 相反地,如果从服务器已经复制过某个主服务器,那么从服务器在开始一次新的复制时将向主服务器发送PSYNC 命 令:
其中runid是上一次复制的主服务器的运行ID,而offset则是从服务器当前的复制偏移量,接收到这个命令的主服务器会通过这两个参数来判断应该对从服务器执行哪种同步操作
。
- 根据情况,接收到PSYNC命令的主服务器会向从服务器返回以下三种回复的其中一种:
- ·如果主服务器返回+FULLRESYNC 回复,那么表示主服务器将与从服务器执行完整重同步操作:其中runid是这个主服务器的运行ID,从服务器会将这个ID保存起来,在下一次发送PSYNC命令时使用;而offset则是主服务器当前的复制偏移量,从服务器会将这个值作为自己的初始化偏移量。
- 如果主服务器返回+CONTINUE回复,那么表示主服务器将与从服务器执行部分重同步操作,从服务器只要等着主服务器将自己缺少的那部分数据发送过来就可以了。
- 如果主服务器返回-ERR回复,那么表示主服务器的版本低于Redis 2.8,它识别不了PSYNC命令,从服务器将向主服务器发送SYNC命令, 并与主服务器执行完整同步操作。
- PSYNC命令的调用方法有两种:
- 复制的实现:通过向从服务器发送SLAVEOF命令,我们可以让一个从服务器去复制一个主服务器:SLAVEOF <master_ip> <master_port>
Redis2.8或以上版本的复制功能的详细实现步骤
:比如客户端向从服务器发送这条命令时:127.0.0.1:12345> SLAVEOF 127.0.0.1 6379- 步骤1:设置主服务器的地址和端口:从服务器首先要做的就是将客户端给定的主服务器IP地址127.0.0.1 以及端口6379保存到服务器状态的masterhost属性和masterport属性里面:
struct redisServer { ... //主服务器的地址 char *masterhost; // 主服务器的端口 int masterport; ... };
- 步骤2:建立套接字连接(
从服务器创建连向主服务器的套接字
):在SLAVEOF命令执行之后,从服务器将根据命令所设置的IP地址和端口,创建连向主服务器的套接字连接
- 如果从服务器创建的套接字能成功连接(connect)到主服务器,那么从服务器将为这个套接字关联一个专门用于处理复制工作的文件事件处理器,这个处理器将负责执行后续的复制工作,比如接收RDB文件, 以及接收主服务器传播来的写命令
- 而
主服务器在接受(accept)从服务器的套接字连接
之后,将为该套接字创建相应的客户端状态
,并将从服务器看作是一个连接到主服务器的客户端来对待(因为从服务器向主服务器发送命令请求,所以从服务器可以看作是主服务器的客户端)
,这时从服务器将同时具有服务器(server)和客户端(client)两个身份:从服务器可以向主服务器发送命令请求,而主服务器则会向从服务器返回命令回复
- 步骤3:发送PING命令:从服务器成为主服务器的客户端之后,做的第一件事就是向主服务器发送一个PING命令,
- 这个PING命令有两个作用:
- 虽然主从服务器成功建立起了套接字连接,但双方并未使用该套接字进行过任何通信,
通过发送PING命令可以检查套接字的读写状态是否正常
。 - 因为复制工作接下来的几个步骤都必须在主服务器可以正常处理命令请求的状态下才能进行,通过发送PING命令可以
检查主服务器能否正常处理命令请求
。
- 虽然主从服务器成功建立起了套接字连接,但双方并未使用该套接字进行过任何通信,
- 从服务器在发送PING命令之后将遇到以下三种情况的其中一种:
- 如果主服务器向从服务器返回了一个命令回复,但从服务器却不能在规定的时限(timeout)内读取出命令回复的内容,那么表示主从服务器之间的网络连接状态不佳,不能继续执行复制工作的后续步骤。当出现这种情况时,从服务器断开并重新创建连向主服务器的套接字。
- 如果主服务器向从服务器返回一个错误,那么表示主服务器暂时没办法处理从服务器的命令请求,不能继续执行复制工作的后续步骤。 当出现这种情况时,从服务器断开并重新创建连向主服务器的套接字。 比如说,如果主服务器正在处理一个超时运行的脚本,那么当从服务器向主服务器发送PING命令时,从服务器将收到主服务器返回的BUSY Redisis busy running a script.You can only call SCRIPT KILL or SHUTDOWN NOSAVE.错误。
- 如果从服务器读取到"PONG"回复,那么表示主从服务器之间的网络连接状态正常,并且主服务器可以正常处理从服务器(客户端)发送 的命令请求,在这种情况下,从服务器可以继续执行复制工作的下个步骤。
- 这个PING命令有两个作用:
- 步骤4:身份验证:
- 从服务器在收到主服务器返回的"PONG"回复之后,下一步要做的 就是决定是否进行身份验证:
- 如果从服务器设置了masterauth选项,那么进行身份验证。
- 如果从服务器没有设置masterauth选项,那么不进行身份验证。 在需要进行身份验证的情况下,从服务器将向主服务器发送一条AUTH命令,命令的参数为从服务masterauth选项的值。
- 从服务器在身份验证阶段可能遇到的情况有以下几种:
所有错误情况都会令从服务器中止目前的复制工作,并从创建套接字开始重新执行复制,直到身份验证通过,或者从服务器放弃执行复制为止
。
- 如果主服务器没有设置requirepass选项,并且从服务器也没有设置masterauth选项,那么主服务器将继续执行从服务器发送的命令,复制工作可以继续进行。
- 如果从服务器通过AUTH命令发送的密码和主服务器requirepass选项所设置的密码相同,那么主服务器将继续执行从服务器发送的命令, 复制工作可以继续进行。与此相反,如果主从服务器设置的密码不相同,那么主服务器将返回一个invalid password错误。
- 如果主服务器设置了requirepass选项,但从服务器却没有设置masterauth选项,那么主服务器将返回一个NOAUTH错误。另一方面, 如果主服务器没有设置requirepass选项,但从服务器却设置了masterauth选项,那么主服务器将返回一个no password is set错误。 所有错误情况都会令从服务器中止目前的复制工作,并从创建套接字开始重新执行复制,直到身份验证通过,或者从服务器放弃执行复制为止。
- 从服务器在收到主服务器返回的"PONG"回复之后,下一步要做的 就是决定是否进行身份验证:
- 步骤5:发送端口信息:在身份验证步骤之后,
从服务器将执行命令REPLCONF listening- port <port-number>,向主服务器发送从服务器的监听端口号
。- 主服务器在接收到这个命令之后,
会将端口号记录在从服务器所对应的客户端状态的slave_listening_port属性中
:slave_listening_port属性目前唯一的作用就是在主服务器执行INFO replication命令时打印出从服务器的端口号
。
typedef struct redisClient { ... //从服务器的监听端口号。slave_listening_port属性目前唯一的作用就是在主服务器执行INFO replication命令时打印出从服务器的端口号。 int slave_listening_port; ... } redisClient;
- 主服务器在接收到这个命令之后,
- 步骤6:同步:从服务器将向主服务器发送PSYNC命令,执行同步操作,并将自己的数据库更新至主服务器数据库当前所处的状态。
- 在同步操作执行之前,只有
从服务器是主服务器的客户端
,但是在执行同步操作之后,主服务器也会成为从服务器的客户端
:(在同步操作执行之后,主从服务器双方都是对方的客户端, 它们可以互相向对方发送命令请求,或者互相向对方返回命令回复。正因为主服务器成为了从服务器的客户端,所以主服务器才可以通过发送写命令来改变从服务器的数据库状态,不仅同步操作需要用到这一点,这也是主服务器对从服务器执行命令传播操作的基础。
)- 如果PSYNC命令执行的是
完整重同步操作
,那么主服务器需要成为从服务器的客户端
,才能将保存在缓冲区里面的写命令发送给从服务器执行。 - 如果PSYNC命令执行的是
部分重同步操作
,那么主服务器需要成为从服务器的客户端,才能向从服务器发送保存在复制积压缓冲区里面的写命令。
- 如果PSYNC命令执行的是
- 在同步操作执行之前,只有
- 步骤7:命令传播:
当完成了同步之后,主从服务器就会进入命令传播阶段
,这时主服务器只要一直将自己执行的写命令发送给从服务器,而从服务器只要一直接收并执行主服务器发来的写命令,就可以保证主从服务器一直保持一致了
。
- 心跳检测:
在命令传播阶段
,从服务器默认会以每秒一次的频率,向主服务器发送命令:REPLCONF ACK <replication_offset>。replication_offset是从服务器当前的复制偏移量
主服务器通过向从服务器传播命令来更新从服务器的状态,保持主从服务器一致,而从服务器则通过向主服务器发送命令来进行心跳检测,以及命令丢失检测。
- 发送REPLCONF ACK命令对于主从服务器有三个作用:
- 检测主从服务器的网络连接状态。
- 主从服务器可以
通过发送和接收REPLCONF ACK命令来检查两者之间的网络连接是否正常
:如果主服务器超过一秒钟没有收到从服务器发来的REPLCONF ACK命令,那么主服务器就知道主从服务器之间的连接出现问题了。 - 通过向主服务器发送INFO replication命令,在列出的从服务器列表的lag一栏中,我们可以看到相应从服务器最后一次向主服务器发送 REPLCONF ACK命令
距离现在过了多少秒
:在一般情况下,lag的值应该在0秒或者1秒之间跳动,如果超过1秒 的话,那么说明主从服务器之间的连接出现了故障
。
- 主从服务器可以
- 辅助实现min-slaves选项。
- Redis的
min-slaves-to-write和min-slaves-max-lag两个选项
可以防止主服务器
在不安全的情况下执行写命令。(min-slaves-to-write 3代表在从服务器的数量少于3个;min-slaves-max-lag 10代表三个从服务器的延迟(lag) 值都大于或等于10秒时,主服务器将拒绝执行写命令,这里的延迟值就 是上面提到的INFO replication命令的lag值。)
- Redis的
- 检测命令丢失。
- 如果因为网络故障,
主服务器传播给从服务器的写命令在半路丢失
,那么当从服务器向主服务器发送REPLCONF ACK命令时,主服务器将发觉从服务器当前的复制偏移量少于自己的复制偏移量
,然后主服务器就会根据从服务器提交的复制偏移量
,在复制积压缓冲区
里面找到从服务器缺少的数据
,并将这些数据再次重新发送给从服务器
。主服务器向从服务器补发缺失数据这一操作的原理和部分重同步操作的原理非常相似
,这两个操作的区别在于,补发缺失数据操作在主从服务器没有断线的情况下执行,而部分重同步操作则在主从服务器断线并重连之后执行
。- REPLCONF ACK命令和复制积压缓冲区都是Redis 2.8版本新增的,
在Redis 2.8版本以前,即使命令在传播过程中丢失,主服务器和从服务器都不会注意到,主服务器更不会向从服务器补发丢失的数据
,所以为了保证复制时主从服务器的数据一致性,最好使用 2.8或以上版本的Redis
。
- 如果因为网络故障,
- 检测主从服务器的网络连接状态。
巨人的肩膀:
moon聊技术
redis设计与实现
redis官方文档
小林coding