[Redis] 3w字带你详解redis 分布式系统 | 数据范例内部编码 | 过期策略| 持 ...

打印 上一主题 下一主题

主题 862|帖子 862|积分 2586

 Author:MTingle
major:人工智能
  
  Build your hopes like a tower!
  


目次
一.认识分布式系统
单机架构vs分布式系统
应用数据分离架构
读写分离/主从分离架构
微服务架构
本章小结

二.认识Redis
Redis为什么快?
Redis使用场景

三.Redis的过期策略

四.数据范例及其内部编码
1.String数据范例的应用场景
数据查询与存储
Session会话
手机验证码
2.Hash数据范例的应用场景
作为缓存(数据库稀疏性)
3.List数据范例的应用场景 
作为"数组"布局存储元素
Redis作为消息队列
4.Set数据范例的应用场景 
使用set来生存用户的"标签"
使用set来计算用户的共同好友
使用set统计UV
5.Zset数据范例的应用场景 
排行榜系统
6.其他数据范例的应用场景 
Redis geospatial
Redis HyperLogLog
Redis bitmaps && Redis bitfields
7.渐进式遍历
8.n次命令执行与一次命令执行

五.Redis长期化
1.RDB机制(Redis DataBase)
RDB工作流程小结
2.AOF机制(Append Only Field)
Rewrite(重写机制)
AOF的重写流程
3.RDB与AOF的关联
4.混淆长期化
5.本章小结

六.Redis事务
弱原子性
不具备一致性
不具备长期性
不涉及隔离性
Redis事务的实现
WATCH

七.主从复制
什么是主从模式
Nagle算法
拓补布局
全量同步与增量同步
Replication ID
Offset(偏移量)
全量复制流程
部分复制流程
实时复制
心跳包机制

八.哨兵机制
哨兵节点的工作流程    
主节点的挑选
本章小结

九.集群 
分片方式
哈希求余
一致性哈希算法
哈希槽分区算法
集群容灾
数据冗余与备份规复
故障判断
故障迁移
本章小结

十.缓存
"二八定律"
缓存的更新策略
定期天生
实时天生
内存淘汰策略
通用的淘汰策略
Redis内置的淘汰策略
缓存预热,缓存穿透,缓存雪崩,缓存击穿
缓存预热
缓存穿透
缓存雪崩
缓存击穿
本章小结
5.缓存预热,缓存穿透,缓存雪崩,缓存击穿.

十一.分布式锁
锁的过期时间
解锁的校验机制
分布式锁与事务
过期时间续约问题("看门狗watch dog"机制)
分布式锁挂了
redlock算法
本章小结


一.认识分布式系统

单机架构vs分布式系统

单机架构,只有一台服务器,这个服务器负责全部工作.当下,固然分布式系统听起来十分高大上,但是究竟上绝大部分的公司产物都是这种单机架构,哪怕只有一台主机,这台主机的性能也是非常高的,可以支持非常高的并发,非常大的数据存储.

假如公司的业务进一步增长,用户量和数据量都水涨船高,一台主机难以应付的时候,就可以引入更多的主机和硬件资源,但是一台主机上的硬件资源是有限的,当某种资源不够用的时候,我们的处理方式分为以下两种:
1.节省:软件上的优化,但是十分困难,对程序员要求十分高.
2.开源:简单粗暴,增加更多的硬件资源,但是一台主机可以增加的资源是有限的,所以我们就要引入多台主机,一旦引入了多台主机,就可以称为分布式系统了.
系统中的多个模块被部署于不同服务器之上,即可以将该系统称为分布式系统。如Web服务器与 数据库分别工作在不同的服务器上,大概多台Web服务器被分别部署在不同服务器上。
被部署于多台服务器上的、为了实现特定目标的⼀个/组特定的组件,整个团体被称为集群。好比 多个MySQL工作在不同服务器上,共同提供数据库服务目标,可以被称为⼀组数据库集群。(MySQL是一个客户端服务器布局的程序,本体是MySQL服务器)

应用数据分离架构




应用服务器包罗很多业务和逻辑,比力吃cpu和内存,数据库服务器需要更大的硬盘空间,更快的访问速度,配置更大的硬盘空间,还可以使用SSD硬盘.



负载平衡:为了解决用户流量向哪台应用服务器分发的问题,需要⼀个专门的系统组件做流量分发。实际中负载平衡不仅仅指的是工作在应用层的,甚至可能是其他的网络层之中。就像公司的领导,将任务分配给每个组员.
应用服务器比力吃CPU和内存,通过负载平衡,可以引入更多应用服务器,解决上述问题.
负载平衡器承担了全部的哀求,那他不会顶不住吗?负载平衡器对于哀求的承担能力是远超过应用服务器的,他就像一个学校的校长,负责管理老师,是用来分配工作的,而学校的老师是负责执行任务的,假如一个负载平衡器顶不住了,我们也可以引入多个负载平衡器.
以下是几种常见的负载平衡器的流量调理算法
• Round-Robin轮询算法。即非常公平地将哀求依次分给不同的应用服务器。
• Weight-Round-Robin轮询算法。为不同的服务器(好比性能不同)赋予不同的权重(weight), 能者多劳。
• ⼀致哈希散列算法。通过计算用户的特性值(好比IP地址)得到哈希值,根据哈希结果做分发,长处是确保来自相同用户的哀求总是被分给指定的服务器。也就是我们平常遇到的专项客户司理服务。
在当前架构中,固然我们引入了多台应用服务器,解决了cpu和内存的问题,但是这些应用服务器都要访问同一个数据库,数据库是十分脆弱的,当他顶不住云云大的访问量时就会挂掉,这是十分可怕的事故!!!
那我们能像应用服务器的处理方式一样,只是简单的扩大数据库的数量吗?当然是不行的!!!想想这么一个场景:银行管理的账户金额,假如收到⼀笔转账之后⼀份数据库的数据修改了,但另外的数据库没有修改,则用户得到的存款金额将是错误的。此时,数据库的一致性就无法包管了.

读写分离/主从分离架构





于是,我们就引入了读写分离架构,但是在实际的应用场景中,读数据库的操纵是远远高于写数据库的,所以,我们可以构造一个一写多读的布局,即一主多从,主数据库负责写操纵,读数据库要读写数据库的数据做到同步.
数据库天然有个问题,就是相应速度十分慢,于是,我们可以把数据库中常常使用的数据,即热点数据,放到缓存中,缓存访问的速度比我们的内存快得多,于是,我们就实现了数据的冷热分离.



此时,缓存服务器就可以为数据库负重前行,数据的访问依照二八定律,20%的数据可以或许支持80%的访问量.欲带皇冠,必承其重,缓存想要快,代价就是小!
引入分布式系统,不光要可以或许应对更高的哀求量,也要可以或许应对更大的数据量,此时,可能存在一台服务器存不下数据的情况,此时,我们就需要引入更多的主机来存储.

针对数据库进行进一步的拆分,本来一个数据库服务器上就有多个数据库,现在就可以引入多个数据库服务器,每个数据库服务器存储一个大概一部分数据库.假如某个表特别大,我们也可以针对表进行拆分.实现分库分表.
微服务架构


之前的应用服务器,一个服务程序内里做了很多的业务,这个可能导致一个服务器的代码变得越来越复杂,为了方便服务器代码的维护,就可以把这一个复杂的服务器拆分成更多的,功能更单一的,但是更小的服务器,这个就是我们的微服务架构.
微服务的本质是在解决"人"的问题.当应用服务器复杂了,势必要更多的人来维护,当人多了,就需要配套的管理,把这些人组织好.我们可以按照功能,把庞大的服务拆分成多组,就有利于职员的组织布局分配了.
引入微服务,解决了人的问题,但也要付出相应的代价,系统性能下降,拆分出更多的服务,多个功能之间更加依赖网络通信,网络通信的速度比访问硬盘还要慢,为了包管性能不会下降太多,就需要更多的硬件资源,更多的呆板,也就是更多的钱!
而且,系统的复杂度也随着微服务的引入提高,可用性受到影响,服务器多了,出现问题的概率就更大了.这就需要一系列的本事和相应的运维职员来包管系统的可用性,
总结一下微服务的优势:
1.解决了人的问题.
2.使用微服务,可以更方便功能的复用.
3.可以给不同的服务进行不同的部署.
本章小结

1.单机架构(应用程序+数据库服务器)
2.数据库和应用分离,应用程序和数据库服务器分别放到了不同的主机上部署.
3.引入负载平衡,通过负载平衡器,把哀求比力匀称的分发给集群中的每个应用服务器.
4.引入读写分离,数据库主从布局.主节点负责写,从节点负责读,一写多读.
5.引入缓存,冷热数据分离,二八原则,进一步提高服务器针对哀求的处理能力.
6.引入分库分表,可以或许进一步拓展数据库的存储空间.
7.引入微服务,从业务上进一步拆分应用服务器,从业务功能的角度,把应用服务器拆分成更多的功能更单一,更简单,更小的服务器.
所谓的分布式,就是想办法引入更多的硬件资源!!!


二.认识Redis

在第一节冷热分离的阶段,redis就已经登场了,那什么是redis呢?
Redis 是⼀种基于键值对(key-value)的NoSQL数据库,与很多键值对数据库不同的是,Redis 中的值可以是由string(字符串)、hash(哈希)、list(列表)、set(聚集)、zset(有序聚集)、 Bitmaps(位图)、HyperLogLog、GEO(地理信息定位)等多种数据布局和算法组成,因此Redis 可以满足很多的应用场景,而且且由于Redis会将全部数据都存放再内存中,所以它的读写性能非常惊人.
Redis的特性(长处)
1.redis接纳 key-value 的布局存储数据, key 都是 String ,而 value 则可以是上述的这些数据布局,MySQL主要是通过"表"的方式存储组织数据的关系型数据库,而redis通过键值对的方式组织存储数据,是非关系型数据库.
2.针对redis的操纵,可以直接通过简单的交互式命令进行操纵,也可以通过一些脚本的方式,批量执行一些操纵,这些操纵可以带有一些逻辑,redis内嵌了lua脚本.
3.redis具有良好的扩展性,可以在redis原有的基础功能上在进行拓展,redis提供了一组api.可以自己去拓展redis的功能
4.长期化.redis把数据存储在内存上,内存上的数据是已丢失的,好比历程退出大概系统重启时,redis也会把数据存储在硬盘上,内存为主,硬盘为辅,硬盘相称于对内存的数据进行了备份,假如redis重启了,就会在重启时加载硬盘中的备份数据,使redis的内存规复到重启前的状态.
5.集群,redis作为一个分布式系统的中间件,可以或许支持集群是十分关键的,这个程度拓展雷同于MySQL的分库分表.一个redis能存储的数据是有限的,引入多个主机,部署多个redis节点,每个redis存储数据的一部分.
6.高可用,redis自身也是支持"主从"布局的,从节点相称于主节点的备份,高可用的焦点是备份/冗余.
Redis为什么快?

1.Redis的数据在内存中,访问内存的速度远快于访问硬盘.
2.Redis的焦点功能都是比力简单的逻辑,焦点功能都是比力简单的操纵内存的数据布局.
3.从网络的角度上,redis使用了IO多路复用的方式,使用一个线程,管理多个socket.
4.Redis是单线程模型(高版本的Redis引入了多线程)如许的单线程模型,减少了不必要的线程之间的竞争开销(多线程要想提高效率,条件是CPU密集型任务,多个线程充实使用CPU多核资源)但是Redis的焦点任务主要是操纵内存的数据布局,不会很吃CPU.
Redis只是用一个线程处理全部的命令哀求,不是说Redis服务器历程内部真的就只有一个线程,其实也有多个线程,多个线程用于处理网络IO,假设有多个客户端同时操纵一个Redis服务器:


此时这两个服务器相称于并发执行上述的哀求,此时是否意味着也会产生雷同的线程安全问题?
荣幸的是,不会产生线程安全问题,由于Redis服务器内部实际上是单线程模型,包管了当前收到的多个哀求是串行执行的!!!
多个哀求同时到达Redis服务器,在队列中排队,等候Redis将其一个个取出,次序执行命令.
这个场景就涉及到了:粘包问题
粘包问题是面向字节省语言的共同问题,此时我们可以指定命令的长度大概用特别标识符将不同的命令间隔开来解决粘包问题.
5.Redis使用C语言开发(但是此种说法不太准确,由于MySQL也是使用C语言开发的,但是很慢).
Redis使用场景

1.作为数据库,大多数情况下,考虑到数据存储,优先考虑大,但也有时候,优先考虑快!
2.使用MySQL存数据,但是根据二八定律把热点数据取出,存入Redis中,让Redis为MySQL负重前行.'
3.消息队列,Redis开发的初志,固然事与愿违.
4.session storage.cookie是实现用户信息的生存,需要session共同,session存储在服务器上,cookie是欣赏器在本地长期化存储信息的一种机制,欣赏器在本地存储了一个用户的身份标识(sessionId)

在多台服务器的情况下,客户端的登录操纵通过负载平衡器可能会分配到不同的应用服务器,不同的服务器不持有用户先前的登录信息,这就可能导致用户反复登录,但是我们通过Redis,就可以使这多台服务器共享同一份登录信息,应用程序重启了,会话不丢失.
大概也可以想办法让负载平衡器把同一个用户的哀求始终打到同一个呆板上,但如许就不能轮询了.
Redis常常用于缓存,挡在MySQL前面,替MySQL负重前行,在生产情况中,我们不能使用 keys * 操纵,就像在MySQL中不能使用 select * 一样,假如Redis被一个keys * 阻塞了,此时其他的Redis操纵就超时了,这些哀求就会直接查数据库,忽然一大波哀求过来,MySQL措手不及,就很轻易挂了,此时整个系统就根本瘫痪了,假如不能及时发现,将会造成巨大的损失.
雷同的,假如误删了几条Redis中的热点数据,影响不大,但是假如删除了全部大概很大一部分数据,这些哀求就会跑到MySQL这边,很轻易把MySQL搞挂!!!
三.Redis的过期策略

Redis中的 key 是怎么实现过期的呢?一个Redis中可能同时存在很多很多的key,这些key中可能有很大一部分都有过期时间,此时,Redis服务器是咋知道哪些可以已经过期要被删除了,哪些key还没过期?假如直接遍历全部的key,显然是行不通的,而且效率非常低下,还可能会导致哀求超时,哀求直接跑去查MySQL了.
Redis的团体策略是:
1.定期删除,每次抽取一部分,进行验证过期时间,同时要包管这个抽取查抄的过程足够快,这里之所以对定期删除的时间有明白要求,是由于Redis是单线程程序,主要的任务是处理每个命令的任务,其次才是扫描过期的key,假如扫描过期的key消耗的时间过多,就可能导致正常处理哀求的命令阻塞了,产生雷同于 keys * 的效果,把MySQL服务器搞挂.
2.惰性删除,假设这个key已经到了定期删除的时间,但是暂时还没删除它,key还存在,紧接着,反面又有一次操纵访问这个key,此时Redis就会去查抄这个key是否过期了,于是就触发了过期删除操纵,然后返回一个nil.
这就像去超市买东西,超时会定期去扫除过期的商品(定期删除),当我们拿到一个商品去购物台结账时,老板查抄了一下过期时间,发现了商品过期了,就不会卖给我们,于是我们就白手而归.(惰性删除,返回nil).
要留意,Redis并没有接纳定时器的方式来实现过期 key 的删除,这种方法基于优先级队列大概时间轮,实现一个高效的定时器,高效,节省CPU的条件下来处理多个key,为啥Redis没有接纳定时器的方式,官方没有给出具体的理由,猜测可能是Redis早期版本奠定了单线程的基调,这种方法要引入多线程,打破了作者的初志.但是,我们依然有必要了解一下这两种策略.
1.基于优先级队列
优先级队列是按照优先级进出,优先级高的先出队列,于是,我们可以界说"过期时间越早的,优先级就越高",现在假定有多个key在一个优先级队列中,指定过期时间早的先出队列,队首元素就是最早要过期的key,此时定时器中只要分配一个线程,让这个线程去查抄队首元素看是否过期即可,此时扫描不需要遍历全部的key,只需要盯着队首元素即可.
但是,我们也不能针对队首元素查抄太频繁,会浪费CPU资源,此时的做法就是可以根据当前时刻和队首元素的过期时间设置一个等候,等时间差不多了,系统再唤醒这个线程去查抄.此时就可以资助我们节省CPU开销.
当线程休眠时,假如有新的任务加入队列,就可以唤醒线程,查抄一下队首元素,重新设置阻塞时间即可.
2.基于时间轮实现的定时器

我们可以把时间划分成很多个小段,每个小段都挂着一个链表,这个指针会每隔固定的时间就走到下一个格子,把这个格子上链表的任务实验执行以下,观察是否过期.
四.数据范例及其内部编码



list中的linkedlist和ziplist,从Redis3.2之后引入了新的quicklist,同时分身了linkedlist和ziplist的有点,quicklist就是一个链表,每个元素又是一个ziplist,把空间和效率都折中分身到.
在哈希表元素比力少时,会被压缩成ziplist,可以或许节省空间,Redis上有很多key,可能某些key的value是hash,此时假如key特别的多,对应的hash也会特别多,但是每个hash又不大的情况,就只管去压缩,之后就可以让团体占用的内存更小.
使用 object encoding key 就可以查看对应的value实际编码方式.
1.String数据范例的应用场景

String内部有三种编码方式
1). int 64位/8字节整数.
2). embstr 压缩字符串,实用于表现比力短的字符串.
3). raw 普通字符串,实用于表现更长的字符串,只是单纯的持有字节的数组. 
数据查询与存储




应用服务器访问数据时,先查询Redis,假如Redis中存在数据,则从Redis中读取数据并返回,不需要读取数据库,若Redis中不存在,则去访问MySQL,把读到的结果取出,而且写入到Redis中,那么,当Redis中的数据越来越多时,该怎么办?
1.数据写入的时候会界说一个过期时间
2.Redis也在内存不敷时,提供了淘汰策略(后文详解)
Session会话

cookie,欣赏器存储数据的机制.
session,服务器存储数据的机制,这两个都有键值对组成.
当我们去医院看病时,会有多个医生,就相称于多个服务器,通过负载平衡的方式为我们分配医生,当我们看病前,我们会刷一下就诊卡,医生通过就诊卡的数据就可以对我们的信息有一个清楚的了解,当我们下次来看病时,通过负载平衡分配到了另一个医生,那当我们就诊前刷就诊卡后,这个医生也可以对我们的病情和个人信息一清二楚,cookie和session就起到了雷同的作用,让不同的服务器都能给我们提供良好的服务.


手机验证码

很多应用出于安全考虑,会在每次进行登录时,让用户输入手机号而且共同给手机发送验证码,然后让用户再次输入收到的验证码并进行验证,从而确定是否是用户本人。为了短信接口不会频繁访 问,会限制用户每分钟获取验证码的频率,比方一分钟不能超过5次.

2.Hash数据范例的应用场景

Redis本身就是键值对组成的,key是字符串范例,value则可以是哈希范例.
形如 key f1 value1 f2 value2 f3 value3 ......
hash内部编码方式有两种,一种是ziplist,另一种是hashtable.ziplist可以节省空间,有得必有失,ziplist的读写速度是比力慢的,当元素过多,就会慢的落井下石,因此当元素过多时,编码方式就会酿成哈希表,哈希表首先是一个数组,有的有元素,有的没有,所以就会存在一定的空间浪费.
当我们需要遍历hash表时,可以使用hscan,渐进式遍历,可以防止不警惕把MySQL搞挂,hscan就是敲一次命令,遍历一小部分,在敲一次命令,再遍历一小部分...直到遍历完整个hash表.
这种化整为零的思想,我们之前在学习concurrentHashMap时也打仗过,concurrentHashMap是一个线程安全的哈希表,他在扩容时也是一次扩容一部分,按照化整为零的方式完成扩容.
作为缓存(数据库稀疏性)

String固然可以作为缓存,但是很多时候hash会更加符合.



上述场景中,使用String也可以达成同样的目的,但是需要使用JSON的格式,但是假如需要修改某个字段,则需要把整个JSON都取出来,然后解析成对象再操纵,操纵结束后再重写成JSON的格式写会redis,但是,假如使用hash,则会非常方便数据的修改~
hash的方式,读写field更加直观高效,但也因此付出了更大的存储空间.
使用原生字符串范例这种方式:

相称于把同一个数据的各个属性给分散开了,低内聚,我们在管理数据时都是要追求高内聚,低耦合的.
所谓高内聚,就是把关联的东西都放一起,最好都能放到指定的地方.
所谓耦合则是指,两个模块,代码之间的关联关系,低耦合就是关联关系小,不轻易互相影响,关联关系越大,耦合越大,越轻易互相影响.追求低耦合,避免牵一发而动浑身,一个地方报错,处处报错.

3.List数据范例的应用场景 





list内部的编码方式并非是一个简单的数组,而是更接近于"双端队列",列表的元素是有序的,且允许重复.
作为"数组"布局存储元素


Redis中的数据如何组织要根据实际应用场景来决定.
Redis作为消息队列


谁先执行这个brpop命令,谁就能拿到这个元素,紧接着就要返回.举个栗子,假如消耗者1拿到元素,消耗者以就要从队列中返回,紧接着就是消耗者2拿元素,消耗者1假如想要继续拿元素,就要再次执行brpop这个命令,起到了一个轮询的效果.

多个列表这种场景非常常见,比方我们常用的抖音,一个通道用来传输短视频数据,一个用来传递点赞信息,一个用来传输评论数据.多个频道可以让数据传输时不会互相影响,解耦合.

4.Set数据范例的应用场景 

聚集就是把一下有关联的数据放到一起,聚集的元素是无序的,且不能重复.inset为整数聚集,这是为了节省空间做出的优化,当元素均为整数时,而且元素个数不是很多的时候,Redis会将set范例优化为整数聚集,当元素个数变多,大概种类有非整数时,就会用hashtable存储元素.
使用set来生存用户的"标签"

根据用户的特性,如性别,年事,居住地,爱好等...定制用户画像,投其所好推送相关产物.上述数据,很多公司之间都在共享,搜集到的用户特性,就会转换成"标签"(简短的字符串),此时就可以把标签生存到Redis的set中.
使用set来计算用户的共同好友

基于"聚集求交集",A和B是共同好友,A和C是共同好友,B和C是共同好友,于是系统就会把D保举给A,基于聚集操纵可以实现好友保举.
使用set统计UV

一个互联网产物一般通过PV(page view)和UV(user view)两个指标衡量用户规模.
PV: 用户每次访问该服务器,每次访问都会产生一个pv.
UV: 每个用户访问服务器,都会产生一个uv,但是同一个用户多次访问,不会使uv增加,uv需要按照用户进行去重,上述的去重过程可以通过set实现.

5.Zset数据范例的应用场景 

Zset聚集是一个有序聚集.Zset中的member引入了一个属性,score,浮点范例,每个member都会安排一个分数,进行排序的时候,就是按照此处的分数巨细进行升序/降序排序.
Zset中的元素仍然要求是唯一的.但是score可以重复,重复score则按照member的字典序排序.Zset主要照旧用来存member的,score只是辅助.

排行榜系统

Zset最关键的应用场景照旧排行榜系统,比方微博热搜,游戏天梯排行,成绩排行等,用来排行的分数固然是实时厘革的,但是Zset也能做到高效更新.调解的时间复杂度为(logN).
玩家那么多,Zset能存下来吗?答案是肯定的,哪怕一个游戏有1亿个玩家,我们设置userId为4个字节,score为8个字节,存储一个玩家大概是12个字节,12亿字节省等于1.2GB,这个对于一个游戏来说,存下来照旧轻轻松松的.亿,万万,百万这些单元,对于数据量没有直观的认识时,我们可能会认为特别大,但是对于计算机来说都只是小case.
大概的换算规则为:10亿==1GB,1GB==1000MB,1MB==1000kb,请务必做到"章口就来".
对于游戏排行榜系统,有些排序是比力复杂的,需要综合数据,好比微博的热度,需要综合欣赏量,点赞量,转发量,评论量等等,这个时候我们就可以使用zset的weight计算得分进行排序.此时的score就是各个维度数值的综合结果.具体怎么分配weight要具体情况具体分析.
以上我们就将Redis常见的会集数据范例介绍完毕了,下面我们来介绍一些特定情况下比力好用的数据范例.

6.其他数据范例的应用场景 

Redis geospatial

用来存储坐标(经纬度),存储一些点后,就可以让用户给定一个坐标,去从刚才存储的点里进行查找,可以按照半径,矩形地区等方法.这个功能在地图中十分重要.

Redis HyperLogLog

应用场景只有一个,估算聚集中的元素个数.
Set中有一个应用场景,就是统计服务器的UV,使用set当然可以完成统计,但是假如UV数据量特别大的话,就会消耗set中很多的内存空间.假设set存储的userId按照8个字节存储,一亿个UV就是8亿个字节,大概是0.8GB,而HyperLogLog可以最多使用12kb完成上述效果.set之所以需要消耗这么大的空间,是由于set需要存储每个元素,而HyperLogLog不存储元素的内容,但是能记录元素的特性,从而在新增元素的时候,可以或许知道当前新增的元素是已经存在,照旧是一个全新的元素,但是HyperLogLog是估算,并不是准确的计算.只能用来计算,但是不能告诉你这些元素是啥.HyperLogLog存储元素的时候,提取特性的过程是不可你的,数据的信息大量丢失,就像可以把猪肉酿成猪肉铺,但是没办法把猪肉铺变回猪肉.
Redis bitmaps && Redis bitfields

Redis bitmaps: 位图,使用bit位来进行表现整数,位图本质上照旧一个聚集,是set范例针对整数的特化版本,可以节省空间,而且把10存储在位图中,计算机进行位运算是十分高效的,
Redis bitfields: 位域,可以明白成一串二进制序列(字节数组),同时可以把这个字节数组中的某几个位,赋予特定的含义,而且可以进行读取,修改,算术运算等相关操纵.位域相比起hash,String来说,目的仍然是节省空间.
7.渐进式遍历

keys * 一次性的把整个redis中全部的key都获取到,这个操纵会比力危险,可能一下子太多的key,阻塞redis的服务器,进而导致本应redis服务的哀求超时,通过渐进式遍历就可以一次获取到全部的key,又不会卡死服务器.

cursor: 光标,指向了当前遍历的位置.当光标为0,就意味着此次遍历是重新开始获取,返回分为两部分:
 1) 是为了告诉你,下次遍历从哪开始.
2) 真正遍历到的key的内容.
cursor仅仅是一个字符串而已,不能明白成"下标",不是一个递增的整数,光标这个概念,客户端是不能认识的,redis服务器则可以知道光标对应的位置.
count只是给redis的一个"提示"大概"建议",具体写入的count和返回的key个数不一定完全相同,但是不会相差很多.此处的count和MySQL的limit不一样,MySQL的limit则是准确的个数.
cout这里的数字不需要每次遍历都设置成一样的,这里的渐进式遍历,在遍历的过程中不会在服务器这边存储任何的状态信息,此处的遍历是可以随时终止的,不会对服务器产生任何的副作用.
渐进式遍历的scan固然解决了阻塞的问题,但假如遍历期间键有所厘革,增加,修改,删除,则可能导致便利的时候键重复遍历大概遗漏,这带你务必要在实际开发中考虑.
8.n次命令执行与一次命令执行



网络通信的过程是十分消耗时间的,而且网络之间通信需要消耗网络带宽,而且带宽成本十分昂贵,所以我们只管可以一次通信完成的不要分成多次.
五.Redis长期化

MySQL 的事务有四个比力焦点的特性:
1.原子性: 原子性、通过事务,把多个操纵打包到一起.(事务最重要的特性,初心!!)
2.一致性: 相称于原子性的延伸.当数据库中间出问题了,好比a和b转账,但是中途发生了以外,a的账户已经扣钱了,但是b没有拿到钱,钱凭空消失了,一致性,可以包管不会产生像上述这种, "钱凭空消失",这种不科学的情况,另一方面,还通过约束,来避免数据出现一些非法的情况.
3.长期化: 事务任何的修改,都是长期化存在(写入硬盘的),无论是重起程序,照旧重启主机,修改都不会丢失。(数据库本身就是为了长期化存储)
4.隔离性: 多个事务并发执行的时候,可能会带来一些问题,通过隔离性来对这里的问题进行权衡,看你是希望数据只管准确,照旧速度只管快.
而此处我们谈到的Redis的长期化,我们知道Redis是一个内存数据库,把数据存储存储在内存中,内存的数据是不长期的,当系统重启等情况发生了,就会丢失,要想做到长期,Redis就要把数据存储到硬盘上.
Redis相比MySQL如许的关系型数据库,最明显的优势就是快,为了包管速度快,数据照旧得存在内存中,但是数据还得想办法存储在硬盘上.
当我们插入一个数据时,就需要把这个数据同时写入到内存和硬盘中.当我们查询某个数据的时候,直接从内存读取,硬盘数据只是在Redis重启的时候,用来规复内存中的数据的.固然消耗了更多的空间,一份数据存储了两遍,但是硬盘是比力便宜的,如许的开销不会带来太多的成本.
Redis实现长期化的策略有两个,一个是RDB,另一个是AOF.
RDB为定期备份,AOF则是实时备份.
1.RDB机制(Redis DataBase)

RDB定期把我们redis内存中的全部数据,都写入硬盘中,天生一个"快照",后续Redis一旦重启,就可以根据刚刚的"快照",就能把内存中的数据给规复回来.
"定期"具体来说有两种实现方式:
1). 手动触发,程序员通过Redis客户端,执行特定的命令,来触发快照的天生,命令又分为以下两种:
a). save: 执行save的时候,Redis就会全力以赴的进行"快照天生"操纵,此时就会阻塞 Redis 的其他客户端的命令,导致雷同 keys * 如许的后果.
b). bgsave: bg的意思为background,不会影响Redis服务器处理其他客户端的哀求和命令.
那么我们前面学到过,Redis是单线程,那这是不是意味着,Redis偷偷搞成了多线程?
并非云云,此处为并发编程,Redis使用的是"多历程"的方式来完成并发编程,来完成bgsave的实现.
当我们执行 bgsave 的时候,会创建出一个子历程,由子历程完成长期化操纵,子历程的状态和父历程执行fork命令前的一模一样.而且,当子历程和父历程依照写时拷贝的原则,当他们内容相同时,他们会共用同一份内存,只有当父历程的内容发生改变时,他们才会"分家".
假如直接使用 save 命令,此时是不会触发子历程和文件替换逻辑的,假如是save命令,就直接在当前历程中,往刚才的同一个文件中写入数据.
2).主动触发,在Redis的配置文件中,设置一下,让Redis每隔多长时间/每产生多少次修改就触发.RDB长期化操纵是可以触发多次的.

Redis天生的RDB文件,是存放在Redis的工作目次中的,也是在Redis配置文件中进行设置的.默认的RDB机制天生的镜像文件为 dump.rdb ,Redis服务器默认就是开启了RDB的.
rgb文件是一个二进制文件,把内存中的数据以压缩的形式,生存到这个二进制文件中.压缩固然需要消耗一定的CPU资源,但是可以节省存储空间.这个文件不可以胡乱修改,假如把数据格式破坏了,后续当Redis服务器重新启动,需要实验加载这个RDB文件时,假如发现格式错误,可能就会加载数据失败.rdb文件也可能会由于一些意外,好比网络传输故障而被破坏,此时redis服务器就无法启动.
Redis还未我们提供了rdb文件的查抄工具,redis-check-rdb*.运行的时候,加入RDB文件作为命令行参数,此时就是以查抄工具的方式来运行,不会真的启动redis服务器.
当执行天生RDB镜像操纵的时候,此时就会把需要天生的快照数据先生存到一个临时文件中,当这个快照天生完毕之后,再删除之前的RDB文件,把新天生的临时RDB文件修改成刚刚的dump.rdb.自始至终,RDB文件都是只有一个的.
打开redis的配置文件,找到相应的内容,就可以对RDB天生的要求进行修改.

但是,此处的数据修改有一个根本的原则:天生一次RDB快照,这个成本是一个比力高的成本,因此,不能让这个操纵执行的过于频繁.两次RDB之间的间隔,最少的是60s.
假如我们同时收到了多个天生RDB文件的哀求,系统只会执行此中一个,别的哀求则会返回.
假设这么一个场景,12点时天生了RDB文件,此时硬盘上的快照数据和内存中的一致,1分钟后,redis开始收到了大量的key的厘革哀求,要再过一分钟,才能天生下一个快照文件,但是悲催的是,我们的redis服务器在快照天生一半的时候挂了,此时就会导致12点之后的数据丢失了,AOF机制的提出就是为了解决这个问题!
当我们正常关闭redis的时候,比方命令:  service redis-server restart ,也会触发RDB机制.但假如是 kill -9 大概 服务器掉电,此时redis来不及天生RDB,内存中尚未生存到快照中的数据就会随偏重启而丢失.
所以假如我们把RGB文件手动故意改坏,正常通过 service redis-server restart 重启服务器,就会在redis 服务器退出的时候重新天生 RDB 快照,把我们刚刚该坏的文件替换掉,就可以正常重启redis.
大概redis主从复制的时候,主节点=也会主动天生RDB快照,然后把RDB快照文件内容传输给从节点.(后文详解).
redis文件是二进制的,假如直接把坏了的RDB文件交给redis服务器去使用,得到的结果是不可预期的,可能redis服务器能启动,但是得到的数据有问题,也有可能redis服务器直接启动失败.

RDB工作流程小结

1.执行bgsave命令,redis父历程判断当前历程是否存在其他正在执行的子历程,如RDB/AOF子历程,假如存在besave命令直接返回.
2.父历程执行fork创建子历程,fork过程中父历程会阻塞,通过Info status命令查看 lastest_fork_userc选项,可以获取最近一次 fork 的耗时,单元为微秒.
3.父历程 fork 完成后,bgsave命令返回"Background saving started"信息并不在阻塞父历程,可以继续相应其他命令.
4.子历程创建RDB文件,根据父历程内存天生临时文件快照,完成后对资源进行原子替换.执行lastsave命令可以获取最后一次天生的 RDB 的时间,对应 info 统计的 rdb_last_saving_time选项.
5.历程发送信号给父历程表现完成,父历程更新统计信息.


2.AOF机制(Append Only Field)

AOF 实时备份机制,当开启 AOF 后,RDB文件就不见效了,启动的时候不再读取RDB文件内容了,此时,redis重新启动会,就会读取 AOF 文件,用来规复数据,AOF一般是默认关闭状态,需要修改配置文件,开启AOF功能.

AOF 是一个文本文件,每次进行操纵,都会被记录到文本文件中,通过一些特别符号作为分隔符,来对命令的细节作出区分.
Redis是一个单线程服务器,引入 AOF 机制后,需要做到实时备份,既要写内存,又要写硬盘,那样Redis的速度不会下降很多吗?
实际上,这个对Redis处理哀求的速度并没有很大的影响,原因如下:
1). AOF机制并非是直接让工作线程把数据写入硬盘,而是先写入一个内存的缓冲区,积累一波后,再统一写入硬盘,这大大低落了Redis读写硬盘的次数.假设现在有100个哀求,一次性写入硬盘,比分成100次.每次写入一个哀求快得多,写硬盘的时候,写入硬盘数据的多少,对性能影响没有很大,但是写入硬盘的次数则影响很大.
举个生活中的例子,嗑瓜子,假设垃圾桶在厨房,人在客堂嗑瓜子,一次嗑一粒瓜子,然后跑去厨房丢一次瓜子壳,这个效率就是十分慢的.但假如我们一次嗑一堆瓜子,统一扔一次瓜子壳,这个效率是不是就秒杀前面的那种.我们AOF读写硬盘的机制,就假如这个嗑瓜子的例子~~
假如把数据写入缓冲区,本质上照旧写入到内存中,假如这个时候历程忽然挂了,在缓冲区中还没来得及写入硬盘的数据就会直接丢失,这实际上也是Redis在效率和数据的可靠性中做出的一种取舍.
同时,Redis也给出了一些选项供程序员来决定缓冲区的革新策略如何取舍,革新的频率越高,性能受到的影响就越大,同时数据的可靠性就越高,反之,革新频率低,性能受到的影响就小,数据可靠性就低.

2).硬盘上读写数据,是次序读写的,次序读写的速度比力快,固然比内存照旧要慢上很多,随机访问的速度则是相对较慢的.AOF是每次把新的操纵写入到原有文件的末尾,属于次序写入.
Rewrite(重写机制)

随着AOF文件的持续增长,体积越来越大,就会影响到Redis下次启动的时长.Redis启动的时候需要读取AOF文件的内容,但是并不是AOF的全部内容,AOF文件固然记录了中间的过程,但是Redis在重新启动的时候,只关注最终的结果,而不会关注AOF文件中的冗余部分.

因此,Redis就存在一个机制,对AOF文件进行整理操纵,这个整理就是能剔除冗余操纵,而且合并一些操纵,达到给AOF文件"瘦身"如许的效果.

AOF的重写流程

创建子历程fork,父历程仍然负责哀求的接收,子历程负责针对AOF文件进行重写,重写的时候,不关心AOF文件原来有啥,只关心内存最终的数据状态.子历程只需要把内存当前的数据获取出来,然后以AOF的格式写入到一个新的AOF文件中(内存中数据的状态,就已经相称于是把AOF文件最终整理后的样子).子历程写数据的时候,非常雷同于RDB,天生一个镜像快照,只不过RDB这里是按照二进制的方式来天生的,AOF重写则是按照AOF这里要求的文本格式来天生的,但共同目的都是把当前内存中的全部数据状态记录到文件中.

子历程在写新的AOF文件的同时,父历程仍然在不停的接收客户端的新的哀求,父历程照旧会先把这些写哀求产生的AOF数据先写入到缓冲区,再更新到原有的AOF文件里.
在创建子历程的一瞬间,子历程就继承了当前父历程的内存状态,因此,子历程里的内存数据是父历程fork之前的状态,fork之后,新来的哀求,对内存造成的修改,子历程是不知道的,此时,父历程这里又预备了一个aof_rewrite_buf缓冲区,装门方fork之后的数据,子历程这边,把AOF文件写完之后,会通过信号通知一下父历程,父历程再把aof_rewrite_buf缓冲区的内容也写到新的AOF文件里.然后就可以用新的AOF文件代替旧的AOF文件.
观察上图,我们会发现,父历程在fork了子历程之后,还会继续写原来的那个,即将灭亡的aof文件,如许做是否故意义?
当时是有的!!!父历程不得不写,考虑到极端情况,假设在重写的过程中,重写了一半了,服务器挂了,子历程的数据就会丢失,新的aof文件内容不完整,所以父历程假如不对峙写旧的aof文件,重启了就没办法包管数据的完整性了.
3.RDB与AOF的关联

假如,在执行bgrewriteaof的时候,当前的Redis已经在进行AOF重写了,此时,不会再次执行AOF重写,而是直接将命令返回,这点和RDB是一样的.
假如,在执行bgrewriteaof的时候,当前的Redis在天生RDB文件的快照,会咋样?此时,AOF重写操纵就会等候,等候RDB快照天生完毕,再进行AOF重写.
RDB对于fork之后的新数据,就直接置之不理了,AOF则对于fork之厥后的新数据,接纳了 aof_rewrite_buf 缓冲区的方式来处理.
RDB本身的设计理念是用来定期备份的,既然是定期备份,就难以和新数据保持一致,AOF的理念则是实时备份,实时备份一定比定期备份好吗?不一定~这个需要看具体的业务场景!!!
4.混淆长期化

AOF文件是按照文本的方式来写入文件的,但是文本格式对于后续的加载成本是很高的!Redis就引入了混淆长期化的方式,结合了RDB和AOF的特点.
按照AOF的方式,每一个哀求和操纵,都记录入文件,在触发AOF重写之后,就会把当前内存的状态按照RDB的二进制格式更新的AOF文件中,后续再进行的操纵,还是按照AOF文本的方式追加到二进制文件后.

当Redis上同时存在AOF文件和RDB快照的时候,此时以谁为主?
以AOF为主,RDB文件会被忽略,由于AOF中的数据更加的全,更加的可靠!!!


5.本章小结

1. Redis 提供了两种长期化方案:RDB和AOF。
2. RDB视为内存的快照,产生的内容更为紧凑,占用空间较小,规复时速度更快。但产生RDB的开销较大,不适合进行实时长期化,⼀般用于冷备和主从复制。
3. AOF视为对修改命令生存,在规复时需要重放命令。而且有重写机制来定期压缩AOF⽂件。
4. RDB和AOF都使用fork创建子历程,使用Linux子历程拥有父历程内存快照的特点进行长期化, 尽可能不影响主历程继续处理后续命令
六.Redis事务

在之前,我们学过MySQL的事务,此中比力重要的四个特性是,原子性,一致性,长期性,隔离性,Redis的事务,和MySQL一比,就显得有点弟弟了.
弱原子性

关于Redis是否存在原子性,是有争议的.原子,曾经被认为是不可拆分的最小单元,原子性最本来的含义,是把多个操纵打包到一起,要么全都执行,要么全都不执行.Redis确实也做到了上述的含义.但是Redis不像MySQL一样,Redis不包管执行的操纵是成功的.
MySQL在原子性上走的更远.他也是把多个操纵打包到一起,不同的是,这些操纵,要么全都执行成功,要么全都不执行,假如事务中有操纵是执行失败的,MySQL会进行回滚,把中间已经执行的操纵全部回退.由于MySQL这个标杆的存在,提高了原子性的门槛,让我们谈到原子性的时候,更多的是想到MySQL中这种带有回滚的原子性.
不具备一致性

Redis没有约束,也没用回滚机制,事务在执行过程中假如有某个修改操纵的出现,就可能引起不一致的情况.
不具备长期性

Redis本身就是内存数据库,数据是存储在内存中的,固然Redis也有长期化机制,但是这里的长期化机制和事务没有啥直接的关系.
不涉及隔离性

Redis是一个单线程模型的服务器程序,全部的哀求/事务,都是"串行"执行的.
Redis的事务主要的意义,就是为了"打包",避免其他客户端的命令,插队插到中间
Redis事务的实现

MySQL的事务之所以这么强,是由于在背后付出了很大的代价,空间上,要耗费更多的空间来存储数据,时间上,也有更大的执行开销.正由于MySQL的这些问题,Redis才有了上场的时机.
Redis中实现事务,是引入了队列,每个客户端都有一个队列,开启事务的时候,此时客户端输入的命令,就会发送给服务器,而且进入这个队列中,当遇到"执行事务"的命令时,才会把队列中的这些命令按照次序依次执行.
当我们需要把多个操纵打包到一起执行时,就可以使用到Redis的事务.好比,在多线程中,我给我们执行下面这个逻辑可能就会出现线程安全问题,此时我们就需要通过加锁的方式来避免指令"插队".

但在Redis中,我们就可以使用事务来解决上述问题.
Redis原生命令中固然不能进行条件判断,但是Redis支持lua脚本,通过lua脚本就可以实现上述的条件判断,而且和事务一样,是打包批量执行的.lua脚本的实现方式,相称于是Redis事务的进阶版本~
但是需要留意,Redis假如是按照集群模式部署,是不支持事务的.
开启事务:MULTI
执行事务:EXEC
放弃当前事务ISCARD
当开启事务,而且,给服务器发送了多少命令后,此时服务器重启了,这个事务的效果此时就等同于discard.
当我们在一个客户端1开启事务,执行命令 set key 111 ,而后再客户端2执行命令 set key 222,之后我们在客户端1提交事务,之后我们去查询key的value时,值仍然为111,客户端2的命令是不起作用的.
WATCH

watch 监控某个key是否在执行事务之前发生了改变,刚才的这个场景中,我们就可以使用watch命令来监控这个key在事务的 muti 和 exec 之间,set key之后,是否在外部被其他客户端修改了.此时,exec在执行上述事务中的命令的时候,发现key在外部有修改,于是真正执行 set key 222 的时候就没有真正执行.

watch 的实现,雷同于一个"乐观锁".与"乐观锁"相对应的是"灰心锁",这里的"乐观"与"灰心",不是指某个具体的锁,而是指的是某一类锁的特性.简单来说,乐观锁就是,在加锁之前,有一个生理预期,接下来产生锁辩论的概率比力低,灰心锁,就是枷锁之前,也有一个生理预期,接下来产生锁辩论的概率比力高.(锁辩论指的是,两个线程针对同一个锁加锁,一个加锁成功,另一个就得阻塞等候).
这个就像小明要去问老师问题,小明是个"乐观"的人,他觉得老师一定有空帮他解决问题,于是就直接去找老师了,假如老师此时是空闲的,小明就可以问老师问题,假如老师很忙,小明也不会打扰老师,就会等老师有空先(固然没加锁,但是能识别出数据访问辩论).
假设小明是一个"灰心"的人,他认为老师很忙,所以会先扣问老师是否有空,定一个具体的时间(相称于给老师这个资源加锁),然后再去问老师问题.
乐观锁和灰心锁没有孰优孰劣之分,要看具体的使用场景,我们之前学过的Synchronized默认情况下就是一个乐观锁,当发现锁竞争比力频繁的时候下,会主动转化成灰心锁.
Redis的watch就相称于基于 版本号 如许的机制来实现了"乐观锁" ,当执行 watch key 的时候,就会给这个key安排一个版本号,版本号可以明白成一个整数,每次在修改的时候,版本号都会变大,watch必须搭配事务使用,而且在mutli之后执行.当执行事务exec命令时,此处就会做出判断,判断当前的key版本号和最初的是否一致.假如一致,证实当前的key在事务开启到执行的过程中,没有被其他客户端修改,于是才能真的被设置,假如不一致,证实 key 被别的客户端修改过了,此时就会丢弃事务中的操纵,exec返回null.
七.主从复制

分布式系统的引入,涉及到一个非常关键的问题:单点问题.
简单来说,假如某个服务器程序,只有一个节点(只搞了一个物理服务器来部署这个服务器程序),就会产生以下问题:
1.可用性问题,假如这个服务器挂了,意味着服务就中断了.
2.性能/支持的并发量也是有限的
引入分布式系统,就是为了解决上述的单点问题,在分布式系统中,往往希望有多个服务器来部署 redis 服务,从而构成一个redis集群,此时就可以让这个集群给整个分布式系统中的其他的服务提供更稳定,更高效的数据存储功能.
在分布式系统中,希望使用多个服务器来部署 redis,存在以下主要的redis的部署方式.
1.主从模式.
2.主从+哨兵模式.
3.集群模式.
下面我们来分别介绍这几种模式.
什么是主从模式

在多少个 redis 节点中,有的是"主"节点,有的是"从"节点,假设有三个物理服务器,称为是三个节点,分别部署了一个 redis-server 历程,此时就可以把此中一个的节点称为"主节点",另外两个节点作为从节点,从节点的数据要随着主节点厘革,从节点的数据要和主节点保持一致.
本来在主节点上生存的一堆数据,引入从节点之后,就是要把主节点上面的数据复制出来,放到从节点中,后续主节点中有任何数据的修改都要同步到从节点上,从节点就相称于主节点的副本.
从节点的数据和主节点是时刻保持一致的,从主节点读取数据和从从节点读取数据没有区别.假如后续客户端需要读数据,就可以从上述节点中随机挑一个给这个客户端服务.引入了更多的计算资源,自然就能支持更高的并发量.
redis中的从节点不允许修改数据,从节点只允许读操纵,不允许写操纵,主节点可以读,也可以写.主从模式,主要是针对解决读操纵,进行高并发和可用性的提高,写操纵的可用性和并发性,都是十分依赖主节点的,主节点不能搞多个,所幸,实际业务场景中,读操纵往往比写操纵更频繁.

引入主从节点前,只有单个redis服务器节点,这个节点挂了整个redis就挂了,引入了主从布局,这些节点不太可能同时挂,可用性就提高了.进一步提高可用性,还可以把这些呆板放到多个不同的机房,异地多活,防止这些呆板被一锅端.
假如某个从节点挂掉了,影响不大,可以继续从主节点和其他从节点读取数据,效果完全相同.
假如挂掉的是主节点,影响就大了.从节点只能读数据,假如需要写数据,就没法写入了.引入主从复制,可用性固然提高了,但是还没到十分理想的程度.

主从节点之间要创建tcp毗连,如许主节点这边产生任何数据的修改,从节点就能立刻感知到.当然,从节点和主节点之间的数据同步也不是瞬间完成的.
当主从节点之间断开毗连,从节点就不再属于任何一个其他节点了,但是内里有的数据是不会抛弃的.但是,当主节点数据更新后,从节点也无法同步数据了.
主从布局有很多种~但是,主从复制是从主到从,而不能从 从到从 ,从节点假如被修改,主节点是感知不到的,如许就会出现数据不一致了.

Nagle算法

对于主从复制,由于是基于tcp的,tcp内部支持了nagle算法,这个是默认开启的.
开启时,就会增加tcp的传输延迟,节省网络带宽,关闭后,就会增加tcp的传输延迟,增加网络带宽,这个的目的,和tcp的捎带应答是一样的,针对小的tcp数据包进行合并,减少了包的个数.关闭nagle算法,从节点和主节点可以更快速的同步,但是相应的要消耗更多的网络带宽.
拓补布局

拓补布局,就是多少个节点之间,按照啥样的方式来进行组织链接.如下图所示的拓补布局,假如数据写哀求太多,此时就会给主节点造成一定的压力,可以通过关闭主节点的AOF,只在从节点上开启aof,但是这种设定方式有一个严肃的缺陷,主节点一旦挂了,就不能让他重启,假如主动重启,由于没有aof文件,就会丢失数据,进一步主从同步,从节点的数据也会丢失.
所以当主节点挂了,我们可以让主节点从从节点这里获取到 AOF 文件,再启动.

实际开发当中,读哀求远远超过写哀求,如下图中的布局,主节点的数据发生改变,就需要把改变的数据同时同步给全部的从节点,随着从节点个数的增加,同步一条数据,就要传输多次.这种扁平化的布局,就需要主节点有很高的网卡带宽.


针对上述的问题,我们可以将布局继续优化,如下图所示,此时,主节点对网络带宽的要求就大大低落了,但是,一旦数据修改,同步的延时是比刚才长的.

全量同步与增量同步

redis提供了psync命令,完成数据同步的过程,psyn不需要我们手动执行,redis服务器会在创建好主从同步关系之后,主动执行psyn.
从节点负责执行psyn,从节点从主节点处拉取数据.就像生活中,我们就是一个个从节点,老师是一个主节点,我们向老师要课件,就是一种"主从同步".
Replication ID

replication id(以下简称 replid) 是由主节点天生的,主节点启动的时候就会天生,从节点晋升成主节点的时候,也会天生,即使是同一个主节点,每次重启,天生的 replid 都是不同的,从节点和主节点创建了复制关系,就会从主节点这边获取到 replication id.

一般情况下,replid2是用不上的,假设有一个主节点A,从节点B,主节点A天生replid,B获取A的replid,假如A和B之间出现了网络抖动,B认为A挂了,B就会自己成为主节点,给自己天生一个replid,此时,B也会记得之前旧的replid,通过replid2将其记录下来,后续网络稳定,B就可以根据replid2和A重新创建起主从关系,但是这个需要我们手动干预,也可以通过哨兵机制主动完成这个过程(后文详解).
Offset(偏移量)

offset,偏移量,主节点和从节点都会维护偏移量(整数).
主节点的偏移量,主节点上会受到很多的修改命令操纵,每个命令都要占据几个字节,主节点会把这些修改命令,每个命令的字节数进行累加.从节点的偏移量,就形貌了,现在从节点的数据同步到那里了.
假如主节点和从节点的偏移量一样了,证实主从节点现在的进度是一样的,主从数据完全相同.从节点每秒钟会上报自身的复制偏移量给主节点.
replication id 和 offset 共同形貌了一个"数据聚集".假如发现这两个呆板的 replication id 和 offset 完全相同,就可以认为这两个 redis 呆板上存储的数据完全一样!!!
psync可以获取从节点的全量数据,也可以获取一部分数据.主要就是根据offset的进度,offset写作-1,就是获取全量数据,offset写作具体的正整数,就是从当前偏移量的位置来获取.获取全部数据的方式是最稳妥的,但是效率十分低.假如从节点之前已经从主节点这里复制过一部分数据了,就只需要把之前没复制过的数据搞过来即可.
不过从节点索要哪些部分,主节点不一定就给哪些,主节点会自行判断,看当前是否方便给部分数据,不方便就只给全部数据.

当从节点初次和主节点进行数据同步时,大概主节点不方便进行部分复制的时候,主从复制会接纳全量复制的方式,当从节点已经从主节点上复制过数据了,由于网络抖动,从节点重启了,大概从节点需要重新从主节点这边同步数据,此时看看能不能只同步小部分数据,大部分的数据都是一致的,这种情况下就接纳部分复制.
全量复制流程

1)从节点发送psync命令给主节点进行数据同步,由于是第一次进行复制,从节点没有主节点的运行ID和复制偏移量,所以发送psync?-1。
2)主节点根据命令,解析出要进行全量复制,复兴+FULLRESYNC相应。
3)从节点接收主节点的运行信息进行生存。
4)主节点执行bgsave进行RDB文件的长期化。(RDB文件是二进制格式,节省空间,不能使用已有的RDB文件,要重新天生RDB文件,已有的RDB可能和当前的数据存在较大差异)
5)从节点发送RDB文件给从节点,从节点生存RDB数据到本地硬盘。
6)主节点将从天生RDB到接收完成期间执行的写命令,写⼊缓冲区中,等从节点生存完RDB文件 后,主节点再将缓冲区内的数据补发给从节点,补发的数据仍然按照rdb的二进制格式追加写入到收 到的rdb文件中.保持主从⼀致性。
7)从节点清空自身原有旧数据。
8)从节点加载RDB文件得到与主节点⼀致的数据。
9)假如从节点加载RDB完成之后,而且开启了AOF长期化功能,它会进行bgrewrite操纵,得到最 近的AOF文件。
在主从节点天生RDB文件和传输的时候,还会继续受到很多的修改操纵,这些新的修改数据也必须同步给从节点,当从节点收完了主节点发来的RDB之后,主节点就会把这些新的修改操纵也发送给从节点.
假如从节点开启了AOF在上述加载数据的过程中,从节点会产生很多的AOF文件,由于当前收到的数据是大批量的,可能存在冗余信息,所以需要针对AOF文件进行整理.
主节点,进行全量复制的时候,也支持无硬盘模式,主节点天生的RDB二进制数据不是直接生存到文件中了,而是直接进行网络传输.从节点之前是把收到的RDB数据写入硬盘中,然后再加载,在无硬盘模式下,这个过程也可以省略,直接把收到的数据进行加载了.
及时引入了无硬盘模式,整个操纵仍然是一个重量的,耗时的操纵,网络传输大规模的数据是一个非常大的工程.
部分复制流程

 1)当主从节点之间出现网络中断时,假如超过repl-timeout时间,主节点会认为从节点故障并终端 复制毗连。
2)主从毗连中断期间主节点依然相应命令,但这些复制命令都因网络中断无法及时发送给从节点,所 以暂时将这些命令滞留在复制积压缓冲区中。
3)当主从节点网络规复后,从节点再次连上主节点。
4)从节点将之前生存的replication Id(形貌数据的泉源)和复制偏移量作为psync的参数发送给主节点,哀求进行部分复制。
5)主节点接到psync哀求后,进行必要的验证。随后根据offset(形貌数据的复制进度)去复制积压缓冲区查找符合的数据, 并相应+CONTINUE给从节点。
6)主节点将需要从节点同步的数据发送给从节点,最终完成⼀致性。
积压缓冲区,就是内存中一个简单的队列,会记录最近一段时间修改的数据,总量有限,随着时间的推移,就会把之前的旧的数据逐渐删掉.假如offset的进度在挤压缓冲区之内,就可以执行部分复制,只要把需要的地方复制过去即可,假如超出了挤压缓冲区的范围,就要进行全量复制了.
实时复制

从节点已经和主节点同步好了数据,做到了数据一致,但是之后,主节点还会不断地收到新的数据修改哀求,主节点的数据会随之改变,此时就需要可以或许同步给从节点.主从节点之间就会创建TCP长毗连,然后主节点把自己收到的修改数据哀求,通过上述的毗连发送给从节点,从节点根据这些修改哀求,修改内存中的数据.
心跳包机制

在执行实时复制的时候,需要包管毗连处于一个可用状态.这个时候就需要引入心跳包机制.
主节点: 默认每隔 10s 给从节点发送一个 ping 命令,从节点返回 pong .
从节点: 默认每隔 1s 就给主节点发起一个特定的哀求,就会上报当前从节点复制数据修改的进度.
这些数值都是可以修改的.
当主节点挂了的时候,程序员会先看看主节点能不能抢救,好欠好抢救,假如主节点这边挂了的原因难以定位,大概短时间难以解决,就需要挑一个从节点成为新的主节点.这个过程十分的繁琐,而且操纵起来轻易堕落,于是,我们就引入了哨兵机制来解决这个问题.
八.哨兵机制

实际开发中,对于服务器后端的开发,监控程序是非常重要的,服务器要求要有比力高的可用性,服务器恒久运行总会有一些意外,具体什么时候出现意外,我们很难得知,这个时候我们可以选择写一个程序,用程序来盯着服务器的运行状态.监控程序往往还需要搭配报警程序来通知程序员.
主从复制最大的问题照旧在主节点上,假如主节点挂了,从节点固然能提供读操纵,但是从节点不能主动的升级成主节点,不能替换原有主节点的角色,此时就需要程序员手工规复主节点.这个过程十分的繁琐,为了应对这个问题,redis引入了哨兵机制,主动对挂了的主节点进行替换.
哨兵机制是通过独立的历程来表现的,和之前的redis-server是不同的历程,redis-sentinel不负责数据存储,只是针对redis-server历程起到监控的效果,通常哨兵节点也会搞一个聚集,这个聚集有多个哨兵节点组成,而且哨兵节点的个数一般为奇数.



哨兵节点的工作流程    

1)  假如主节点挂了,哨兵节点就要发挥作用了,此时一个哨兵节点发现主节点挂了(主观下线),还不够,需要多个哨兵节点来认同这件事(客观下线),主要是为了防止误判
2) 当发现主节点确实是挂了(客观下线),这些哨兵中就会推选出一个leader,由这个leader负责从现有的从节点中,挑选一个新的主节点.
3) 挑选出的新的主节点之后,哨兵节点就会主动控制这个被选中的节点,执行 slaveof no one 而且控制其他从节点,修改 slaveof 到新的主节点上.
4) 哨兵节点会主动的通知客户端程序,告知新的主节点是谁,而且后续客户端再进行写操纵,就会针对新的主节点操纵了. 
redis哨兵节点只有一个也是可以的,但是假如只有一个,他自身也是很轻易出现问题的,万一这个哨兵节点挂了,后续redis节点也挂了,就无法执行主动规复了.而且出现误判的概率也很高,当网络出现抖动大概丢包,延迟这些问题后,假如只有一个哨兵节点,就很轻易造成误判.
在分布式系统中,根本的原则是应该只管避免使用"单点".哨兵节点最好搞多个,且为奇数个,由于后续哨兵节点判断主节点是否挂了的时候是需要进行投票的,奇数个哨兵节点能有用避免平票的出现.     
主节点的挑选

当leader推举完毕,需要leader挑选一个从节点称为新的主节点,那么他是如何选择的呢?
1) 优先级,每个redis数据节点都会在配置文件中,有一个优先级设置(slave-priority),优先级高的从节点就会胜出.
2) offset,offset最大的胜出.offset越大,从节点和主节点这边同步数据的进度就越接近. 
3) run id,每个redis启动的时候会随机天生一串数字,巨细全屏缘分.此时,选谁都可以,任意挑一个.     
本章小结

• 哨兵节点不能只有⼀个.否则哨兵节点挂了也会影响系统可用性.
• 哨兵节点最好是奇数个.方便推举leader,得票更轻易超过半数.
• 哨兵节点不负责存储数据.仍然是redis主从节点负责存储.
• 哨兵+主从复制解决的问题是"提高可用性",不能解决"数据极端情况下写丢失"的问题.
• 哨兵+主从复制不能提高数据的存储容量.当我们需要存的数据接近大概超过呆板的物理内存,如许 的布局就难以胜任了. 为了能存储更多的数据,就引入了集群       


九.集群 

广义的集群,只要你是多个呆板,构成了分布式系统,都可以称为是一个"集群".前面的主从布局也可以成为是集群.
狭义的集群,redis提供了集群模式,这个集群模式之下,主要是要解决存储空间不敷的问题.
假设有 1TB 的数据,拿两天呆板来存,每个呆板只需要存512GB,拿四台,每台只需要存256GB,当然这里的两台,四台不是说光搞这么多呆板就够了,究竟每个呆板还要搭配多少个从节点存储数据,随着呆板数量增加,每天呆板存储的数据量就减少了,只要呆板的规模足够多,就可以存储任意巨细的数据了.
分片方式

把数据分成这么多份,应该怎么分呢,主流的分片方式有三种.
哈希求余

借鉴了哈希表的根本思想,借助 hash 函数,把一个 key 映射到整数,再针对数组的长度求余,就可以得到一个数组下标.好比三个分片,编号0,1,2.此时就可以针对要插入的数据的 key 计算 hash 值(好比使用md5算法),再把这个hash值分别余上分片个数,就得到一个下标,此时就可以把这个数据放到改下标对应的分片中.
hash(key)%N==0 此时这个key就放到0号盘,hash(key)%N==1,就放到1号盘,hash(key)%N==2,就放到2号盘.
但是,一旦服务器集群需要扩容,就需要更高的成本了,分片的主要目的就是为了提高存储能力,分片越多,能存的数据就越多,成本也越高,一般都实现少搞几个分片,但是随着业务的逐渐增长,数据变多了,就需要扩容了.
假如发现某个数据,在扩容之后,不应该待在当前分片,就需要搬运数据.如下图所示:

一共20个数据,只有3个数据不需要搬运,假如是20亿的数据呢?上述级别的扩容,开销极大,往往是不能直接在生产情况上操纵的,只能通过替换的方式实现扩容,依赖的呆板更多,成本更高,操纵步调非常复杂.
一致性哈希算法

在 hash 求余这种操纵之中,当前key属于哪个分片,是瓜代的,在 一致性hash 如许的设定下,把瓜代出现改进成了连续出现.当插入新的 3号分片 之后,只需要把 0号分片 上这一段搬运给3号就可以了,别的分片保持稳定.

固然搬运成本低落了,但是这几个分片上的数据量就可能不匀称了,产生了数据倾斜.
假如一次扩容搞多个分片,固然能解决数据倾斜的问题,但是呆板的成本过高了!
哈希槽分区算法

redis真正接纳的分片算法是哈希槽分区算法:
hash_slot = crc16(key) % 16384

这个算法可以很好的解决数据倾斜的问题.(16384=2^14)

会进一步的把上述这些哈希槽分配到不同的分片上,这里只是一种可能得分片方式,实际上分片非常机动,每个分片持有的槽位号可以是连续的,也可以是不连续的.
此处每个分片都会使用"位图"如许的数据布局来表现出当前有多少槽位号.
这种方法实际上就是把 hash求余 和 hash一致性算法 相结合.
假如出现了扩容要怎么处理?

此时我们就很好的解决了数据倾斜问题的出现.
假如我们分片数量很多,每个分片上只有一个槽位,就很那包管数据在各个分片上的平衡性,有的槽位有数据,有的槽位没有,由于key是先映射到槽位,再映射到分片的,假如每个分片包罗的槽位多,假如槽位个数相称,就可以认为包罗的key数量是相称的,假如分片包罗的槽位少,槽位个数不一定能直观的反应key的数量.
集群容灾

集群容灾是指在由多个节点组成的集群系统中,通过一系列技能和策略,确保在面对各种可能导致系统故障或服务中断的灾难事件时,整个集群仍能保持一定的服务能力,大概可以或许在尽可能短的时间内规复到正常运行状态,以保障关键业务的连续性和数据的完整性
数据冗余与备份规复

数据冗余,通过在多个节点或存储设备上存储相同的数据副本,确保即使部分节点或存储出现故障,数据仍然可用。常见的方式有磁盘镜像、数据条带化等。
备份规复,定期对集群中的数据进行备份,备份可以是全量备份和增量备份相结合的方式。当灾难发生导致数据丢失或损坏时,可以使用备份数据进行规复。
故障判断

1.节点A给节点B发送ping包,B就会给A返回⼀个pong包.ping和pong除了 message type 属性之外,其他部分都是⼀样的.这里包罗了集群的配置信息(该节点的id,该节点从属于哪个分片, 是主节点照旧从节点,从属于谁,持有哪些slots的位图...).
2. 每个节点,每秒钟,都会给⼀些随机的节点发起ping包,而不是全发一遍.如许设定是为了避免在节 点很多的时候,心跳包也非常多(好比有9个节点,假如全发,就是9*8有72组心跳了,而且这是按照N^2如许的级别增长的). 
3. 当节点A给节点B发起ping包,B不能准期回应的时候,此时A就会实验重置和B的tcp毗连,看能否毗连成功.假如仍然毗连失败,A就会把B设为PFAIL状态(相称于主观下线).
4. A判断B为PFAIL之后,会通过redis内置的Gossip协议,和其他节点进行沟通,向其他节点确认B 的状态.(每个节点都会维护⼀个自己的"下线列表",由于视角不同,每个节点的下线列表也不⼀定相同).
5. 此时A发现其他很多节点,也认为B为PFAIL,而且数量超过总集群个数的⼀半,那么A就会把B标记成FAIL(相称于客观下线),而且把这个消息同步给其他节点(其他节点收到之后,也会把B标记成 FAIL). 至此,B就彻底被判断为故障节点了.
故障迁移

1. 从节点判断自己是否具有参选资格.假如从节点和主节点已经太久没通信(此时认为从节点的数据和 主节点差异太大了),时间超过阈值,就失去竞选资格.
2. 具有资格的节点,好比C和D,就会先休眠⼀定时间.休眠时间=500ms基础时间+[0,500ms]随机 时间+排名*1000ms.offset的值越大,则排名越靠前(越小).
3. 好比C的休眠时间到了,C就会给其他全部集群中的节点,进行拉票操纵.但是只有主节点才有投票 资格.
4. 主节点就会把自己的票投给C(每个主节点只有1票).当C收到的票数超过主节点数量的⼀半,C就 会晋升成主节点.(C自己负责执行slaveofnoone,而且让D执行slaveofC).
5. 同时,C还会把自己成为主节点的消息,同步给其他集群的节点.大家也都会更新自己生存的集群布局信息
要留意,与redis的哨兵机制不同,哨兵是选出leader,由leader选出新的主节点.集群这里则是直接投票,根据投票结果直接选出选的主节点.

本章小结

1. 集群是什么,解决了什么问题?
2. 数据分片算法:哈希求余算法  一致性哈希算法  哈希槽分区算法
3. 集群容灾,故障判断,故障转移
4. 集群扩容
十.缓存

缓存(cache)是计算机中的⼀个经典的概念.在很多场景中都会涉及到. 核心思路就是把⼀些常用的数据放到触手可及(访问速度更快)的地方,方便随时读取.
对于计算机硬件来说,往往访问速度越快的设备,成本越高,存储空间越小. 缓存是更快,但是空间上往往是不敷的.因此大部分的时候,缓存只放⼀些热点数据(访问频繁的数据), 就非常有用了.
对于硬件的访问速度来说,通常情况下: CPU寄存器>内存>硬盘>网络.
"二八定律"

20%的热点数据,可以或许应对80%的访问场景. 因此只需要把这少量的热点数据缓存起来,就可以应对大多数场景,从而在团体上有明显的性能提升.
为什么说关系型数据库性能不高:
硬件上: 1. 数据库把数据存储在硬盘上,硬盘的IO速度并不快.尤其是随机访问. 2.假如查询不能命中索引,就需要进行表的遍历,这就会大大增加硬盘IO次数
软件上: 1. 关系型数据库对于SQL的执行会做⼀系列的解析,校验,优化工作. 2. 假如是⼀些复杂查询,好比联合查询,需要进行笛卡尔积操纵,效率更是低落很多.
而且由于MySQL等关系型数据库,效率比力低,所以承担的并发量有限,一旦哀求数量多了,数据库压力巨大,就很轻易挂掉.
为了提高MySQL能承担的并发量,我们引入了缓存,把一些频繁读取的热点数据生存到缓存上,后续在查询数据的时候,假如缓存中已经存在了,就不需要查MySQL了.



缓存的更新策略

缓存的更新策略主要分为定期天生和实时天生.
定期天生

每隔⼀定的周期,好比一天,一周,一个月等,对于访问的数据频次进行统计.挑选出访问频次最高的前N% 的数据.
以搜索引擎为例子,可以写一套离线的流程,通过 定时任务 来触发:
用户在搜索引擎中会输⼊一个"查询词",有些词是属于高频的,大家都爱搜(鲜花,蛋糕,同城结交,不孕不育...).有些词就属于低频的,大家很少搜. 搜索引擎的服务器会把哪个用户什么时间搜了啥词,都通过日志的方式记录的明显白白.然后 每隔⼀段时间对这期间的搜索结果进行统计(日志的数量可能非常巨大,这个统计的过程可能需要使用hadoop大概spark等方式完成).从而就可以得到"⾼频词表"
完成热词统计,根据热词找到搜索结果的数据,把得到缓存数据同步到缓存服务器上,控制这些缓存服务器重启.
上述过程,长处是实际上实现起来比力简单,过程更加可控,方便排查问题,缺点是,实时性不够,假如出现了一些突发性问题,有一些本来不是热词的内容,成了热词,新的热词就可能给MySQL这类数据库带来较大的压力.
实时天生

假如用户查询数据,会先到redis查,查到数据就直接返回,假如redis不存在,就去数据库查,把查到的数据写入redis,经过一段时间的"动态平衡",redis中的可以就逐渐都成了热点数据了,但是如许不断写redis,就会使得redis的内存占用越来越多,逐渐达到了内存上限(不一定是呆板的上限,redis中可以配置,最多使用多少内存),假如继续插入数据就会触发问题,于是redis就引入了内存淘汰策略.
内存淘汰策略

通用的淘汰策略

FIFO (First In First Out) 先辈先出,把缓存中存在时间最久的(也就是先来的数据)淘汰掉.
LRU(LeastRecentlyUsed)淘汰最久未使用的,记录每个key的最近访问时间.把最近访问时间最⽼的key淘汰掉.
LFU(LeastFrequently Used)淘汰访问次数最少的,记录每个key最近⼀段时间的访问次数.把访问次数最少的淘汰掉.
Random随机淘汰,从全部的key中抽取荣幸儿被随机淘汰掉.

Redis内置的淘汰策略

volatile-lru 当内存不敷以容纳新写入数据时,从设置了过期时间的key中使用LRU(最近最少使用)算法进行淘汰
 allkeys-lru 当内存不敷以容纳新写入数据时,从全部key中使用LRU(最近最少使用)算法进行淘汰.
volatile-lfu 4.0版本新增,当内存不敷以容纳新写入数据时,在过期的key中,使用LFU算法 进行删除key.
allkeys-lfu 4.0版本新增,当内存不敷以容纳新写入数据时,从全部key中使用LFU算法进行淘汰.
volatile-random 当内存不敷以容纳新写⼊数据时,从设置了过期时间的key中,随机淘汰数据.
 allkeys-random 当内存不敷以容纳新写入数据时,从全部key中随机淘汰数据.
volatile-ttl 在设置了过期时间的key中,根据过期时间进行淘汰,越早过期的优先被淘汰. (相称于FIFO,只不过是局限于过期的key)
noeviction 默认策略,当内存不敷以容纳新写入数据时,新写入操纵会报错.

缓存预热,缓存穿透,缓存雪崩,缓存击穿

缓存预热

redis服务器初次加入之后,服务器里是没有数据的,此时全部的哀求都会打给MySQL,随着时间的推移,redis上的数据越积累越多,MySQL承担的压力就逐渐减少了.缓存预热就是用来解决上述问题的,把定期天生和实时天生结合一下,先通过离线的方式,通过一些统计路径,先把热点数据找到一批,先导入MySQL,此时导入的这批热点数据就可以帮MySQL承担很大的压力,随着时间的推移,就使用新的热点数据淘汰掉旧的数据.
缓存穿透

查询某个key,在redis中没有,在MySQL中也没有,这个key肯定不会被更新到redis中,这次查询没有,下次查询仍然没有,假如像如许的数据存在很多,而且还反复查询,一样会给MySQL带来很大的压力.
缓存穿透的典型场景主要有:
1) 业务设计不公道,好比缺少必要的参数验证环节,导致非法的key也被进行查询(典型).
2) 开发运维操纵失误,不警惕把部分数据从数据库上误删(不太典型).
3) 遭到黑客恶意攻击(少见).
比力靠谱的解决方案是,假如发现这个key在redis和MySQL中都不存在,仍然写入redis,在redis中为它设置一个非法的值,好比空字符串"".
还可以引入布隆过滤器,把全部的key到放入布隆过滤器中,每次查询redis,MySQL之前都先判断一下key是否在玻璃过滤器中存在.布隆过滤器本质上是结合了hash和bitmap,以比力小的空间开销,比力快的时间速度,实现针对key是否存在的判断.
缓存雪崩

由于在短时间内,redis上大规模的key失效,导致缓存命中率陡然下降,而且MySQL的压力迅速上升,甚至直接宕机.
缓存雪崩主要的场景是:
1) redis直接挂了,redis宕机/redis集群模式下大量节点宕机.
2) redis正常运行,但是可能之前短时间内设置了很多key给redis,而且设置的过期时间是相同的,短时间内大量的key过期,给redis设置的key作为缓存的时候,有时候为了考虑缓存的时效性,就会设置过期时间,和redis内存淘汰机制是搭配使用的.
对于缓存雪崩,我们可以加强监控报警,加强redis集群可用性的包管,大概不给key设置过期时间,大概设置过期时间的时候添加随机因子,避免同一时刻大量key过期.
缓存击穿

相称于缓存雪崩的特别情况,针对热点key,忽然过期了,导致大量的哀求直接访问到数据库上了,甚至引起了数据库宕机.
针对这个问题,我们可以基于统计的发现热点key,并设置为永不过期.
进行必要的服务降级,比方访问数据库的时候使用分布式锁,限制同时哀求数据库的并发数.服务降级指的是,在特定的情况下,适当的关闭一些不重要的功能,只保留焦点功能.
本章小结

1. 缓存的根本概念.
2. 如何使用redis作为缓存.
3. 缓存的更新策略,redis内存淘汰机制.
4.缓存使用的留意事项.
5.缓存预热,缓存穿透,缓存雪崩,缓存击穿.



十一.分布式锁

在上一章的结尾,我们提到了一个重要的概念,分布式锁.在⼀个分布式的系统中,也会涉及到多个节点访问同⼀个公共资源的情况.此时就需要通过锁来做互斥控制,避免出现雷同于"线程安全"的问题.而java的synchronized大概C++的std::mutex,如许的锁都是只能在当前历程中见效,在分布式的这种多个历程多个主机的场景下就无能为力了. 此时就需要使用到分布式锁.
在分布式系统中,是有很多个历程的,每个服务器都是独立的历程.因此,之前的锁,就难以对现在分布式系统中多个历程之间产生制约.而且,在分布式历程中,多个历程之间的执行次序也是不确定的,具有随机性.
如下如所示,在分布式情况当中,买票的时候,如何解决超卖问题呢?

引入分布式锁,就可以很好的解决超卖问题!!!


买票服务器再进行买票的时候,操纵过程中,就会先加锁,具体操纵就是在redis上设置一个特定的 key-value ,完成上述的买票操纵,再把这个 key-value 删撤消,其他服务器也想要买票的时候,也去redis上实验设置 key-value,假如发现 key-value 已经存在,就认为加锁失败,放弃加锁照旧阻塞,具体看代码操纵.
于是,我们就可以包管第一个服务器执行"查询-->更新"过程中,第二个服务器不会执行"查询",也就解决了上述的"超卖问题".
所谓的分布式锁,也是一个大概一组单独的服务器程序,给其他的服务器提高加锁如许的服务,刚才的买票场景中,使用MySQL的事务,也可以批量执行 查询+修改 操纵,但是分布式系统要访问的共享资源不一定是MySQL,也可能是其他的没有事务的存储介质,也可能是执行一段特定的操纵,通过统一的服务器完成执行的操纵.
锁的过期时间

考虑这么一个情况,当某个服务器加锁成功了,往redis中设置了一个key,但是由于执行后续逻辑的过程中,程序瓦解了,没有执行到解锁,之后别的服务器去实验加锁的时候,都会由于key的存在加锁失败.
之前我们在java中学过 try-catch-finally 大概c++中的 try-catch(RALL)来达成雷同finally雷同的效果,在程序终止时将锁烧毁,但是这种做法只是针对历程内的锁,对分布式锁无效,好比服务器异常掉电,历程异常终止,如许的情况下,redis上设置的key无人删除,也就导致了后期其他服务器无法获取到锁.
考虑到这个问题,可以给set的key设置一个过期时间,一旦时间到了就把key主动删撤消.可以通过 set ex nx 如许的命令来设置完成.好比设置key的过期时间为1000ms,最极端的情况下服务器挂了,也只需要1s之后,锁就主动烧毁了.
需要留意,我们不能将这个命令分开成 set nx + expire 两条,这种设置方式是无效的,由于redis上的多个命令无法包管原子性,假如set nx成功了,设置过期时间expire失败了,就会出现大问题,相比前者,使用一条命令设置更加稳妥.
解锁的校验机制

所谓的加锁,就是给redis设置一个 key-value ,解锁就是将这个 key-value 删撤消.是否可能服务器1实现了加锁,服务器2执行了解锁如许的场景呢?
正常来说,肯定不是程序员故意的,可能是代码出现了bug,不警惕执行了解锁操纵,进一步带来了严肃的问题.为了解决上述问题,我们就需要引入一点校验机制.
1) 给服务器编号,每个服务器都有一个自己的身份标识.
2) 再进行加锁时,设置一个 key-value 对应着要针对哪个资源加锁,value就可以存储刚才服务器的标号,标识出当前这个锁是哪个服务器加上的,后续在解锁的时候,就可以进行校验了.
3) 解锁的时候,先查询一下这个锁对应的服务器编号,然后判断一下这个编号是否就是当前执行解锁的服务器编号,假如是才执行 del 解锁操纵.
这个是服务器这边需要执行的逻辑,通过校验机制,能有用地避免"误解锁".
分布式锁与事务

在解锁操纵中,先查询判断,再进行del,这两部操纵不是原子的,就可能会出现问题.如下图所示,在判断后,线程A和B都属于服务器A,value是服务器的id,于是A和B得到的结果都相同,都可以执行解锁操纵,于是del被重复执行.此时,就会把线程2中C加锁的操纵给解锁.

归根结底,都是由于 get 和 del 不是原子产生的问题,使用事务就能解决上述问题,redis的事务固然比力弱,但是照旧可以或许避免"插队"的.
过期时间续约问题("看门狗watch dog"机制)

要在加锁的过程中,给key指定过期时间,过期时间设置多少符合呢?
假如设置的短,就可能导致业务逻辑没有执行完,就解锁了.
假如设置的时间过长,就可能导致"锁开释的不及时"的问题.
更好的处理方式是,动态续约.服务器这边有一个专门的线程来负责动态续约.我们把这个线程叫做"看门狗"(watch dog).这也是一个比力广义的概念,很多场景涉及到过期时间的操纵,都会引入"看门狗".
初始情况下,设置一个过期时间,好比设置1s,就提前在还剩300ms的时候,假如当前任务还没执行完,就把当前的过期时间再设置上1s,等到时间又快到了,照旧没执行完,就在此续约,实现了无限续杯!!!
假如服务器挂了,此时没有服务器续约,自然锁就能很快的主动开释.
分布式锁挂了

使用redis作为分布式锁,我们就不得不考虑他挂了的场景,要想包管"高可用",就需要通过一系列的"预案演习".作为分布式系统,就要随时考虑某个节点挂了的情况,需要包管某个节点挂了不会影响大局!!!
redlock算法

redlock算法是redis作者给出的一个方案,焦点观念就是"冗余"!!!


此处的加锁,就是按照一定次序的加锁,针对这些组 redis 都进行加锁操纵,假如某个节点挂了,加不上锁,就继续给下个节点加锁,假如写入 key 成功的节点个数超过一半,就视为加锁成功,同理,解锁的时候,也就把上述节点都执行一边解锁.
如许的话,即时某个锁节点挂了,也不影响团体的锁的精确性!!!
简而言之,Redlock算法的焦点就是,加锁操纵不能只写给⼀个Redis节点,而要写个多个!!分布式系统 中任何⼀个节点都是不可靠的.最终的加锁成功结论是"少数服从多数的". 由于⼀个分布式系统不⾄于大部分节点都同时出现故障,因此如许的可靠性要比单个节点来说靠谱不少.
本章小结

1) 分布式锁是什么,然后实现
2) 锁的过期时间,解锁的校验机制,锁的事务,
3) 看门狗策略
4) redlock算法

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

用户国营

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表