1.消息队列的面试连环炮
前言
- 你用过消息队列么?
- 说说你们项目里是怎么用消息队列的?
- 我们有一个订单系统,订单系统会每次下一个新订单的时候,就会发送一条消息到ActiveMQ里面去,后台有一个库存系统,负责获取消息,然后更新库存。
- 为什么使用消息队列?
- 你的订单系统不发送消息到MQ,而是直接调用库存系统的一个接口,然后直接调用成功了,库存也更新了,那就不需要使用消息队列了呀
- 使用消息队列的主要作用是:异步、解耦、削峰
- 消息队列都有什么优缺点?
- Kafka、activeMQ、RibbitMQ、RocketMQ都有什么优缺点?
- 如何保证消息队列的高可用?
- 如何保证消息不被重复消费?如何保证消息消费时的幂等性?
- 如何保证消息的可靠性传输,要是消息丢失了怎么办?
- 如何保证消息的顺序性?
- 如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?
- 如果让你写一个消息队列,该如何进行架构设计,说一下你的思路?
面试官问的问题不是发散的,而是从点、铺开,比如先聊一聊高并发的话题,就这个话题里面继续聊聊缓存、MQ等等东西。对于每个小话题,比如说MQ,就会从浅入深。
为什么使用消息队列?
剖析
首先明白为什么系统中要使用到消息队列这种东西?
因为之前面试的大量候选人,说自己项目中使用了Redis,MQ,但是其实他们并不清楚为什么要用这个东西,说白了就是为了用而用,或者是别人设计的架构,从头到尾就没有思考过。
没有对自己的架构问过为什么的人,一定是平时没有思考的人,面试官对这类候选人印象通常不好。
解答
其实就是问问你消息队列有哪些场景,然后你项目里面的具体是什么场景,说说你在这个场景里用什么消息队列是什么?
消息队列的场景使用场景很多,主要是三个:解耦、异步、和削峰
解耦
不使用MQ时
A系统发送数据到B、C、D系统,但没有使用消息队列时候的耦合场景
当后面系统不断增加,比如 E,F系统的加入,以及D系统的移除
因为A系统和其它各种系统耦合起来,那么需要处理的事情会给出多
使用MQ后
系统A发送一条消息,到消息队列中,哪个系统需要获取到哪里,那么从MQ中消费数据,如果新系统E加入的话,那么只需要编写代码,然后也直接从MQ中消费即可,当系统D不需要这个数据时,那么只需要不对该消息进行消费即可。系统A不需要考虑给谁发送数据,也不需要维护这个代码,不需要考虑人家是否调用成功、失败、超时等等情况
总结:通过一个MQ,发布和订阅模型,Pub/Sub模型,系统A就和其它系统彻底解耦。
需要考虑一下负责的系统中,是否有类似的场景,就是一个系统或者一个模块,调用了多个系统,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要同步调用接口的,如果用MQ给他异步化解耦,也是可以的,这个时候可以考虑在自己的项目中,是不是可以运用这个MQ来进行系统的解耦。
异步
不用MQ的同步高延时请求场景
下面的一个场景就是系统A,调用了其它三个系统的服务,我们发现用户在执行一个请求后,需要花费很长的时间
我们发现,用户执行一个接口,就需要花费350毫秒,假设我们将每个接口的耗时增加,可能会将近花费1秒,这个时候一般用户几乎不能接受,因为一般互联网类的企业,对用户的直接操作,一般要求是每个请求都必须在200ms以内完成,因为这个是对用户是无感知的
使用MQ进行异步化
系统A只需要发送消息到MQ中就直接返回了,然后其它系统各自在MQ中进行消费。用户在执行系统A的时候,就会感觉非常快就得到响应了。
削峰
没有用MQ的削峰
一般的MySQL,抗到QPS=2000的时候就已经达到了瓶颈,如果每秒请求达到了5000的话,可能直接就把MySQL打死了。如果MySQL被打死,然后整个系统就崩溃,然后系统就没法使用。
但是中午的高峰期过了之后,到下午的时候,就成了低峰期,可能也就一万用户同时在网站上操作,每秒的请求数量可能就50个请求,对整个系统几乎没有任何压力。
使用MQ来进行削峰
削峰就是大量的请求过来,然后MQ将其消化掉了,然后通过其它系统从MQ中取消息,在逐步进行消费,保证系统的有序运行。一般高峰期不会持续太长,在一段时间后,就会被下游系统消化掉。
消息队列有什么优点和缺点?
优点上面已经说了:解耦、异步、削峰,缺点呢?显而易见的
- 系统可用性降低:系统引入的外部依赖越多,越容易挂掉,本来你就是A系统调用BCD三个系统接口就好了,人家ABCD四个系统好好的,没啥问题,这个时候却加入了MQ进来,万一MQ挂了怎么办?MQ挂了整套系统也会崩溃了。
- 系统复杂性提高:硬生生加个MQ进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?
- 一致性问题:A系统处理完了直接返回成功了,人都以为你的请求成功了,但是问题是,要在BCD三个系统中,BD两个系统写库成功了,结果C系统写库失败了,这样就会存在数据不一致的问题。
- 所以说消息队列实际上是一种复杂的架构,你引入它有好多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,最后发现系统复杂性提升了一个数量级,也许是复杂10倍,但是关键时刻,用还是得用。
有什么区别,以及适用场景?
主流MQ包括:kafka、ActiveMQ、RabbitMQ和RocketMQ
常见的MQ其实就上面的四种
特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
---|---|---|---|---|
单机吞吐量 | 万级,吞吐量比RocketMQ和Kafka要低一个数量级 | 万级,吞吐量比RocketMQ和Kafka要低一个数量级 | 10万级,RocketMQ也是可以支撑高吞吐的一种MQ | 10万级1这是kafka最大的优点,就是吞吐量高。一般配置和数据类的系统进行实时数据计算、日志采集等场景 |
时效性 | ms级 | 微妙级,这是RabbitMQ的一大特点,就是延迟最低 | ms级 | 延迟在ms级内 |
可用性 | 基于主从架构实现高可用 | 高,基于主从架构实现高可用 | 非常高,分布式架构 | 非常高,kafka是分布式的,一个数据多个副本,少数机器宕机后,不会丢失数据,不会导致不可用 |
消息可靠性 | 有较低的概率丢失数据 | 消息不丢失 | 经过参数优化配置,可以做到0丢失 | 经过参数优化配置可以做到0丢失 |
核心特点 | MQ领域的功能及其完备 | 基于Erlang开发,所以并发能力强,性能及其好,延时很低 | MQ功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用,是实时上的标准。 |
非常成熟,功能强大,在业内大量公司以及项目都有应用。 但是偶尔消息丢失的概率,并且现在社区以及国内应用都越来越少,官方社区对ActiveMQ5.X维护越来越少,而且确实主要是基于解耦和异步来用的,较少在大规模吞吐场景中使用 | erlang语言开发的,性能及其好,延时很低。而且开源的版本,就提供的管理界面非常棒,在国内一些互联网公司近几年用RabbitMQ也是比较多一些,特别适用于中小型的公司 缺点显而易见,就是吞吐量会低一些,这是因为它做的实现机制比较中,因为使用erlang开发,目前没有多少公司使用其开发。所以针对源码界别的定制,非常困难,因此公司的掌控非常弱,只能依赖于开源社区的维护。 | 接口简单易用,毕竟在阿里大规模应用过,有阿里平台保障,日处理消息上 百亿之多,可以做到大规模吞吐,性能也非常好,分布式扩展也很方便,社区维护还可以,可靠性和可用性都是OK的,还可以支撑大规模的topic数量,支持复杂MQ业务场景。 | 仅仅提供较少的核心功能,但是提供超高的吞吐量,ms级别的延迟,极高的可用性以及可靠性,分布式可以任意扩展。 同时kafka最好是支撑较少的topic数量即可,保证其超高的吞吐量。 |
综上所述:
- 一般的业务要引入MQ,最早大家都是用ACviceMQ,但是现在大家用的不多了,没有经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了,不太图鉴使用
- RabbitMQ后面被大量的中小型公司所使用,但是erlang语言阻碍了大量的Java工程师深入研究和掌握它,对公司而言,几乎处于不可控的状态,但是RabbitMQ目前开源稳定,活跃度也表较高。
- RocketMQ是阿里开源的一套消息中间件,目前也已经经历了天猫双十一,同时底层使用Java进行开发
如果中小型企业技术实力一般,技术挑战不是很高,可以推荐,RabbitMQ。如果公司的基础研发能力很强,想精确到源码级别的掌握,那么推荐使用RocketMQ。同时如果项目是聚焦于大数据领域的实时计算,日志采集等场景,那么Kafka是业内标准。
如何保证消息队列的高可用?
剖析
这个问题用的很好,不会具体到某个MQ,而是问一个整体,然后通过你使用的MQ,来具体谈谈该MQ的可用性的理解。
RabbitMQ高可用性
RabbitMQ是比较有代表性的,因为是基于主从做高可用性的。
RabbitMQ 三种模式:单机模式,普通集群模式,镜像集群模式
单机模式
就是demo级别的,一般就是本地启动后玩一玩,没有人生产环境中使用。
普通集群模式
- 意思就是在多台机器上启动多个RabbitMQ实例,每台机器启动一个,但是创建的Queue,只会放在一个RabbitMQ实例上,但是每个实例都同步queue元数据,在消费的时候,实际上是连接到另外一个实例上,那么这个实例会从queue所在实例上拉取数据过来,这种方式确实很麻烦,也不怎么好,没做到所谓的分布式 ,就是个普通集群。因为这导致你要么消费每次随机连接一个实例,然后拉取数据,要么固定连接那个queue所在实例消费数据,前者有数据拉取的开销,后者导致单实例性能瓶颈。
- 而且如果那个放queue的实例宕机了,会导致接下来其它实例无法从那个实例拉取,如果 你开启了消息持久化,让rabbitmq落地存储消息的话,消息不一定会丢,得等到这个实例恢复了,然后才可以继续从这个queue拉取数据。
这里没有什么所谓的高可用性可言,这个方案主要就是为了解决吐吞量,就是集群中的多个节点来服务于某个queue的读写操作。
存在两个缺点
- 可能会在RabbitMQ中存在大量的数据传输
- 可用性没有什么保障,如果queue所在的节点宕机,就会导致queue的消息丢失
集群镜像模式
这种模式,才是RabbitMQ的高可用模式,和普通的集群模式不一样的是,你创建的queue无论元数据还是queue里的消息都会存在与多个实例中,然后每次你写消息到queu的时候,都会自动把消息推送到多个实例的queue中进行消息同步。
这样的好处在于,你任何一个机器宕机了,别的机器都可以用。坏处在于,性能开销提升,消息同步所有的机器,导致网络带宽压力和消耗增加,第二就是没有什么扩展性科研,如果某个queue负载很重,你加机器,新增的机器也包含了这个queue的所有数据,并没有办法线性扩展你的queue
那么如何开启集群镜像策略呢?就是在RabbitMQ的管理控制台,新增一个策略,这个策略就是镜像集群模式下的策略,指定的时候,可以要求数据同步到所有的节点,也可以要求就 同步到指定数量的节点,然后再次创建queue的时候,应用这个策略,就会自动将数据同步到其它节点上去了。
集群镜像模式下,任何一个节点宕机了都是没问题的,因为其他节点还包含了这个queue的完整的数据,别的consumer可以到其它活着的节点上消费数据。
但是这个模式还存在问题:就是不是分布式的,如果这个queue的数据量很大,大到这个机器上的容量无法容纳的时候,此时应该怎么办呢?
kafka实现高可用
kafka一个最基本的架构认识:多个broker组件,每个broker是一个节点,你创建一个topic,这个topic可以划分成多个partition,每个partition可以存在于不同的broker上,每个partition就放一部分数据。
这就是天然的分布式消息队列,就是说一个topic的数据,是分散在多个机器上的,每个机器上就放一部分数据。
实际上RabbitMQ之类的,并不是分布式消息队列,他就是传统的消息队列,只不过提供了一些集群、HA的机制而已,因为无论怎么玩,RabbitMQ一个queue的数据都放在一个节点里了,镜像集群下,也是每个节点都放这个queu的完整数据。
kafka0.8以前,是没有HA机制的,就是任何一个broker宕机了,那个broker上的partition就废了,没法读也没办法写,没有什么高可用可言,而在0.8版本后,提供了HA机制,就是replica副本机制,每个partition的数据都会同步到其它机器上,形成自己的多个replica副本,然后所有的replica就是follower,写的时候,leader会负责数据都同步到所有的follower上,读的时候就直接读取leader上的数据即可。只能读写leader?很简单,要是你能随意读写每个follower,那么就需要保证数据一致性的问题,系统复杂度太高,很容易出问题,kafka会均匀的将一个partition的所有replica分布在不同的机器上,这样才能够提高容错性
每个副本不会存储节点的全部数据,而是数据可能分布在不同的机器上。
同时多个副本中,会选取一个作为leader,其它的副本是作为follower,并且只有leader能对外提供读写,同时leader在写入数据后,它还会把全部的数据同步到follower中,保证数据的备份。
此时,高可用的架构就出来了,假设现在某个机器宕机了,比如其中的一个leader宕机了,但是因为每个leader下还有多个follower,并且每个follower都进行了数据的备份,因此kafka会自动感知leader已经宕机,同时将其它的follower给选举出来,作为新的leader,并向外提供服务支持。
如果保证消息的重复消费?
面试题:如何保证消息的重复消费?如何保证消息消费的幂等性?
剖析
其实这是一个常见的问题,既然是消费消息,那肯定是要考虑会不会重复消费?能不能避免重复消费?或者重复消费了也别造成系统异常可以吗?关于消息重复消费的问题,其实本质上就是问你使用消息队列如何保证幂等性,这个是你架构中要考虑的问题。
首先是比尔RabbitMQ、RocketMQ、Kafka都会出现消息重复消费的问题,因为这个问题通常不是MQ自己保证的,而是保证消息的不丢失,我们首先从Kafka上来说:
kafka实际上有个offset的概念,就是每个消息写进去,都有一个offset,代表他的序号,然后consumer消费了数据之后,每隔一段时间,会把自己消费过的消息offset提交一下,代表我已经消费过了,下次我要是重启啥的,你就让我从上次消费到的offset来继续消费。
但是凡事总有以外,比如我们之前生产经常遇到的,就是你有时候重启系统,看你怎么重启,如果碰到着急的,直接kill杀死进程,然后重启,这就会导致consumer有些消息处理了没来得及提交offset,然后重启后,就会造成少数消息重复消费的问题。
重复消费不可怕,重要的是有没有考虑过重复消费之后,怎么保证幂等性?
例如:有个系统,消费一条数据往数据库插入一条,要是消息重复消费了两次,那么就插入两条数据了,这个数据也就出错了。
消费者如果在准备提交offset,但是还没有提交的时候,消费者进程被重启,那么此时已经消费过数据的offset并没有提交,kafka也就不知道你已经消费了,那么消费者再次上线进行消费的时候,会把已经消费的数据,重新在传递过来,这就是消息重复消费的问题。
幂等性是什么?
通俗点说:幂等性就是一个数据,或者一个请求,给你执行多次,得保证对应的数据不会改变,并且不能出错,这就是幂等性。
怎么保证消息队列消费的幂等性?
一条数据重复出现两次,但是数据库里只有一条数据,这就保证了系统的幂等性。
解决思路
- 比如那个数据要写库,首先根据主键查一下,如果这个数据已经有了,那就别插入了,执行update即可
- 如果用的是redis,那就没问题了,因为每次都是set操作,天然的幂等性
- 如果不是上面的两个场景,那就做的稍微复杂一点,需要让生产者发送每条消息的时候,需要加一个全局唯一的id,类似于订单id之后的东西,然后你这里消费到了之后,先根据这个id去redis中查找,之前消费过了么,如果没有消费过,那就进行处理,然后把这个id写入到redis中,如果消费过了,那就别处理了,保证别重复消费相同的消息即可。
- 还有比如基于数据库唯一键来保证重复数据不会重复插入多条,我们之前线上系统就有这个问题,就是拿到数据的时候,每次重启可能会重复,因为Kafka消费者还没来得及提交offset,重复数据拿到了以后,我们进行插入的时候,因为有了唯一键约束了,所以重复数据只会插入报错,不会导致数据库中出现脏数据。
如何保证消息传输不丢失?
面试题:如何保证消息的可靠性传输(如何处理消息丢失的问题)?
剖析
消息队列有三个重要原则:消息不能多,不能少
不能多,指的就是刚刚提到的重复消费和幂等性问题,不能少,指的是数据在传输过程中,不会丢失。
如果说使用MQ用来传递非常核心的消息,比如说计费,扣费的一些消息,比如设计和研发一套核心的广告平台,计费系统是一个很重的业务,操作是很耗时的,所以说广告系统整体的架构里面,实际是将计费做成异步化的,然后中间就是加了一个MQ。例如在广告主投放了一个广告,约定的是每次用户点击一次就扣费一次,结果是用户动不动就点击了一次,扣费的时候搞的消息丢了,公司就会不断的少几块钱。这样积少成多,这就是造成了公司的巨大损失。
为什么会丢数据
丢数据,一般分为两种,要么是MQ自己弄丢了,要么是我们消费的时候弄丢了。我们可以从RabbitMQ和Kafka分别来进行分析。
RabbitMQ一般来说都是承载公司的核心业务的,数据是绝对不能弄丢的。
生产者弄丢了数据
生产者将数据发送到RabbitMQ的时候,可能数据就在半路给搞丢了,因为网络啥的问题,都有可能。
此时选择用RabbitMQ提供的事务功能,就是生产者发送数据之前,开启RabbitMQ事务(channel.txSelect),然后发送消息,此时就可以回滚事务(channel.txRollback),然后重试发送消息,如果收到了消息,那么可以提交事务,但是问题是,RabbitMQ事务机制一搞,基本上吞吐量会下来,因为太损耗性能。
所以一般来说,如果你要确保写RabbitMQ消息别丢,可以开启confirm模式,在生产者那里设置了开启confirm模式之后,RabbitMQ会给你回传一个ack消息,告诉你这个消息OK了,如果RabbitMQ没能处理这个消息,会给你回调一个接口,告诉你这个消息接收失败,你可以重试
// 开启事务
try {
// 发送消息
} catch(Exception e) {
// 重试发送消息
}
// 提交
但是,因为事务机制,是同步的
针对于上述事务造成性能下降的问题,下面的方法是开启confirm模式
- 首先把channel设置成confirm模式
- 然后发送一个消息
- 发送完消息之后,就不用管了
- RabbitMQ如果接收到这个消息的话,就会回调你生产者本地的一个接口,通知你说这条消息我们已经收到了
- RabbitMQ如果在接收消息的时候出错了,就会回调这个接口
一般生产者如果要保证消息不丢失,一般是用confirm机制,因为是异步的模式,在发送消息之后,不会阻塞,直接可以发送下一条消息,这样吞吐量会更高一些。
RabbitMQ丢失数据
这个就是RabbitMQ自己丢失数据,这个时候就必须开启RabbitMQ的持久化,就是消息写入之后,同时需要持久化到磁盘中,哪怕是RabbitMQ自己宕机了,也能够从磁盘中读取之前存储的消息,这样数据一般就不会丢失了,但是存在一个极端的情况,就是RabbitMQ还没持久化的时候,就已经宕机了,那么可能会造成少量的数据丢失,但是这个概率是比较小的。
设置持久化的两个步骤,第一个是创建queue的时候,将其持久化的,这样就保证了RabbitMQ持久化queue的元数据,但是不会持久化queue中的数据,第二个就是发送消息的时候,将消息的deliveryMode设置为2,就是将消息设置为持久化的,此时RabbitMQ将会将消息持久化到磁盘上,必须同时设置两个持久化才行,哪怕是Rabbit挂了,也会从磁盘中恢复queue 和 queue中的数据。
而且持久化可以跟生产者那边的confirm机制配置起来,只有消息被持久化到磁盘后,才会通知生产者ACK了,所以哪怕是在持久化磁盘之前,RabbitMQ挂了,数据丢了,生产者收不到ACK,你也是可以自己重发的。
消费者丢失数据
消费者丢失数据,主要是因为打开了AutoAck的机制,消费者会自动通知RabbitMQ,表明自己已经消费完这条数据了,但是如果你消费到了一条消息,还在处理中,还没处理完,此时消费者就会自动AutoAck了,通知RabbitMQ说这条消息已经被消费了,此时不巧的是,消费者系统宕机了,这条消息就会丢失,因为RabbitMQ以为这条消息已经处理掉。
在消费者层面上,我们需要将AutoAck给关闭,然后每次自己确定已经处理完了一条消息后,你再发送ack给RabbitMQ,如果你还没处理完就宕机了,此时RabbitMQ没收到你发的Ack消息,然后RabbitMQ就会将这条消息分配给其它的消费者去处理。
如何保证消息的顺序性?
场景
以前做过一个MySQL binlog同步系统,压力还是非常大的,日同步数据要达到上亿。常见一点的在于 大数据项目中,就需要同步一个mysql库过来,然后对公司业务的系统做各种的复杂操作。
在mysql里增删改一条数据,对应出来的增删改3条binlog,接着这三条binlog发送到MQ里面,到消费出来依次执行,这个时候起码得保证能够顺序执行,不然本来是:增加、修改、删除,然后被换成了:删除、修改、增加,不全错了呢。
本来这个数据同步过来,应该是最后删除的,结果因为顺序搞错了,最后这个数据被保留了下来,数据同步就出错
- RabbitMQ:一个queue,多个consumer,这不明显乱了
- Kafka:一个topic,一个partition,一个consumer,内部多线程,就会乱套
在消息队列中,一个queue中的数据,一次只会被一个消费者消费掉
但因为不同消费者的执行速度不一致,在存入数据库后,造成顺序不一致的问题
RabbitMQ保证消息顺序性
RabbitMQ:拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦,或者就是一个queue,但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理。
下图为:一个consumer 对应 一个 queue,这样就保证了消息消费的顺序性。
Kafka保证消息消息顺序性
一个topic,一个partition,一个consumer,内部单线程消费,写N个内存,然后N个线程分别消费一个内存queu即可。注意,kafka中,写入一个partition中的数据,一定是有顺序的,
但是在一个消费者的内部,假设有多个线程并发的进行数据的消费,那么这个消息又会乱掉
这样时候,我们需要引入内存队列,然后我们通过消息的key,然后我们通过hash算法,进行hash分发,将相同订单key的散列到我们的同一个内存队列中,然后每一个线程从这个Queue中拉数据,同一个内存Queue也是有顺序的。
百万消息积压在队列中如何处理?
如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有百万消息积压接小时,说说解决思路?
剖析
MQ大幅度积压这件事挺常见的,一般不出,出了的话就是大型生产事故,例如:消费端每次消费之后要写MySQL,结果MySQL挂了,消费端就不动了,或者一直出错,导致消息消费速度极其慢。
场景1:积压大量消息
几千万的消息积压在MQ中七八个小时,这也是一个真实遇到过的一个场景,确实是线上故障了,这个时候要不然就是修复consumer,让他恢复消费速度,然后傻傻的等待几个小时消费完毕,但是很显然这是一种比较不机智的做法。
假设1个消费者1秒消费1000条,1秒3个消费者能消费3000条,一分钟就是18万条,1000万条也需要花费1小时才能够把消息处理掉,这个时候在设备允许的情况下,如何才能够快速处理积压的消息呢?
一般这个时候,只能够做紧急的扩容操作了,具体操作步骤和思路如下所示:
- 先修复consumer的问题,确保其恢复消费速度,然后将现有consumer都停止
- 临时建立好原先10倍或者20倍的queue数量
- 然后写一个临时的分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的10倍数量的queue
- 接着临时征用10倍机器来部署consumer,每一批consumer消费一个临时queue的数据
- 这种做法相当于临时将queue资源和consumer资源扩大了10倍,以正常的10倍速度
也就是让消费者把消息,重新写入MQ中,然后在用 10倍的消费者来进行消费。
场景2:大量消息积压,并且设置了过期时间
假设你用的是RabbitMQ,RabbitMQ是可以设置过期时间的,就是TTL,如果消息在queue中积压超过一定的时间,就会被RabbitMQ给清理掉,这个数据就没了。这个时候就不是数据被大量积压的问题,而是大量的数据被直接搞丢了。
这种情况下,就不是说要增加consumer消费积压的消息,因为实际上没有啥积压的,而是丢了大量的消息,我们可以采取的一个方案就是,批量重导,这个之前线上也有遇到类似的场景,就是大量的消息积压的时候,然后就直接丢弃了数据,然后等高峰期过了之后,例如在晚上12点以后,就开始写程序,将丢失的那批数据,写个临时程序,一点点查询出来,然后重新 添加MQ里面,把白天丢的数据,全部补回来。
假设1万个订单积压在MQ里面,没有处理,其中1000个订单都丢了,你只能手动写程序把那1000个订单查询出来,然后手动发到MQ里面去再补一次。
场景3:大量消息积压,导致MQ磁盘满了
如果走的方式是消息积压在MQ里,那么如果你很长时间都没有处理掉,此时导致MQ都快写满了,咋办?
这个时候,也是因为方案一执行的太慢了,只能写一个临时程序,接入数据来消费,然后消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到凌晨的时候,在把消息填入MQ中进行消费。
如何设计一个消息中间件架构?
如果让你写一个消息队列,该如何进行架构设计?说下你的思路
这种问题,说白了,起码不求你看过那些技术的源码,但是你应该大概知道那些技术的基本原理,核心组成部分,基本架构个构成,然后参照一些开源技术把一个系统设计出来的思路说一下就好了。
思路
- 首先MQ得支持可伸缩性,那就需要快速扩容,就可以增加吞吐量和容量,可以设计一个分布式的系统,参考kafka的设计理念,broker - > topic -> partition,每个partition放一台机器,那就存一部分数据,如果现在资源不够了,可以给topic增加partition,然后做数据迁移,增加机器,不就可以存放更多的数据,提高更高的吞吐量。
- 其次得考虑一下这个MQ的数据要不要落地磁盘?也就是需不需要保证消息持久化,因为这样可以保证数据的不丢失,那落地盘的时候怎么落?顺序写,这样没有磁盘随机读写的寻址开销,磁盘顺序读的性能是很高的,这就是kafka的思路。
- 其次需要考虑MQ的可用性?这个可以具体到我们上面提到的消息队列保证高可用,提出了多副本 ,leader 和follower模式,当一个leader宕机的时候,马上选取一个follower作为新的leader对外提供服务。
- 需不需要支持数据0丢失?可以参考kafka零丢失方案
其实一个MQ肯定是很复杂的,问这个问题其实是一个开放性问题,主要是想看看有没有从架构的角度整体构思和设计的思维以及能力
消息队列相关问题总结
一般而言,如果一个面试官水平还不错,会沿着从浅入深挖一个点,然后按着这个思路一直问下去,除了这里的七大问题之后,甚至还能挑着你熟悉的一个MQ一直问到源码级别,还可能结合项目来仔细问,先讲讲具体的业务细节,然后将业务跟这些MQ的问题场景结合起来,看看你的每个细节是如何处理和实现的。
2.分布式搜索引擎的面试连环炮
业内目前来说事实上的一个标准,就是分布式搜索引擎一般大家都是用ElasticSearch,(原来的话使用的是Solr),但是确实,这两年大家一般都用更加易用的es。
ElasticSearch 和 Solr 底层都是基于Lucene,而Lucene的底层原理是 倒排索引
倒排索引是什么
倒排索引适用于快速的全文检索,一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表
例如:
假设文档集合中包含五个文档,每个文档的内容如下所示,在图中最左端一栏是每个文档对应的编号,我们的任务就是对这个文档集合建立倒排索引
中文和英文等语言不通,单词之间没有明确分割符号,所以首先要用分词系统将文档自动切分成单词序列,这样每个文档就转换为由单词序列构成的数据流,为了系统后续处理方便,需要对每个不同的单词赋予唯一的单词编号,同时记录下哪些文档包含这个单词,在如此处理结束后,我们就可以得到最简单的倒排索引了
索引系统还可以记录除此之外的更多信息,下图是记录了单词出现的频率(TF)即这个单词在文档中出现的次数,之所以要记录这个信息,是因为词频信息在搜索结果排序时,计算查询和文档相似度是很重要的一个计算因子,所以将其记录在倒排列表中,以便后续排序时进行分值计算。
倒排列表还可以记录单词在某个文档出现的位置信息
(1, <11>, 1), (2, <7>, 1), (3, <3, 9>, 2)
有了这个索引系统,搜索引擎可以很方便地响应用户的查询,比如用户输入查询词 “Facebook”,搜索系统查找倒排索引,从中可以读出包含这个单词的文档,这些文档就是提供给用户的搜索结果,而利用单词频率信息,文档频率信息即可以对这些候选搜索结果进行排序,计算文档和查询的相似性,按照相似性得分由高到低排序输出,此即为搜索系统的部分内部流程。
中文分词器原理
方法1
分词器的原理本质上是词典分词。在现有内存中初始化一个词典,然后在分词过程中挨个读取字符和字典中的字符相匹配,把文档中所有词语拆分出来的过程。
方法2 字典树
Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
下面一个存放了[大学、大学生、学习、学习机、学生、生气、生活、活着]这个词典的trie树:
它可以看作是用每个词第n个字做第n到第n+1层节点间路径哈希值的哈希树,每个节点是实际要存放的词。
现在用这个树来进行“大学生活”的匹配。依然从“大”字开始匹配,如下图所示:从根节点开始,沿最左边的路径匹配到了大字,沿着“大”节点可以匹配到“大学”,继续匹配则可以匹配到“大学生”,之后字典中再没有以“大”字开头的词,至此已经匹配到了[大学、大学生]第一轮匹配结束
继续匹配“学”字开头的词,方法同上步,可匹配出[学生]
继续匹配“生”和“活”字开头的词,这样“大学生活”在词典中的词全部被查出来。
可以看到,以匹配“大”字开头的词为例,第一种匹配方式需要在词典中查询是否包含“大”、“大学”、“大学”、“大学生活”,共4次查询,而使用trie树查询时当找到“大学生”这个词之后就停止了该轮匹配,减少了匹配的次数,当要匹配的句子越长,这种性能优势就越明显。
失败指针
再来看一下上面的匹配过程,在匹配“大学生”这个词之后,由于词典中不存在其它以“大”字开头的词,本轮结束,将继续匹配以“学”字开头的词,这时,需要再回到根节点继续匹配,如果这个时候“大学生”节点有个指针可以直指向“学生”节点,就可以减少一次查询,类似地,当匹配完“学生”之后如果“学生”节点有个指针可以指向“生活”节点,就又可以减少一次查询。这种当下一层节点无法匹配需要进行跳转的指针就是失败指针,创建好失败指针的树看起来如下图:
图上红色的线就是失败指针,指向的是当下层节点无法匹配时应该跳转到哪个节点继续进行匹配
失败指针的创建过程通常为:
- 创建好trie树。
- BFS每一个节点(不能使用DFS,因为每一层节点的失败指针在创建时要确保上一层节点的失败指针全部创建完成)。
- 根节点的子节点的失败指针指向根节点。
- 其它节点查找其父节点的失败指针指向的节点的子节点是否有和该节点字相同的节点,如果有则失败指针指向该节点,如果没有则重复刚才的过程直至找到字相同的节点或根节点。
查询过程如下:
ES的分布式架构原理能说一下么?
elasticsearch设计的理念就是分布式搜索引擎,底层其实还是基于lucene的。
核心思想就是在多台机器上启动多个es进程实例,组成了一个es集群。
es中存储数据的基本单位是索引,比如说你现在要在es中存储一些订单数据,你就应该在es中创建一个索引,order_idx,所有的订单数据就都写到这个索引里面去,一个索引差不多就是相当于是mysql里的一张表。index -> type -> mapping -> document -> field。
index:mysql里的一张表
type:没法跟mysql里去对比,一个index里可以有多个type,每个type的字段都是差不多的,但是有一些略微的差别。
好比说,有一个index,是订单index,里面专门是放订单数据的。就好比说你在mysql中建表,有些订单是实物商品的订单,就好比说一件衣服,一双鞋子;有些订单是虚拟商品的订单,就好比说游戏点卡,话费充值。就两种订单大部分字段是一样的,但是少部分字段可能有略微的一些差别。
所以就会在订单index里,建两个type,一个是实物商品订单type,一个是虚拟商品订单type,这两个type大部分字段是一样的,少部分字段是不一样的。
很多情况下,一个index里可能就一个type,但是确实如果说是一个index里有多个type的情况,你可以认为index是一个类别的表,具体的每个type代表了具体的一个mysql中的表
每个type有一个mapping,如果你认为一个type是一个具体的一个表,index代表了多个type的同属于的一个类型,mapping就是这个type的表结构定义,你在mysql中创建一个表,肯定是要定义表结构的,里面有哪些字段,每个字段是什么类型。。。
mapping就代表了这个type的表结构的定义,定义了这个type中每个字段名称,字段是什么类型的,然后还有这个字段的各种配置
实际上你往index里的一个type里面写的一条数据,叫做一条document,一条document就代表了mysql中某个表里的一行给,每个document有多个field,每个field就代表了这个document中的一个字段的值
接着你搞一个索引,这个索引可以拆分成多个shard,每个shard存储部分数据。
接着就是这个shard的数据实际是有多个备份,就是说每个shard都有一个primary shard,负责写入数据,但是还有几个replica shard。primary shard写入数据之后,会将数据同步到其他几个replica shard上去。
通过这个replica的方案,每个shard的数据都有多个备份,如果某个机器宕机了,没关系啊,还有别的数据副本在别的机器上呢。高可用了吧。
es集群多个节点,会自动选举一个节点为master节点,这个master节点其实就是干一些管理的工作的,比如维护索引元数据拉,负责切换primary shard和replica shard身份拉,之类的。
要是master节点宕机了,那么会重新选举一个节点为master节点。
如果是非master节点宕机了,那么会由master节点,让那个宕机节点上的primary shard的身份转移到其他机器上的replica shard。急着你要是修复了那个宕机机器,重启了之后,master节点会控制将缺失的replica shard分配过去,同步后续修改的数据之类的,让集群恢复正常。
其实上述就是elasticsearch作为一个分布式搜索引擎最基本的一个架构设计
ES查询和读取数据的工作原理是什么?
(1)es写数据过程
1)客户端选择一个node发送请求过去,这个node就是coordinating node(协调节点)
2)coordinating node,对document进行路由,将请求转发给对应的node(有primary shard)
3)实际的node上的primary shard处理请求,然后将数据同步到replica node
4)coordinating node,如果发现primary node和所有replica node都搞定之后,就返回响应结果给客户端
(2)es读数据过程
查询,GET某一条数据,写入了某个document,这个document会自动给你分配一个全局唯一的id,doc id,同时也是根据doc id进行hash路由到对应的primary shard上面去。也可以手动指定doc id,比如用订单id,用户id。
你可以通过doc id来查询,会根据doc id进行hash,判断出来当时把doc id分配到了哪个shard上面去,从那个shard去查询
1)客户端发送请求到任意一个node,成为coordinate node
2)coordinate node对document进行路由,将请求转发到对应的node,此时会使用round-robin随机轮询算法,在primary shard以及其所有replica中随机选择一个,让读请求负载均衡
3)接收请求的node返回document给coordinate node
4)coordinate node返回document给客户端
(3)es搜索数据过程
es最强大的是做全文检索,就是比如你有三条数据
java真好玩儿啊
java好难学啊
j2ee特别牛
你根据java关键词来搜索,将包含java的document给搜索出来
es就会给你返回:java真好玩儿啊,java好难学啊
1)客户端发送请求到一个coordinate node
2)协调节点将搜索请求转发到所有的shard对应的primary shard或replica shard也可以
3)query phase:每个shard将自己的搜索结果(其实就是一些doc id),返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果
4)fetch phase:接着由协调节点,根据doc id去各个节点上拉取实际的document数据,最终返回给客户端
(4)搜索的底层原理,倒排索引,画图说明传统数据库和倒排索引的区别
(5)写数据底层原理
1)先写入buffer,在buffer里的时候数据是搜索不到的;同时将数据写入translog日志文件
2)如果buffer快满了,或者到一定时间,就会将buffer数据refresh到一个新的segment file中,但是此时数据不是直接进入segment file的磁盘文件的,而是先进入os cache的。这个过程就是refresh。
每隔1秒钟,es将buffer中的数据写入一个新的segment file,每秒钟会产生一个新的磁盘文件,segment file,这个segment file中就存储最近1秒内buffer中写入的数据
但是如果buffer里面此时没有数据,那当然不会执行refresh操作咯,每秒创建换一个空的segment file,如果buffer里面有数据,默认1秒钟执行一次refresh操作,刷入一个新的segment file中
操作系统里面,磁盘文件其实都有一个东西,叫做os cache,操作系统缓存,就是说数据写入磁盘文件之前,会先进入os cache,先进入操作系统级别的一个内存缓存中去
只要buffer中的数据被refresh操作,刷入os cache中,就代表这个数据就可以被搜索到了
为什么叫es是准实时的?NRT,near real-time,准实时。默认是每隔1秒refresh一次的,所以es是准实时的,因为写入的数据1秒之后才能被看到。
可以通过es的restful api或者java api,手动执行一次refresh操作,就是手动将buffer中的数据刷入os cache中,让数据立马就可以被搜索到。
只要数据被输入os cache中,buffer就会被清空了,因为不需要保留buffer了,数据在translog里面已经持久化到磁盘去一份
3)只要数据进入os cache,此时就可以让这个segment file的数据对外提供搜索了
4)重复1~3步骤,新的数据不断进入buffer和translog,不断将buffer数据写入一个又一个新的segment file中去,每次refresh完buffer清空,translog保留。随着这个过程推进,translog会变得越来越大。当translog达到一定长度的时候,就会触发commit操作。
buffer中的数据,倒是好,每隔1秒就被刷到os cache中去,然后这个buffer就被清空了。所以说这个buffer的数据始终是可以保持住不会填满es进程的内存的。
每次一条数据写入buffer,同时会写入一条日志到translog日志文件中去,所以这个translog日志文件是不断变大的,当translog日志文件大到一定程度的时候,就会执行commit操作。
5)commit操作发生第一步,就是将buffer中现有数据refresh到os cache中去,清空buffer
6)将一个commit point写入磁盘文件,里面标识着这个commit point对应的所有segment file
7)强行将os cache中目前所有的数据都fsync到磁盘文件中去
translog日志文件的作用是什么?就是在你执行commit操作之前,数据要么是停留在buffer中,要么是停留在os cache中,无论是buffer还是os cache都是内存,一旦这台机器死了,内存中的数据就全丢了。
所以需要将数据对应的操作写入一个专门的日志文件,translog日志文件中,一旦此时机器宕机,再次重启的时候,es会自动读取translog日志文件中的数据,恢复到内存buffer和os cache中去。
commit操作:1、写commit point;2、将os cache数据fsync强刷到磁盘上去;3、清空translog日志文件
8)将现有的translog清空,然后再次重启启用一个translog,此时commit操作完成。默认每隔30分钟会自动执行一次commit,但是如果translog过大,也会触发commit。整个commit的过程,叫做flush操作。我们可以手动执行flush操作就是将所有os cache数据刷到磁盘文件中去。
不叫做commit操作,flush操作。es中的flush操作,就对应着commit的全过程。我们也可以通过es api,手动执行flush操作,手动将os cache中的数据fsync强刷到磁盘上去,记录一个commit point,清空translog日志文件。
9)translog其实也是先写入os cache的,默认每隔5秒刷一次到磁盘中去,所以默认情况下,可能有5秒的数据会仅仅停留在buffer或者translog文件的os cache中,如果此时机器挂了,会丢失5秒钟的数据。但是这样性能比较好,最多丢5秒的数据。也可以将translog设置成每次写操作必须是直接fsync到磁盘,但是性能会差很多。
实际上你在这里,如果面试官没有问你es丢数据的问题,你可以在这里给面试官炫一把,你说,其实es第一是准实时的,数据写入1秒后可以搜索到;可能会丢失数据的,你的数据有5秒的数据,停留在buffer、translog os cache、segment file os cache中,有5秒的数据不在磁盘上,此时如果宕机,会导致5秒的数据丢失。
如果你希望一定不能丢失数据的话,你可以设置个参数,官方文档,百度一下。每次写入一条数据,都是写入buffer,同时写入磁盘上的translog,但是这会导致写性能、写入吞吐量会下降一个数量级。本来一秒钟可以写2000条,现在你一秒钟只能写200条,都有可能。
10)如果是删除操作,commit的时候会生成一个.del文件,里面将某个doc标识为deleted状态,那么搜索的时候根据.del文件就知道这个doc被删除了
11)如果是更新操作,就是将原来的doc标识为deleted状态,然后新写入一条数据
12)buffer每次refresh一次,就会产生一个segment file,所以默认情况下是1秒钟一个segment file,segment file会越来越多,此时会定期执行merge
13)每次merge的时候,会将多个segment file合并成一个,同时这里会将标识为deleted的doc给物理删除掉,然后将新的segment file写入磁盘,这里会写一个commit point,标识所有新的segment file,然后打开segment file供搜索使用,同时删除旧的segment file。
es里的写流程,有4个底层的核心概念,refresh、flush、translog、merge
当segment file多到一定程度的时候,es就会自动触发merge操作,将多个segment file给merge成一个segment file。
ES在数据量很大的情况下(数十亿级别)如何提高查询性能?
说实话,es性能优化是没有什么银弹的,啥意思呢?就是不要期待着随手调一个参数,就可以万能的应对所有的性能慢的场景。也许有的场景是你换个参数,或者调整一下语法,就可以搞定,但是绝对不是所有场景都可以这样。
一块一块来分析吧
在这个海量数据的场景下,如何提升es搜索的性能,也是我们之前生产环境实践经验所得
(1)性能优化的杀手锏——filesystem cache
os cache,操作系统的缓存
你往es里写的数据,实际上都写到磁盘文件里去了,磁盘文件里的数据操作系统会自动将里面的数据缓存到os cache里面去
es的搜索引擎严重依赖于底层的filesystem cache,你如果给filesystem cache更多的内存,尽量让内存可以容纳所有的indx segment file索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。
性能差距可以有大,我们之前很多的测试和压测,如果走磁盘一般肯定上秒,搜索性能绝对是秒级别的,1秒,5秒,10秒。但是如果是走filesystem cache,是走纯内存的,那么一般来说性能比走磁盘要高一个数量级,基本上就是毫秒级的,从几毫秒到几百毫秒不等。
之前有个学员,一直在问我,说他的搜索性能,聚合性能,倒排索引,正排索引,磁盘文件,十几秒。。。。
学员的真实案例
比如说,你,es节点有3台机器,每台机器,看起来内存很多,64G,总内存,64 * 3 = 192g
每台机器给es jvm heap是32G,那么剩下来留给filesystem cache的就是每台机器才32g,总共集群里给filesystem cache的就是32 * 3 = 96g内存
我就问他,ok,那么就是你往es集群里写入的数据有多少数据量?
如果你此时,你整个,磁盘上索引数据文件,在3台机器上,一共占用了1T的磁盘容量,你的es数据量是1t,每台机器的数据量是300g
你觉得你的性能能好吗?filesystem cache的内存才100g,十分之一的数据可以放内存,其他的都在磁盘,然后你执行搜索操作,大部分操作都是走磁盘,性能肯定差
当时他们的情况就是这样子,es在测试,弄了3台机器,自己觉得还不错,64G内存的物理机。自以为可以容纳1T的数据量。
归根结底,你要让es性能要好,最佳的情况下,就是你的机器的内存,至少可以容纳你的总数据量的一半
比如说,你一共要在es中存储1T的数据,那么你的多台机器留个filesystem cache的内存加起来综合,至少要到512G,至少半数的情况下,搜索是走内存的,性能一般可以到几秒钟,2秒,3秒,5秒
如果最佳的情况下,我们自己的生产环境实践经验,所以说我们当时的策略,是仅仅在es中就存少量的数据,就是你要用来搜索的那些索引,内存留给filesystem cache的,就100G,那么你就控制在100gb以内,相当于是,你的数据几乎全部走内存来搜索,性能非常之高,一般可以在1秒以内
比如说你现在有一行数据
id name age …30个字段
但是你现在搜索,只需要根据id name age三个字段来搜索
如果你傻乎乎的往es里写入一行数据所有的字段,就会导致说70%的数据是不用来搜索的,结果硬是占据了es机器上的filesystem cache的空间,单挑数据的数据量越大,就会导致filesystem cahce能缓存的数据就越少
仅仅只是写入es中要用来检索的少数几个字段就可以了,比如说,就写入es id name age三个字段就可以了,然后你可以把其他的字段数据存在mysql里面,我们一般是建议用es + hbase的这么一个架构。
hbase的特点是适用于海量数据的在线存储,就是对hbase可以写入海量数据,不要做复杂的搜索,就是做很简单的一些根据id或者范围进行查询的这么一个操作就可以了
从es中根据name和age去搜索,拿到的结果可能就20个doc id,然后根据doc id到hbase里去查询每个doc id对应的完整的数据,给查出来,再返回给前端。
你最好是写入es的数据小于等于,或者是略微大于es的filesystem cache的内存容量
然后你从es检索可能就花费20ms,然后再根据es返回的id去hbase里查询,查20条数据,可能也就耗费个30ms,可能你原来那么玩儿,1T数据都放es,会每次查询都是5~10秒,现在可能性能就会很高,每次查询就是50ms。
elastcisearch减少数据量仅仅放要用于搜索的几个关键字段即可,尽量写入es的数据量跟es机器的filesystem cache是差不多的就可以了;其他不用来检索的数据放hbase里,或者mysql。
所以之前有些学员也是问,我也是跟他们说,尽量在es里,就存储必须用来搜索的数据,比如说你现在有一份数据,有100个字段,其实用来搜索的只有10个字段,建议是将10个字段的数据,存入es,剩下90个字段的数据,可以放mysql,hadoop hbase,都可以
这样的话,es数据量很少,10个字段的数据,都可以放内存,就用来搜索,搜索出来一些id,通过id去mysql,hbase里面去查询明细的数据
(2)数据预热
假如说,哪怕是你就按照上述的方案去做了,es集群中每个机器写入的数据量还是超过了filesystem cache一倍,比如说你写入一台机器60g数据,结果filesystem cache就30g,还是有30g数据留在了磁盘上。
举个例子,就比如说,微博,你可以把一些大v,平时看的人很多的数据给提前你自己后台搞个系统,每隔一会儿,你自己的后台系统去搜索一下热数据,刷到filesystem cache里去,后面用户实际上来看这个热数据的时候,他们就是直接从内存里搜索了,很快。
电商,你可以将平时查看最多的一些商品,比如说iphone 8,热数据提前后台搞个程序,每隔1分钟自己主动访问一次,刷到filesystem cache里去。
对于那些你觉得比较热的,经常会有人访问的数据,最好做一个专门的缓存预热子系统,就是对热数据,每隔一段时间,你就提前访问一下,让数据进入filesystem cache里面去。这样期待下次别人访问的时候,一定性能会好一些。
(3)冷热分离
关于es性能优化,数据拆分,我之前说将大量不搜索的字段,拆分到别的存储中去,这个就是类似于后面我最后要讲的mysql分库分表的垂直拆分。
es可以做类似于mysql的水平拆分,就是说将大量的访问很少,频率很低的数据,单独写一个索引,然后将访问很频繁的热数据单独写一个索引
你最好是将冷数据写入一个索引中,然后热数据写入另外一个索引中,这样可以确保热数据在被预热之后,尽量都让他们留在filesystem os cache里,别让冷数据给冲刷掉。
你看,假设你有6台机器,2个索引,一个放冷数据,一个放热数据,每个索引3个shard
3台机器放热数据index;另外3台机器放冷数据index
然后这样的话,你大量的时候是在访问热数据index,热数据可能就占总数据量的10%,此时数据量很少,几乎全都保留在filesystem cache里面了,就可以确保热数据的访问性能是很高的。
但是对于冷数据而言,是在别的index里的,跟热数据index都不再相同的机器上,大家互相之间都没什么联系了。如果有人访问冷数据,可能大量数据是在磁盘上的,此时性能差点,就10%的人去访问冷数据;90%的人在访问热数据。
(4)document模型设计
有不少同学问我,mysql,有两张表
订单表:id order_code total_price
1 测试订单 5000
订单条目表:id order_id goods_id purchase_count price
1 1 1 2 2000
2 1 2 5 200
我在mysql里,都是select * from order join order_item on order.id=order_item.order_id where order.id=1
1 测试订单 5000 1 1 1 2 2000
1 测试订单 5000 2 1 2 5 200
在es里该怎么玩儿,es里面的复杂的关联查询,复杂的查询语法,尽量别用,一旦用了性能一般都不太好
设计es里的数据模型
写入es的时候,搞成两个索引,order索引,orderItem索引
order索引,里面就包含id order_code total_price
orderItem索引,里面写入进去的时候,就完成join操作,id order_code total_price id order_id goods_id purchase_count price
写入es的java系统里,就完成关联,将关联好的数据直接写入es中,搜索的时候,就不需要利用es的搜索语法去完成join来搜索了
document模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。es能支持的操作就是那么多,不要考虑用es做一些它不好操作的事情。如果真的有那种操作,尽量在document模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如join,nested,parent-child搜索都要尽量避免,性能都很差的。
很多同学在问我,很多复杂的乱七八糟的一些操作,如何执行
两个思路,在搜索/查询的时候,要执行一些业务强相关的特别复杂的操作:
1)在写入数据的时候,就设计好模型,加几个字段,把处理好的数据写入加的字段里面
2)自己用java程序封装,es能做的,用es来做,搜索出来的数据,在java程序里面去做,比如说我们,基于es,用java封装一些特别复杂的操作
(5)分页性能优化
es的分页是较坑的,为啥呢?举个例子吧,假如你每页是10条数据,你现在要查询第100页,实际上是会把每个shard上存储的前1000条数据都查到一个协调节点上,如果你有个5个shard,那么就有5000条数据,接着协调节点对这5000条数据进行一些合并、处理,再获取到最终第100页的10条数据。
分布式的,你要查第100页的10条数据,你是不可能说从5个shard,每个shard就查2条数据?最后到协调节点合并成10条数据?你必须得从每个shard都查1000条数据过来,然后根据你的需求进行排序、筛选等等操作,最后再次分页,拿到里面第100页的数据。
你翻页的时候,翻的越深,每个shard返回的数据就越多,而且协调节点处理的时间越长。非常坑爹。所以用es做分页的时候,你会发现越翻到后面,就越是慢。
我们之前也是遇到过这个问题,用es作分页,前几页就几十毫秒,翻到10页之后,几十页的时候,基本上就要5~10秒才能查出来一页数据了
1)不允许深度分页/默认深度分页性能很惨
你系统不允许他翻那么深的页,pm,默认翻的越深,性能就越差
2)类似于app里的推荐商品不断下拉出来一页一页的
类似于微博中,下拉刷微博,刷出来一页一页的,你可以用scroll api,自己百度
scroll会一次性给你生成所有数据的一个快照,然后每次翻页就是通过游标移动,获取下一页下一页这样子,性能会比上面说的那种分页性能也高很多很多
针对这个问题,你可以考虑用scroll来进行处理,scroll的原理实际上是保留一个数据快照,然后在一定时间内,你如果不断的滑动往后翻页的时候,类似于你现在在浏览微博,不断往下刷新翻页。那么就用scroll不断通过游标获取下一页数据,这个性能是很高的,比es实际翻页要好的多的多。
但是唯一的一点就是,这个适合于那种类似微博下拉翻页的,不能随意跳到任何一页的场景。同时这个scroll是要保留一段时间内的数据快照的,你需要确保用户不会持续不断翻页翻几个小时。
无论多少页,性能基本上都是毫秒级的
因为scroll api是只能一页一页往后翻的,是不能说,先进入第10页,然后去120页,回到58页,不能随意乱跳页。所以现在很多产品,都是不允许你随意翻页的,app,也有一些网站,做的就是你只能往下拉,一页一页的翻
ES生产集群的部署架构是什么?每个索引的数据量大概是多少?么给索引大概有多少分片?
这个问题,包括后面的redis什么的,谈到es、redis、mysql分库分表等等技术,面试必问!就是你生产环境咋部署的?说白了,这个问题没啥技术含量,就是看你有没有在真正的生产环境里干过这事儿!
有些同学可能是没在生产环境中干过的,没实际去拿线上机器部署过es集群,也没实际玩儿过,也没往es集群里面导入过几千万甚至是几亿的数据量,可能你就不太清楚这里面的一些生产项目中的细节
如果你是自己就玩儿过demo,没碰过真实的es集群,那你可能此时会懵,但是别懵。。。你一定要云淡风轻的回答出来这个问题,表示你确实干过这事儿
3、面试题剖析
其实这个问题没啥,如果你确实干过es,那你肯定了解你们生产es集群的实际情况,部署了几台机器?有多少个索引?每个索引有多大数据量?每个索引给了多少个分片?你肯定知道!
但是如果你确实没干过,也别虚,我给你说一个基本的版本,你到时候就简单说一下就好了
(1)es生产集群我们部署了5台机器,每台机器是6核64G的,集群总内存是320G
(2)我们es集群的日增量数据大概是2000万条,每天日增量数据大概是500MB,每月增量数据大概是6亿,15G。目前系统已经运行了几个月,现在es集群里数据总量大概是100G左右。
(3)目前线上有5个索引(这个结合你们自己业务来,看看自己有哪些数据可以放es的),每个索引的数据量大概是20G,所以这个数据量之内,我们每个索引分配的是8个shard,比默认的5个shard多了3个shard。
3.分布式缓存连环炮
在项目中缓存是如何使用的?缓存如果使用不当会造成什么后果?
为啥在项目里要用缓存?
用缓存,主要是两个用途:高性能 和 高并发
高性能
假设有这么个场景,有一个操作,一个请求过来,然后执行N条SQL语句,然后半天才查询出一个结果,耗时600ms,但是这个结果可能接下来几个小时就不会变了,或者变了也可以不用立即反馈给用户,这个时候就可以使用缓存了。
我们可以把花费了600ms查询出来的数据,丢进缓存中,一个key对应一个value,下次再有人来查询的时候,就不走mysql了,而是直接从缓存中读取,通过key直接查询出value,耗时2ms,性能提升300倍。这就是所谓的高性能。
就是把一些复杂操作耗时查询出来的结果,如果确定后面不怎么变化了,但是马上还有很多读请求,这个时候,就可以直接把结果存放在缓存中,后面直接读取缓存即可。
就第一次从数据库中获取,后面直接从缓存中获取即可,性能提升很高
高并发
MySQL这么重的数据库,并不适合于高并发,虽然可以使用,但是天然支持的就不好,因为MySQL的单机撑到2000QPS的时候,就容易报警了
为什么缓存可以支持高并发
首先因为缓存是走内存的,内存天然就可以支持高并发,但是数据库因为是存储在硬盘上的,因此不要超过2000QPS
场景
所以要是有一个系统,高峰期过来每秒的请求有1W个,要是MySQL单机的话,一定会宕机的,这个时候就只能用上缓存,把很多数据放到缓存中,这样请求过来了之后,就直接从缓存中获取数据,而不查询数据库。缓存的功能很简单,说白了就是一个 key - value式数据库,单机支撑的并发量轻松超过一秒几万 到 十多万,单机的承载量是mysql单机的几十倍。
缓存带来的不良后果
场景的缓存问题有三个
- 缓存与数据库双写不一致的问题
- 缓存穿透
- 缓存雪崩
- 缓存并发竞争
4.Redis的面试连环炮
面试题
- Redis和Memcache有什么区别
- Redis的线程模型是什么?
- Redis的数据类型及应用场景?
- 为什么单线程的Redis比多线程的Memcache的效率要高?
- 为什么Redis是单线程但是还可以支撑高并发?
- Redis如何通过读写分离来承受百万的QPS
- Redis的持久化策略有哪些?AOF和RDB各有什么优缺点
- Redis的过期策略以及LRU算法
- 如何保证Redis的高并发和高可用?
- redis的主从复制原理能介绍一下么?
- redis的哨兵原理能介绍一下么?
- Redis主备切换的数据丢失问题:异步复制、集群脑裂
- Redis哨兵的底层原理
剖析
Redis最基本的一个内部原理和特点就是NIO异步的单线程工作模型。Memcache是早些年个大互联网公司常用的缓存方案,但是现在近几年都是使用的redis,没有什么公司使用Memcache了。
注意:Redis中单个Value的大小最大为512MB,redis的key和string类型value限制均为512MB
Redis和Memcache的区别
从Redis作者给出的几个比较
- Redis拥有更多的数据结构
- Redis相比Memcache来说,拥有更多的数据结构和支持更丰富的数据操作,通常在Memcache里,你需要将数据拿到客户端来进行类似的修改,在set进去。这就大大增加了网络IO的次数和体积,在Redis中,这些复杂的操作通常和一般的set/get一样高效。所以,如果需要缓存能够支持更复杂的结构和操作,那么Redis是不错的选择
- Redis内存利用率对比
- 使用简单的key-value存储的话,Memcache的内存利用率更高,而Redis采用Hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于Memcache
- 性能对比
- 由于Redis只使用了单核,而Memcache可以使用多核,所以平均每核上Redis在存储小数据比Memcache性能更高,而在100K以上的数据中,Memcache性能更高,虽然Redis最近也在存储大数据的性能上进行优化,但是比起Memcache还有略有逊色。
- 集群模式
- Memcache没有原生的集群模式,需要依赖客户端来实现往集群中分片写入数据,但是Redis目前是原生支持cluster模式的。
Redis都有哪些数据类型,及使用场景
-
String
- 最基本的类型,就和普通的set 和 get,做简单的key - value 存储
-
Hash
- 这个是 类似于Map的一种结构,就是一半可以将结构化数据,比如对象(前提是这个对象没有嵌套其它对象)给缓存在redis中,每次读写redis缓存的时候,可以操作hash里面的某个字段
key=150 value={ "id": 150, "name": "张三", "age": 20, }
- Hash类的数据结构,主要用来存放一些对象,把一些简单的对象给缓存起来,后续操作的时候,你可以直接仅仅修改这个对象中某个字段的值。
-
List
- 有序列表,可以通过list存储一些列表型的数据结构,类似粉丝列表,文章的评论列表之类的东西。
- 可以通过lrange命令,从某个元素开始读取多少个元素,可以基于list实现分页查询,基于Redis实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就是一页一页走。
- 可以制作一个简单的消息队列,从list头插入,从list 的尾巴取出
-
Set
- 无序列表,自动去重
- 直接基于Set将系统中需要去重的数据丢进去,如果你需要对一些数据进行快速的全局去重,就可以使用基于JVM内存里的HashSet进行去重,但是如果你的某个系统部署在多台机器上的话,只有使用Redis进行全局的Set去重
- 可以基于set玩儿交集、并集、差集的操作,比如交集吧,可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁?把两个大v的粉丝都放在两个set中,对两个set做交集
-
Sort Set
- 排序的set,去重但是可以排序,写进去的时候给一个分数,自动根据分数排序,这个可以玩儿很多的花样,最大的特点是有个分数可以自定义排序规则
- 比如说你要是想根据时间对数据排序,那么可以写入进去的时候用某个时间作为分数,人家自动给你按照时间排序了
- 排行榜:将每个用户以及其对应的什么分数写入进去,zadd board score username,接着zrevrange board 0 99,就可以获取排名前100的用户;zrank board username,可以看到用户在排行榜里的排名
zadd board 85 zhangsan
zadd board 72 wangwu
zadd board 96 lisi
zadd board 62 zhaoliu
96 lisi
85 zhangsan
72 wangwu
62 zhaoliu
zrevrange board 0 3
获取排名前3的用户
96 lisi
85 zhangsan
72 wangwu
zrank board zhaoliu
Redis持久化对于生产环境的意义
故障发生时候会怎么样?
如何应对故障的发生?
Redis持久化的意义
Redis持久化的意义,在于故障恢复,也属于高可用的一个环节。例如
当存放在内存中数据,会因为Redis的突然挂掉,而导致数据丢失
Redis的持久化,就是将内存中的数据,持久化到磁盘上中,然后将磁盘上的数据放到阿里云ODPS中
通过持久化将数据存储在磁盘中,然后定期比如说同步和备份到一些云存储服务上去。
Redis中的RDB和AOF两种持久化机制
当出现Redis宕机时,我们需要做的是重启redis,尽快让他对外提供服务,缓存全部无法命中,在redis里根本找不到数据,这时候就会出现缓存雪崩的问题。所有的请求,没有在Redis中命中,就会去MySQL数据库这种数据源头中找,一下子MySQL无法承受高并发,那么系统将直接宕机。这个时候MySQL宕机,因为没办法从MySQL中将缓存恢复到Redis中,因为Redis中的数据是从MySQL中来的。
RDB持久化机制
简单来说RDB:就是将Redis中的数据,每个一段时间,进行数据持久化
AOF持久化机制
Redis将内存中的数据,存放到一个AOF文件中,但是因为Redis只会写一个AOF文件,因此这个AOF文件会越来越大。
AOF机制对每条写入命令作为日志,以append-only的模式写入一个日志文件中,在Redis重启的时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集。
因为Redis中的数据是有一定限量的,不可能说Redis内存中的数据不限量增长,进而导致AOF无限量增长。
内存大小是一定的,到一定时候,Redis就会用缓存淘汰算法,LRU,自动将一部分数据从内存中给清除。
AOF,是存放每条写命令的,所以会不断的膨胀,当大到一定的时候,AOF做rewrite操作。
AOF rewrite操作,就会基于当时redis内存中的数据,来重新构造一个更小的AOF文件,然后将旧的膨胀的很大的文件给删了。
如果我们想要Redis仅仅作为纯内存的缓存来使用,那么可以禁止RDB和AOF所有的持久化机制
通过AOF和RDB,都可以将Redis内存中的数据给持久化到磁盘上面来,然后可以将这些数据备份到其它地方去,例如阿里云的OOS。
如果Redis挂了,服务器上的内存和磁盘上的数据都丢了,可以从云服务上拷贝回来之前的数据,放到指定的目录下,然后重新启动Redis,Redis就会自动根据持久化数据文件,去恢复内存中的数据,继续对外提供服务。
如果同时使用RDB和AOF两种持久化机制,那么在Redis重启的时候,会使用AOF来重新构建数据,因为AOF中的数据更加完整。
RDB持久化机制的优点
- RDB会生成多个数据文件,每个数据文件都代表了某个时刻中Redis的数据,这种多个数据文件的方式,非常适合做冷备份,可以将这种完整的数据文件发送到一些远程的安全存储上去,例如阿里云ODPS分布式存储上,以预定好的备份策略来定期备份Redis中的数据
- RDB也可以做冷备份,生成多个文件,每个文件代表了某个时刻的完整的数据快照
- AOF也可以做冷备,只有一个文件,但是你可以每隔一段时间,去copy一份文件出来
- RDB做冷备份的优势在于,可以由Redis去控制固定时长生成快照文件的事情,比较方便。AOF还需要自己写一些脚本去做这个事情,各种定时。
- RDB对Redis对外提供的读写服务,影响非常小,可以让Redis保持高性能,因为Redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可。
- RDB每次写都是些Redis内存的,只是在一定的时间内,才将数据写入磁盘
- AOF每次都要写文件,虽然可以快速写入 OS Cache中,但是还是会有一定的时间开销,速度肯定比RDB略慢一点。
- 相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复Redis进程,更加快速。
- RDB数据做冷备份,在最坏的情况下,提供数据恢复的时候,速度比AOF快。
- AOF,存放的指令日志,做数据恢复的时候,其实是要回放和执行所有的指令日志,来恢复出来内存中的所有数据的,而RDB就是一份数据文件,恢复的时候,直接加载进内存即可。
综合上面可以知道:RDB特别适合做冷备份
RDB持久化的缺点
- 如果想要在Redis故障时,尽可能的少丢失数据,那么RDB没有AOF好,一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦Redis经常宕机,那么丢失最近5分钟的数据。
这个文件也是RDB最大的缺点,就是不适合做第一优先级的恢复方案,如果你依赖RDB做第一优先级方案,会导致数据丢失的比较多。
- RDB每次在fork子进程来执行RDB快照数据生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒
- 一般不要让RDB的间隔太长,否则每次生成的RDB文件太长,会对Redis本身的性能会有影响
AOF持久化的优点
- AOF可以更好的保护数据不丢失,一般AOF会间隔一秒,通过一个后台线程执行一次fsync操作,最多丢失1秒
- AOF日志文件以append-only模式写入,所有没有任何磁盘寻址开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易快速修复。
- AOF日志文件及时过大的时候,出现后台的重写操作,也不会影响客户端的读写,因为rewrite log 的时候,会对其中的数据进行压缩,创建出一份需要恢复数据的最小日志出来,再创建新日志文件的时候,老的日志文件还是照常写入,当新的merge后的日志文件ready的时候,再交换新老日志文件即可。
- AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复,比如某人不小心用了 flushall命令,清空了整个Redis数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令删除了,然后再将该AOF文件放回去,就可以通过恢复机制,自动回复所有的数据。
AOF持久化机制的缺点
- 对于同一份数据来说,AOF日志通常比RDB数据快照文件更大
- AOF开启后,支持写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,因此这也就造成了性能不是很高。
- 如果你要保证一条数据都不丢,也可以的,AOF的fsync设置成每次写入一条数据,fsync一次,这样Redis的QPS会大降。
- AOF这种较为复杂的基于命令日志/merge/回放的方式,比基于RDB每次持久化一份完整的数据快照的方式,更加脆弱一些,容易有BUG,不过AOF就是为了避免rewrite过程导致的BUG,因此每次rewrite并不是基于旧的指令来进行merge的,而是基于当时内存中数据进行指令的重新构建,这与健壮性会好一些。
- 唯一的缺点:就是做数据恢复的时候,会比较慢,还有做冷备,定期的被封,不太方便,可能要自己手动写复杂的脚本去做。
RDB和AOF的选择
- 不要仅仅使用RDB,因为那样会导致你丢失很多的数据
- 也不要仅仅使用AOF,因为这样有两个问题
- AOF做冷备,没有RDB冷备恢复快
- RDB每次简单粗暴的生成数据快照,更加健壮,可以避免AOF这种复杂的被封和恢复机制的BUG
- 综合使用AOF和RDB两种持久化机制,用AOF来保证数据不丢失,作为数据恢复的第一选择,用RDB来做不同程度的冷备,在AOF文件都丢失或者损坏不可用的时候,可以使用RDB来进行快速的数据恢复。
Redis的线程模型
文件事件处理器
Redis基于reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器,file event handler,这个文件事件处理器是单线程的,因此Redis才叫做单线程的模型,采用IO多路复用机制同时监听多个socket,根据socket上的事件来选择相应的事件处理器来处理这个事件。
文件事件处理器是单线程模式下运行的,但是通过IO多路复用机制监听了多个socket,可以实现高性能的网络通信模型,又可以跟内部的其它单线程的模块进行对接,保证了Redis内部的线程模型的简单性。
文件事件处理器的结构包含4个部分:多个socket,IO多路复用程序,文件事件分派器,事件处理器等。
多个socket可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个socket,但是会把socket放入到一个队列中排队,每次从队列中取出一个socket给事件分派器,事件分派器把socket给对应的时间处理器。
每次我们一个socket请求过来 和 redis中的 server socket建立连接后,通过IO多路复用程序,就会往队列中插入一个socket,文件事件分派器就是将队列中的socket取出来,分派到对应的处理器,在处理器处理完成后,才会从队列中在取出一个。
这里也就是用一个线程,监听了客户端的所有请求,被称为Redis的单线程模型。
为什么Redis单线程模型效率这么高?
- 纯内存操作
- 核心是非阻塞的IO多路复用机制
- 单线程反而避免了多线程频繁上下文切换的问题
Redis的过期策略
Redis中的数据为什么会丢失
之前有同学问过我,说我们生产环境的redis怎么经常会丢掉一些数据?写进去了,过一会儿可能就没了。我的天,同学,你问这个问题就说明redis你就没用对啊。redis是缓存,你给当存储了是吧?
啥叫缓存?用内存当缓存。内存是无限的吗,内存是很宝贵而且是有限的,磁盘是廉价而且是大量的。可能一台机器就几十个G的内存,但是可以有几个T的硬盘空间。redis主要是基于内存来进行高性能、高并发的读写操作的。
那既然内存是有限的,比如redis就只能用10个G,你要是往里面写了20个G的数据,会咋办?当然会干掉10个G的数据,然后就保留10个G的数据了。那干掉哪些数据?保留哪些数据?当然是干掉不常用的数据,保留常用的数据了。所以说,这是缓存的一个最基本的概念,数据是会过期的,要么是你自己设置个过期时间,要么是redis自己给干掉。
set key value 过期时间(1小时)
set进去的key,1小时之后就没了,就失效了
数据明明都过期了,怎么还占用着内存啊?
还有一种就是如果你设置好了一个过期时间,你知道redis是怎么给你弄成过期的吗?什么时候删除掉?如果你不知道,之前有个学员就问了,为啥好多数据明明应该过期了,结果发现redis内存占用还是很高?那是因为你不知道redis是怎么删除那些过期key的。
redis 内存一共是10g,你现在往里面写了5g的数据,结果这些数据明明你都设置了过期时间,要求这些数据1小时之后都会过期,结果1小时之后,你回来一看,redis机器,怎么内存占用还是50%呢?5g数据过期了,我从redis里查,是查不到了,结果过期的数据还占用着redis的内存。
定期删除和惰性删除
我们Redis设置了过期时间,其实内部是 定期删除 + 惰性删除两个再起作用的。
所谓定期删除,指的是redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。假设redis里放了10万个key,都设置了过期时间,你每隔几百毫秒,就检查10万个key,那redis基本上就死了,cpu负载会很高的,消耗在你的检查过期key上了。注意,这里可不是每隔100ms就遍历所有的设置过期时间的key,那样就是一场性能上的灾难。实际上redis是每隔100ms随机抽取一些key来检查和删除的。
但是问题是,定期删除可能会导致很多过期key到了时间并没有被删除掉,那咋整呢?所以就是惰性删除了。这就是说,在你获取某个key的时候,redis会检查一下 ,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。
并不是key到时间就被删除掉,而是你查询这个key的时候,redis再懒惰的检查一下
通过上述两种手段结合起来,保证过期的key一定会被干掉。
很简单,就是说,你的过期key,靠定期删除没有被删除掉,还停留在内存里,占用着你的内存呢,除非你的系统去查一下那个key,才会被redis给删除掉。
但是实际上这还是有问题的,如果定期删除漏掉了很多过期key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了,咋整?
答案是:走内存淘汰机制。
Redis内存淘汰机制
如果redis的内存占用过多的时候,此时会进行内存淘汰,有如下一些策略:
redis 10个key,现在已经满了,redis需要删除掉5个key
1个key,最近1分钟被查询了100次
1个key,最近10分钟被查询了50次
1个key,最近1个小时倍查询了1次
1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的key给干掉啊
4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)
5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除
很简单,你写的数据太多,内存满了,或者触发了什么条件,redis lru,自动给你清理掉了一些最近很少使用的数据
Redis中的LRU算法
Java版本的LRU
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int CACHE_SIZE;
// 这里就是传递进来最多能缓存多少数据
public LRUCache(int cacheSize) {
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true); // 这块就是设置一个hashmap的初始大小,同时最后一个true指的是让linkedhashmap按照访问顺序来进行排序,最近访问的放在头,最老访问的就在尾
CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > CACHE_SIZE; // 这个意思就是说当map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据
}
如何保证Redis的高并发及高可用?
如何保证Redis的高并发和高可用?
redis的主从复制原理能介绍一下么?
redis的哨兵原理能介绍一下么?
剖析
就是如果你用redis缓存技术的话,肯定要考虑如何用redis来加多台机器,保证redis是高并发的,还有就是如何让Redis保证自己不是挂掉以后就直接死掉了,redis高可用
我这里会选用我之前讲解过这一块内容,redis高并发、高可用、缓存一致性
redis高并发:主从架构,一主多从,一般来说,很多项目其实就足够了,单主用来写入数据,单机几万QPS,多从用来查询数据,多个从实例可以提供每秒10万的QPS。
redis高并发的同时,还需要容纳大量的数据:一主多从,每个实例都容纳了完整的数据,比如redis主就10G的内存量,其实你就最对只能容纳10g的数据量。如果你的缓存要容纳的数据量很大,达到了几十g,甚至几百g,或者是几t,那你就需要redis集群,而且用redis集群之后,可以提供可能每秒几十万的读写并发。
redis高可用:如果你做主从架构部署,其实就是加上哨兵就可以了,就可以实现,任何一个实例宕机,自动会进行主备切换。
Redis如何通过读写分离来承受百万的QPS
redis高并发跟整个系统的高并发之间的关系
redis,你要搞高并发的话,不可避免,要把底层的缓存搞得很好
mysql,高并发,做到了,那么也是通过一系列复杂的分库分表,订单系统,事务要求的,QPS到几万,比较高了
要做一些电商的商品详情页,真正的超高并发,QPS上十万,甚至是百万,一秒钟百万的请求量
光是redis是不够的,但是redis是整个大型的缓存架构中,支撑高并发的架构里面,非常重要的一个环节
首先,你的底层的缓存中间件,缓存系统,必须能够支撑的起我们说的那种高并发,其次,再经过良好的整体的缓存架构的设计(多级缓存架构、热点缓存),支撑真正的上十万,甚至上百万的高并发
redis不能支撑高并发的瓶颈在哪里?
因为单机的Redis,QPS只能在上万左右,成为了支撑高并发的瓶颈。
如果redis要支撑超过10万+的并发,那应该怎么做?
单机的redis几乎不太可能说QPS超过10万+,除非一些特殊情况,比如你的机器性能特别好,配置特别高,物理机,维护做的特别好,而且你的整体的操作不是太复杂,单机在几万。
读写分离,一般来说,对缓存,一般都是用来支撑读高并发的,写的请求是比较少的,可能写请求也就一秒钟几千,一两千。大量的请求都是读,一秒钟二十万次读
读写分离:主从架构 -> 读写分离 -> 支撑10万+读QPS的架构
架构做成主从架构,一主多从,主服务器负责写,并且将数据同步到其它的slave节点,从节点负责读,所有的读请求全部走节点。
同时这样的架构,支持碎片扩容,就是说如果QPS在增加,也很简单,只需要增加 Redis Slave节点即可。
Redis主从架构
redis主从架构 -> 读写分离架构 -> 可支持水平扩展的读高并发架构
基本原理
- redis采用异步方式复制数据到slave节点,不过redis 2.8开始,slave node会周期性地确认自己每次复制的数据量
- 一个master node是可以配置多个slave node的
- slave node也可以连接其他的slave node
- slave node做复制的时候,是不会block master node的正常工作的
- slave node在做复制的时候,也不会block对自己的查询操作,它会用旧的数据集来提供服务; 但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了
- slave node主要用来进行横向扩容,做读写分离,扩容的slave node可以提高读的吞吐量
写操作存放在master node,同时在异步把master上的信息,同步到每个slave node上。
master持久化对于主从架构的安全保障的意义
如果采用了主从架构,那么建议必须开启master node的持久化!不建议用slave node作为master node的数据热备,因为那样的话,如果你关掉master的持久化,可能在master宕机重启的时候数据是空的,然后可能一经过复制,salve node数据也丢了
master -> RDB和AOF都关闭了 -> 全部在内存中
master宕机,重启,是没有本地数据可以恢复的,然后就会直接认为自己IDE数据是空的
master就会将空的数据集同步到slave上去,所有slave的数据全部清空
100%的数据丢失
master节点,必须要使用持久化机制
第二个,master的各种备份方案,要不要做,万一说本地的所有文件丢失了; 从备份中挑选一份rdb去恢复master; 这样才能确保master启动的时候,是有数据的
即使采用了后续讲解的高可用机制,slave node可以自动接管master node,但是也可能sentinal还没有检测到master failure,master node就自动重启了,还是可能导致上面的所有slave node数据清空故障
Redis主从复制原理
当启动一个slave node的时候,它会发送一个PSYNC命令给master node,如果这是slave node重新连接master node,那么master node仅仅会复制给slave部分缺少的数据; 否则如果是slave node第一次连接master node,那么会触发一次full resynchronization
开始full resynchronization的时候,master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端收到的所有写命令缓存在内存中。RDB文件生成完毕之后,master会将这个RDB发送给slave,slave会先写入本地磁盘,然后再从本地磁盘加载到内存中。然后master会将内存中缓存的写命令发送给slave,slave也会同步这些数据。
slave node如果跟master node有网络故障,断开了连接,会自动重连。master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node。
主从复制的断点续传
从redis 2.8开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份
master node会在内存中常见一个backlog,master和slave都会保存一个replica offset还有一个master id,offset就是保存在backlog中的。如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制,但是如果没有找到对应的offset,那么就会执行一次resynchronization
无磁盘化复制
master在内存中直接创建rdb,然后发送给slave,不会在自己本地落地磁盘了
repl-diskless-sync
# 等待一定时长再开始复制,因为要等更多slave重新连接过来
repl-diskless-sync-delay
Redis主从复制的完整复制流程
主从复制流程图
- slave node启动,仅仅保存master node的信息,包括master node的host和ip,但是复制流程没开始master host和ip是从哪儿来的,redis.conf里面的slaveof配置的
- slave node内部有个定时任务,每秒检查是否有新的master node要连接和复制,如果发现,就跟master node建立socket网络连接
- slave node发送ping命令给master node
- 口令认证,如果master设置了requirepass,那么salve node必须发送masterauth的口令过去进行认证
- master node第一次执行全量复制,将所有数据发给slave node
- master node后续持续将写命令,异步复制给slave node
数据同步相关核心机制
指的就是第一次slave连接msater的时候,执行的全量复制,那个过程里面你的一些细节的机制
- master和slave都会维护一个offset
master会在自身不断累加offset,slave也会在自身不断累加offset slave每秒都会上报自己的offset给master,同时master也会保存每个slave的offset
这个倒不是说特定就用在全量复制的,主要是master和slave都要知道各自的数据的offset,才能知道互相之间的数据不一致的情况
- backlog
master node有一个backlog,默认是1MB大小 master node给slave node复制数据时,也会将数据在backlog中同步写一份 backlog主要是用来做全量复制中断候的增量复制的
- master run id
info server,可以看到master run id 如果根据host+ip定位master node,是不靠谱的,如果master node重启或者数据出现了变化,那么slave node应该根据不同的run id区分,run id不同就做全量复制 如果需要不更改run id重启redis,可以使用redis-cli debug reload命令
- psync
从节点使用psync从master node进行复制,psync runid offset master node会根据自身的情况返回响应信息,可能是FULLRESYNC runid offset触发全量复制,可能是CONTINUE触发增量复制
全量复制
- master执行bgsave,在本地生成一份rdb快照文件
- master node将rdb快照文件发送给salve node,如果rdb复制时间超过60秒(repl-timeout),那么slave node就会认为复制失败,可以适当调节大这个参数
- 对于千兆网卡的机器,一般每秒传输100MB,6G文件,很可能超过60s
- master node在生成rdb时,会将所有新的写命令缓存在内存中,在salve node保存了rdb之后,再将新的写命令复制给salve node
- client-output-buffer-limit slave 256MB 64MB 60,如果在复制期间,内存缓冲区持续消耗超过64MB,或者一次性超过256MB,那么停止复制,复制失败
- slave node接收到rdb之后,清空自己的旧数据,然后重新加载rdb到自己的内存中,同时基于旧的数据版本对外提供服务
rdb生成、rdb通过网络拷贝、slave旧数据的清理、slave aof rewrite,很耗费时间
如果slave node开启了AOF,那么会立即执行BGREWRITEAOF,重写AOF
增量复制
- 如果全量复制过程中,master-slave网络连接断掉,那么salve重新连接master时,会触发增量复制
- master直接从自己的backlog中获取部分丢失的数据,发送给slave node,默认backlog就是1MB
- msater就是根据slave发送的psync中的offset来从backlog中获取数据的
异步复制
master每次接收到写命令之后,现在内部写入数据,然后异步发送给slave node
心跳机制
master默认每隔10秒发送一次心跳,salve node每隔1秒发送一个心跳
Redis主从架构如何才能做到99.99%的高可用性?
架构上,高可用性,99.99%的高可用性
99.99%,公式,系统可用的时间 / 系统故障的时间,365天,在365天 * 99.99%的时间内,你的系统都是可以哗哗对外提供服务的,那就是高可用性,99.99%
系统可用的时间 / 总的时间 = 高可用性,然后会对各种时间的概念,说一大堆解释
系统可用性
系统处于不可用
Redis的不可用
一个slave宕机后,不会影响系统的可用性,还有其它slave在提供相同数据的情况下对外提供查询服务。
master宕机后,相当于系统不可用了。
Redis高可用的方案
当Redis的master节点宕机后,redis的高可用架构中,有一个故障转移,叫failover,也可以做主备切换。
总结
Redis实现高并发:一主多从,一般来说,很多项目其实就足够了,单主用来写数据,单机几万QPS,多从用来查询数据,多个从实例可以提供每秒10万QPS
Redis高并发的同时,还需要容纳大量的数据:一主多从,每个实例都容纳了完整的数据,比如Redis主就10G内存量,其实你就可以对只能容纳10G的数据量。如果你的缓存要容纳的数据量很大,达到了几十G,甚至几百G,那就需要使用到Redis集群,而且用Redis集群之后,提供可能每秒几十万的读写并发。
Redis高可用:如果用主从架构部署,在加上哨兵就可以实现任何一个实例宕机,就会自动进行主备切换。
Redis哨兵架构
哨兵介绍
sentinal,中文名是哨兵
哨兵是redis集群架构中非常重要的一个组件,主要功能如下
- 集群监控,负责监控redis master和slave进程是否正常工作
- 消息通知,如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
- 故障转移,如果master node挂掉了,会自动转移到slave node上
- 配置中心,如果故障转移发生了,通知client客户端新的master地址
哨兵本身也是分布式的,作为一个哨兵集群去运行,互相协同工作
- 故障转移时,判断一个master node是宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题
- 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了
目前采用的是sentinal 2版本,sentinal 2相对于sentinal 1来说,重写了很多代码,主要是让故障转移的机制和算法变得更加健壮和简单
哨兵的核心知识
- 哨兵至少需要3个实例,来保证自己的健壮性
- 哨兵 + redis主从的部署架构,是不会保证数据零丢失的,只能保证redis集群的高可用性
- 对于哨兵 + redis主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练
为什么Redis的哨兵集群只有2个节点无法正常工作?
哨兵集群必须部署2个以上节点
如果哨兵集群仅仅部署了个2个哨兵实例,s1 和 s2,quorum=1
+----+ +----+
| M1 |---------| R1 |
| S1 | | S2 |
+----+ +----+
Configuration: quorum = 1
master宕机,s1和s2中只要有1个哨兵认为master宕机就可以还行切换,同时s1和s2中会选举出一个哨兵来执行故障转移同时这个时候,需要majority,也就是大多数哨兵都是运行的,2个哨兵的majority就是2(2的majority=2,3的majority=2,5的majority=3,4的majority=2),2个哨兵都运行着,就可以允许执行故障转移但是如果整个M1和S1运行的机器宕机了,那么哨兵只有1个了,此时就没有majority来允许执行故障转移,虽然另外一台机器还有一个R1,但是故障转移不会执行
经典的3节点哨兵集群
+----+
| M1 |
| S1 |
+----+
|
+----+ | +----+
| R2 |----+----| R3 |
| S2 | | S3 |
+----+ +----+
Configuration: quorum = 2,majority
如果M1所在机器宕机了,那么三个哨兵还剩下2个,S2和S3可以一致认为master宕机,然后选举出一个来执行故障转移,同时3个哨兵的majority是2,所以还剩下的2个哨兵运行着,就可以允许执行故障转移
Redis主备切换的数据丢失问题:异步复制、集群脑裂
主备切换的过程,可能会导致数据丢失
异步复制导致的数据丢失
因为master -> slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了。
脑裂导致的数据丢失
脑裂,也就是说,某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着,此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master
这个时候,集群里就会有两个master,也就是所谓的脑裂。此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了
因此旧master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据
同时原来的master节点上的,client像 旧的 master中写入数据,当网络分区恢复正常后,client写的数据就会因为复制,导致数据的丢失。
解决异步复制和脑裂导致数据丢失
min-slaves-to-write 1
min-slaves-max-lag 10
要求至少有1个slave,数据复制和同步的延迟不能超过10秒
如果说一旦所有的slave,数据复制和同步的延迟都超过了10秒钟,那么这个时候,master就不会再接收任何请求了,上面两个配置可以减少异步复制和脑裂导致的数据丢失
- 减少异步复制的数据丢失
有了min-slaves-max-lag这个配置,就可以确保说,一旦slave复制数据和ack延时太长,就认为可能master宕机后损失的数据太多了,那么就拒绝写请求,这样可以把master宕机时由于部分数据未同步到slave导致的数据丢失降低的可控范围内
- 减少脑裂的数据丢失
如果一个master出现了脑裂,跟其他slave丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求,这样脑裂后的旧master就不会接受client的新数据,也就避免了数据丢失,上面的配置就确保了,如果跟任何一个slave丢了连接,在10秒后发现没有slave给自己ack,那么就拒绝新的写请求,因此在脑裂场景下,最多就丢失10秒的数据
Redis哨兵的底层原理
sdown和odown转换机制
sdown和odown两种失败状态
sdown是主观宕机,就一个哨兵如果自己觉得一个master宕机了,那么就是主观宕机
odown是客观宕机,如果quorum数量的哨兵都觉得一个master宕机了,那么就是客观宕机
sdown达成的条件很简单,如果一个哨兵ping一个master,超过了is-master-down-after-milliseconds指定的毫秒数之后,就主观认为master宕机
sdown到odown转换的条件很简单,如果一个哨兵在指定时间内,收到了quorum指定数量的其他哨兵也认为那个master是sdown了,那么就认为是odown了,客观认为master宕机
哨兵集群的自动发现机制
哨兵互相之间的发现,是通过redis的pub/sub系统实现的,每个哨兵都会往__sentinel__:hello这个channel里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在
每隔两秒钟,每个哨兵都会往自己监控的某个master+slaves对应的__sentinel__:hello channel里发送一个消息,内容是自己的host、ip和runid还有对这个master的监控配置
每个哨兵也会去监听自己监控的每个master+slaves对应的__sentinel__:hello channel,然后去感知到同样在监听这个master+slaves的其他哨兵的存在
每个哨兵还会跟其他哨兵交换对master的监控配置,互相进行监控配置的同步
slave配置的自动纠正
哨兵会负责自动纠正slave的一些配置,比如slave如果要成为潜在的master候选人,哨兵会确保slave在复制现有master的数据; 如果slave连接到了一个错误的master上,比如故障转移之后,那么哨兵会确保它们连接到正确的master上
slave->master选举算法
如果一个master被认为odown了,而且majority哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个slave来,会考虑slave的一些信息
- 跟master断开连接的时长
- slave优先级
- 复制offset
- run id
如果一个slave跟master断开连接已经超过了down-after-milliseconds的10倍,外加master宕机的时长,那么slave就被认为不适合选举为master
(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state
接下来会对slave进行排序
- 按照slave优先级进行排序,slave priority越低,优先级就越高
- 如果slave priority相同,那么看replica offset,哪个slave复制了越多的数据,offset越靠后,优先级就越高
- 如果上面两个条件都相同,那么选择一个run id比较小的那个slave
quorum和majority
每次一个哨兵要做主备切换,首先需要quorum数量的哨兵认为odown,然后选举出一个哨兵来做切换,这个哨兵还得得到majority哨兵的授权,才能正式执行切换
如果quorum < majority,比如5个哨兵,majority就是3,quorum设置为2,那么就3个哨兵授权就可以执行切换
但是如果quorum >= majority,那么必须quorum数量的哨兵都授权,比如5个哨兵,quorum是5,那么必须5个哨兵都同意授权,才能执行切换
configuration epoch
哨兵会对一套redis master+slave进行监控,有相应的监控的配置
执行切换的那个哨兵,会从要切换到的新master(salve->master)那里得到一个configuration epoch,这就是一个version号,每次切换的version号都必须是唯一的
如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout时间,然后接替继续执行切换,此时会重新获取一个新的configuration epoch,作为新的version号
configuraiton传播
哨兵完成切换之后,会在自己本地更新生成最新的master配置,然后同步给其他的哨兵,就是通过之前说的pub/sub消息机制,这里之前的version号就很重要了,因为各种消息都是通过一个channel去发布和监听的,所以一个哨兵完成一次新的切换之后,新的master配置是跟着新的version号的,其他的哨兵都是根据版本号的大小来更新自己的master配置的
5.Redis集群模式连环炮
面试题
- Redis集群模式的工作原理?
- 在集群模式下,redis的key是如何寻址的?
- 分布式寻址都有哪些算法?
- 了解一致性Hash算法么?
- 如何应对缓存雪崩以及缓存穿透问题?
- 如何保证缓存与数据库双写时的数据一致性?
- Redis的并发竞争问题是什么?怎么解决?
- 了解Redis事务的CAS方案?
剖析
在以前,如果前几年的时候,一般来说,redis如果要搞几个节点,每个节点存储一部分的数据,得借助一些中间件来实现,比如说有codis,或者twemproxy,都有。有一些redis中间件,你读写redis中间件,redis中间件负责将你的数据分布式存储在多台机器上的redis实例中。
这两年,redis不断在发展,redis也不断的有新的版本,redis cluster,redis集群模式,你可以做到在多台机器上,部署多个redis实例,每个实例存储一部分的数据,同时每个redis实例可以挂redis从实例,自动确保说,如果redis主实例挂了,会自动切换到redis从实例顶上来。
现在redis的新版本,大家都是用redis cluster的,也就是redis原生支持的redis集群模式,那么面试官肯定会就redis cluster对你来个几连炮。要是你没用过redis cluster,正常,以前很多人用codis之类的客户端来支持集群,但是起码你得研究一下redis cluster吧。
Redis集群模式的工作原理
单机瓶颈
Redis在单机架构下的瓶颈:master节点的数据和slave节点的数据量一样,也就是master容纳多少,slave也只能容纳多少,如果需要放1T数据,在缓存中,那么就遇到的性能瓶颈了。
集群模式
支撑N个redis master node,每个master node都可以挂载多个slave node,读写分离的架构,对于每个master来说,写就写到master,然后读就从mater对应的slave去读,高可用,因为每个master都有salve节点,那么如果mater挂掉,redis cluster这套机制,就会自动将某个slave切换成master,redis cluster(多master + 读写分离 + 高可用),我们只要基于redis cluster去搭建redis集群即可,不需要手工去搭建replication复制+主从架构+读写分离+哨兵集群+高可用
Redis cluster 和 Replication + sentinel
Redis Cluster
是Redis的集群模式
- 自动将数据进行分片,每个master上放一部分数据
- 提供内置的高可用支持,部分master不可用时,还是可以继续工作的
在redis cluster架构下,每个redis要放开两个端口号,比如一个是6379,另外一个就是加10000的端口号,比如16379端口号是用来进行节点间通信的,也就是cluster bus的东西,集群总线。cluster bus的通信,用来进行故障检测,配置更新,故障转移授权
Redis replication + sentinel:高可用模式
如果你的数据量很少,主要是承载高并发高性能的场景,比如你的缓存一般就几个G,单机足够了,replication,一个mater,多个slave,要几个slave跟你的要求的读吞吐量有关系,然后自己搭建一个sentinal集群,去保证redis主从架构的高可用性,就可以了
redis cluster,主要是针对海量数据+高并发+高可用的场景,海量数据,如果你的数据量很大,那么建议就用redis cluster
分布式数据存储的核心算法,数据分布的算法
hash算法 -> 一致性hash算法(memcached) -> redis cluster,hash slot 算法
用不同的算法,就决定了在多个master节点的时候,数据如何分布到这些节点上去,解决这个问题
Hash算法
最老土的hash算法和弊端(大量缓存重建),属于最简单的数据分布算法
但是如果某一台master宕机了,会导致 1/3的数据全部失效,从而大量的数据将会进入MySQL
一致性Hash算法
Memcache中使用的是一致性Hash算法
缓存热点问题
因为上面的一致性Hash环,不能解决缓存热点问题,即集中在某个Hash区间内的值特别多,这样就会导致大量的请求同时涌入一个master节点,而其它的节点处于空闲状态,从而造成master热点问题。
这个时候就引入了虚拟环(虚拟节点)的概念,目的是为了让每个master都做了均匀分布,这样每个区间内的数据都能够 均衡的分布到不同的节点中,而不是按照顺时针去查找,从而造成涌入一个master上的问题。
Redis Cluster
Redis Cluster有固定的16384个Hash slot,对每个key计算CRC16值,然后对16384取模,可以获取key对应的hash slot,redis cluster中每个master都会持有部分slot,比如有3个master,那么可能每个master持有5000多个hash slot,hash slot让node的增加和移除很简单,增加一个master,就将其他master的hash slot移动部分过去,减少一个master,就将它的hash slot移动到其他master上去,移动hash slot的成本是非常低的,客户端的api,可以对指定的数据,让他们走同一个hash slot,通过hash tag来实现
如果有一台master宕机了,其它节点上的缓存几乎不受影响,因为它取模运算是根据 Hash slot来的,也就是 16384,而不是根据Redis的机器数。
Redis Cluster节点通信
基础通信原理
(1)redis cluster节点间采取gossip协议进行通信
跟集中式不同,不是将集群元数据(节点信息,故障,等等)集中存储在某个节点上,而是互相之间不断通信,保持整个集群所有节点的数据是完整的
维护集群的元数据用得,集中式,一种叫做gossip
集中式:好处在于,元数据的更新和读取,时效性非常好,一旦元数据出现了变更,立即就更新到集中式的存储中,其他节点读取的时候立即就可以感知到; 不好在于,所有的元数据的跟新压力全部集中在一个地方,可能会导致元数据的存储有压力
gossip:好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力; 缺点,元数据更新有延时,可能导致集群的一些操作会有一些滞后
我们刚才做reshard,去做另外一个操作,会发现说,configuration error,达成一致
(2)10000端口
每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如7001,那么用于节点间通信的就是17001端口
每隔节点每隔一段时间都会往另外几个节点发送ping消息,同时其他几点接收到ping之后返回pong
(3)交换的信息
故障信息,节点的增加和移除,hash slot信息,等等
gossip协议
gossip协议包含多种消息,包括ping,pong,meet,fail,等等
meet: 某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信
redis-trib.rb add-node
其实内部就是发送了一个gossip meet消息,给新加入的节点,通知那个节点去加入我们的集群
ping: 每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据,每个节点每秒都会频繁发送ping给其他的集群,频繁的互相之间交换数据,互相进行元数据的更新
pong: 返回ping和meet,包含自己的状态和其他信息,也可以用于信息广播和更新
fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了
ping消息深入
ping很频繁,而且要携带一些元数据,所以可能会加重网络负担,每个节点每秒会执行10次ping,每次会选择5个最久没有通信的其他节点,当然如果发现某个节点通信延时达到了cluster_node_timeout / 2,那么立即发送ping,避免数据交换延时过长,落后的时间太长了
比如说,两个节点之间都10分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题,所以cluster_node_timeout可以调节,如果调节比较大,那么会降低发送的频率,每次ping,一个是带上自己节点的信息,还有就是带上1/10其他节点的信息,发送出去,进行数据交换,至少包含3个其他节点的信息,最多包含总节点-2个其他节点的信息
面向集群的Jedis内部实现原理
开发,jedis,redis的java client客户端,redis cluster,jedis cluster api
jedis cluster api与redis cluster集群交互的一些基本原理
1、基于重定向的客户端
redis-cli -c,自动重定向
(1)请求重定向
客户端可能会挑选任意一个redis实例去发送命令,每个redis实例接收到命令,都会计算key对应的hash slot
如果在本地就在本地处理,否则返回moved给客户端,让客户端进行重定向
cluster keyslot mykey,可以查看一个key对应的hash slot是什么
用redis-cli的时候,可以加入-c参数,支持自动的请求重定向,redis-cli接收到moved之后,会自动重定向到对应的节点执行命令
(2)计算hash slot
计算hash slot的算法,就是根据key计算CRC16值,然后对16384取模,拿到对应的hash slot
用hash tag可以手动指定key对应的slot,同一个hash tag下的key,都会在一个hash slot中,比如set mykey1:{100}和set mykey2:{100}
(3)hash slot查找
节点间通过gossip协议进行数据交换,就知道每个hash slot在哪个节点上
2、smart jedis
(1)什么是smart jedis
基于重定向的客户端,很消耗网络IO,因为大部分情况下,可能都会出现一次请求重定向,才能找到正确的节点
所以大部分的客户端,比如java redis客户端,就是jedis,都是smart的
本地维护一份hashslot -> node的映射表,缓存,大部分情况下,直接走本地缓存就可以找到hashslot -> node,不需要通过节点进行moved重定向
(2)JedisCluster的工作原理
在JedisCluster初始化的时候,就会随机选择一个node,初始化hashslot -> node映射表,同时为每个节点创建一个JedisPool连接池
每次基于JedisCluster执行操作,首先JedisCluster都会在本地计算key的hashslot,然后在本地映射表找到对应的节点
如果那个node正好还是持有那个hashslot,那么就ok; 如果说进行了reshard这样的操作,可能hashslot已经不在那个node上了,就会返回moved
如果JedisCluter API发现对应的节点返回moved,那么利用该节点的元数据,更新本地的hashslot -> node映射表缓存
重复上面几个步骤,直到找到对应的节点,如果重试超过5次,那么就报错,JedisClusterMaxRedirectionException
jedis老版本,可能会出现在集群某个节点故障还没完成自动切换恢复时,频繁更新hash slot,频繁ping节点检查活跃,导致大量网络IO开销
jedis最新版本,对于这些过度的hash slot更新和ping,都进行了优化,避免了类似问题
(3)hashslot迁移和ask重定向
如果hash slot正在迁移,那么会返回ask重定向给jedis
jedis接收到ask重定向之后,会重新定位到目标节点去执行,但是因为ask发生在hash slot迁移过程中,所以JedisCluster API收到ask是不会更新hashslot本地缓存
已经可以确定说,hashslot已经迁移完了,moved是会更新本地hashslot->node映射表缓存的
高可用性与主备切换原理
redis cluster的高可用的原理,几乎跟哨兵是类似的
1、判断节点宕机
如果一个节点认为另外一个节点宕机,那么就是pfail,主观宕机,如果多个节点都认为另外一个节点宕机了,那么就是fail,客观宕机,跟哨兵的原理几乎一样,sdown,odown,在cluster-node-timeout内,某个节点一直没有返回pong,那么就被认为pfail,如果一个节点认为某个节点pfail了,那么会在gossip ping消息中,ping给其他节点,如果超过半数的节点都认为pfail了,那么就会变成fail
2、从节点过滤
对宕机的master node,从其所有的slave node中,选择一个切换成master node,检查每个slave node与master node断开连接的时间,如果超过了cluster-node-timeout * cluster-slave-validity-factor,那么就没有资格切换成master,这个也是跟哨兵是一样的,从节点超时过滤的步骤
3、从节点选举
哨兵:对所有从节点进行排序,slave priority,offset,run id,每个从节点,都根据自己对master复制数据的offset,来设置一个选举时间,offset越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举,所有的master node开始slave选举投票,给要进行选举的slave进行投票,如果大部分master node(N/2 + 1)都投票给了某个从节点,那么选举通过,那个从节点可以切换成master,从节点执行主备切换,从节点切换为主节点
4、与哨兵比较
整个流程跟哨兵相比,非常类似,所以说,redis cluster功能强大,直接集成了replication和sentinal的功能
缓存雪崩和缓存穿透?
面试题
了解什么是redis的雪崩和穿透?redis崩溃之后会怎么样?系统该如何应对这种情况?如何处理redis的穿透?
剖析
其实这是问到缓存必问的,因为缓存雪崩和穿透,那是缓存最大的两个问题,要么不出现,一旦出现就是致命性的问题。所以面试官一定会问你
缓存雪崩发生的现象
因为缓存宕机,大量的请求打入数据库,导致整个系统宕机
如何解决缓存雪崩
缓存雪崩的事前事中事后的解决方案
事前:redis高可用,主从+哨兵,redis cluster,避免全盘崩溃
事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL被打死
事后:redis持久化,快速恢复缓存数据,一般重启,自动从磁盘上加载数据恢复内存中的数据。
缓存穿透的现象
缓存穿透也就是,由黑客发出的非法请求,请求大量的无效key,导致无法命中缓存,同时数据库也查询不到,最终导致缓存穿透把数据库打死了。
如何解决缓存穿透
解决方案,每次系统从系统库只要没有查询到,就写一个空值到缓存中查找。
如何保证缓存与数据库的双写一致性?
只要用到了缓存,就可能会涉及到缓存与数据库双存储双写,就一定会有数据一致性的问题,那么你如何解决一致性问题呢
Cache Aside Pattern
最经典的缓存+数据库读写的模式,cache aside pattern
(1)读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应
(2)更新的时候,先删除缓存,然后再更新数据库
为什么是删除缓存,而不是更新缓存呢?
原因很简单,很多时候,复杂点的缓存的场景,因为缓存有的时候,不简单是数据库中直接取出来的值
商品详情页的系统,修改库存,只是修改了某个表的某些字段,但是要真正把这个影响的最终的库存计算出来,可能还需要从其他表查询一些数据,然后进行一些复杂的运算,才能最终计算出
现在最新的库存是多少,然后才能将库存更新到缓存中去,比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据,并进行运算,才能计算出缓存最新的值的
更新缓存的代价是很高的,是不是说,每次修改数据库的时候,都一定要将其对应的缓存去更新一份?也许有的场景是这样的,但是对于比较复杂的缓存数据计算的场景,就不是这样了
如果你频繁修改一个缓存涉及的多个表,那么这个缓存会被频繁的更新,频繁的更新缓存,但是问题在于,这个缓存到底会不会被频繁访问到???
举个例子,一个缓存涉及的表的字段,在1分钟内就修改了20次,或者是100次,那么缓存更新20次,100次; 但是这个缓存在1分钟内就被读取了1次,有大量的冷数据
28法则,黄金法则,20%的数据,占用了80%的访问量
实际上,如果你只是删除缓存的话,那么1分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低
每次数据过来,就只是删除缓存,然后修改数据库,如果这个缓存,在1分钟内只是被访问了1次,那么只有那1次,缓存是要被重新计算的,用缓存才去算缓存
其实删除缓存,而不是更新缓存,就是一个lazy计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算
mybatis,hibernate,懒加载,思想
查询一个部门,部门带了一个员工的list,没有必要说每次查询部门,都里面的1000个员工的数据也同时查出来啊
80%的情况,查这个部门,就只是要访问这个部门的信息就可以了
先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询1000个员工
数据库双写不一致问题分析与解决方案设计
从哪一步开始做,从比较简单的那一块开始做,实时性要求比较高的那块数据的缓存去做,实时性比较高的数据缓存,选择的就是库存的服务,库存可能会修改,每次修改都要去更新这个缓存数据; 每次库存的数据,在缓存中一旦过期,或者是被清理掉了,前端的nginx服务都会发送请求给库存服务,去获取相应的数据,库存这一块,写数据库的时候,直接更新redis缓存,实际上没有这么的简单,这里,其实就涉及到了一个问题,数据库与缓存双写,数据不一致的问题,围绕和结合实时性较高的库存服务,把数据库与缓存双写不一致问题以及其解决方案
最初级的缓存不一致问题
问题:先修改数据库,再删除缓存,如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据出现不一致,解决思路是:先删除缓存,再修改数据库,如果删除缓存成功了,如果修改数据库失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致,因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中
假设删除缓存成功,但是更新数据库失败了,那么不会出现双写不一致的问题
复杂的数据不一致问题
数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改
一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中
数据变更的程序完成了数据库的修改
完了,数据库和缓存中的数据不一样了。。。。
上亿流量高并发场景下,缓存会出现这个问题?
只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题
其实如果说你的并发量很低的话,特别是读并发很低,每天访问量就1万次,那么很少的情况下,会出现刚才描述的那种最初不一致的场景。
但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况。
数据库与缓存更新与读取操作进行异步串行化
更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个jvm内部的队列中,读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个jvm内部的队列中,一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行
这样的话,一个数据变更的操作,先执行,删除缓存,然后再去更新数据库,但是还没完成更新
此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成
这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可
待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中
如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回; 如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值
假设出现了数据库没有这条数据的场景时:
这个时候需要判断一下,内存队列中有没有数据库更新操作,如果没有数据库更新操作,说明这条数据压根可能就是空的,那么不用hang住,直接就返回空。
更新与读取请求串行化缺点
一般来说,如果你的系统不是严格要求缓存 + 数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要使用这个方案,因为读请求 和 写请求 串行化,串到一个内存队列中去,这样就可以保证一定不会出现不一致的情况。
串行化之后,就会导致系统的吞吐量大幅度的降低,用比较正常情况下多几倍的机器去支撑线上的请求。
高并发的场景下,该解决方案要注意的问题
读请求长时阻塞
由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回
该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库
务必通过一些模拟真实的测试,看看更新数据的频繁是怎样的
另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作
如果一个内存队列里居然会挤压100个商品的库存修改操作,每隔库存修改操作要耗费10ms区完成,那么最后一个商品的读请求,可能等待10 * 100 = 1000ms = 1s后,才能得到数据
这个时候就导致读请求的长时阻塞
一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会hang多少时间,如果读请求在200ms返回,如果你计算过后,哪怕是最繁忙的时候,积压10个更新操作,最多等待200ms,那还可以的
如果一个内存队列可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少
其实根据之前的项目经验,一般来说数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的
针对读高并发,读缓存架构的项目,一般写请求相对读来说,是非常非常少的,每秒的QPS能到几百就不错了
一秒,500的写操作,5份,每200ms,就100个写操作
单机器,20个内存队列,每个内存队列,可能就积压5个写操作,每个写操作性能测试后,一般在20ms左右就完成
那么针对每个内存队列中的数据的读请求,也就最多hang一会儿,200ms以内肯定能返回了
写QPS扩大10倍,但是经过刚才的测算,就知道,单机支撑写QPS几百没问题,那么就扩容机器,扩容10倍的机器,10台机器,每个机器20个队列,200个队列
大部分的情况下,应该是这样的,大量的读请求过来,都是直接走缓存取到数据的
少量情况下,可能遇到读跟数据更新冲突的情况,如上所述,那么此时更新操作如果先入队列,之后可能会瞬间来了对这个数据大量的读请求,但是因为做了去重的优化,所以也就一个更新缓存的操作跟在它后面
等数据更新完了,读请求触发的缓存更新操作也完成,然后临时等待的读请求全部可以读到缓存中的数据
读请求并发量过高
这里还必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时hang在服务上,看服务能不能抗的住,需要多少机器才能抗住最大的极限情况的峰值
但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大
按1:99的比例计算读和写的请求,每秒5万的读QPS,可能只有500次更新操作
如果一秒有500的写QPS,那么要测算好,可能写操作影响的数据有500条,这500条数据在缓存中失效后,可能导致多少读请求,发送读请求到库存服务来,要求更新缓存
一般来说,1:1,1:2,1:3,每秒钟有1000个读请求,会hang在库存服务上,每个读请求最多hang多少时间,200ms就会返回
在同一时间最多hang住的可能也就是单机200个读请求,同时hang住
单机hang200个读请求,还是ok的
1:20,每秒更新500条数据,这500秒数据对应的读请求,会有20 * 500 = 1万
1万个读请求全部hang在库存服务上,就死定了
多服务实例部署的请求路由
可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过nginx服务器路由到相同的服务实例上
热点商品的路由问题,导致请求的倾斜
万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能造成某台机器的压力过大
就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以更新频率不是太高的话,这个问题的影响并不是特别大,但是的确可能某些机器的负载会高一些
Redis并发竞争的问题是什么?
这个也是线上非常常见的一个问题,就是多客户端同时并发写一个key,可能本来应该先到的数据后到了,导致数据版本错了。或者是多客户端同时获取一个key,修改值之后再写回去,只要顺序错了,数据就错了
如下图所示:我们有好几个系统同时取访问缓存,并且发起了一个写缓存的操作
- set V1、V2、V3、V4
- 我们期望的是它是有顺序的去执行,但是最后却没有顺序了
- 变成了:set V1、V3、V4、V2
- 一般解决这种问题,就是使用分布式锁
基于Zookeeper的分布式锁
分布式锁:确保同一时间,只有一个系统实例在操作某个key,别人都不需要读和写
这里修改的时候,就需要引入时间戳,因为写入缓存的数据,都是从mysql中查询出来的,都得写入mysql中,写入mysql的时候,是必须保存一个时间戳,同时查询的时候,也需要把时间戳也查询出来
v1 10.00.00
v2 10.00.01
v3 10.00.02
v4 10.00.03
每次要写入之前,首先判断一下当前这个value的时间戳是否比缓存的value的时间戳大,如果比缓存中的时间戳更大,那么就执行写入操作,如果更小,就不能用旧的数据覆盖新的数据。
Redis的事务
可以一次执行多个命令,本质是一组命令的集合。一个事务中的所有命令都会序列化,按顺序串行化的执行而不会被其他命令插入。Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
其中,Redis事务分为三个阶段
- 开始事务
- 命令入队
- 执行事务
正常执行
放弃事务
事务执行出错 - 命令错误
若在事务队列中存在命令性错误(类似于java编译性错误),则执行EXEC命令时,所有命令都不会执行
事务执行出错 - 抛异常
若在事务队列中存在语法性错误(类似于java的1/0的运行时异常),则执行EXEC命令时,其他正确命令会被执行,错误命令抛出异常
Watch使用
使用watch检测balance,事务期间balance数据未变动,事务执行成功
使用watch检测balance,在开启事务后(标注1处),在新窗口执行标注2中的操作,更改balance的值,模拟其他客户端在事务执行期间更改watch监控的数据,然后再执行标注1后命令,执行EXEC后,事务未成功执行。
一但执行 EXEC 开启事务的执行后,无论事务使用执行成功, WARCH 对变量的监控都将被取消。故当事务执行失败后,需重新执行WATCH命令对变量进行监控,并开启新的事务进行操作
watch指令类似于乐观锁,在事务提交时,如果watch监控的多个KEY中任何KEY的值已经被其他客户端更改,则使用EXEC执行事务时,事务队列将不会被执行,同时返回Nullmulti-bulk应答以通知调用者事务执行失败。
线上生产环境的Redis是怎么部署的?
看看你了解不了解你们公司的redis生产集群的部署架构,你的redis是主从架构?集群架构?用了哪种集群方案?有没有做高可用保证?有没有开启持久化机制确保可以进行数据恢复?线上redis给几个G的内存?设置了哪些参数?压测后你们redis集群承载多少QPS?
剖析
redis cluster,10台机器,5台机器部署了redis主实例,另外5台机器部署了redis的从实例,每个主实例挂了一个从实例,5个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒5万,5台机器最多是25万读写请求/s。
机器是什么配置?32G内存+8核CPU+1T磁盘,但是分配给redis进程的是10g内存,一般线上生产环境,redis的内存尽量不要超过10g,超过10g可能会有问题。
5台机器对外提供读写,一共有50g内存。因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis从实例会自动变成主实例继续提供读写服务
你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是10kb。100条数据是1mb,10万条数据是1g。常驻内存的是200万条商品数据,占用内存是20g,仅仅不到总内存的50%。
目前高峰期每秒就是3500左右的请求量,比如我们吧,大型的公司,其实基础架构的team,会负责缓存集群的运维
总结
说实话,这一套东西基本构成了缓存这块你必须知道的基础性的知识,如果你不知道,那么说明你有点失职,确实平时没好好积累。
因为这些问题确实不难,如果我往深了问,可以问的很细,结合项目扣的很细,比如你们公司线上系统高峰QPS 3000?那请求主要访问哪些接口?redis抗了多少请求?mysql抗了多少请求?你到底是怎么实现高并发的?咱们聊聊redis的内核吧,看看你对底层了解的多么?如果要缓存几百GB的数据会有什么坑该这么弄?如果缓存出现热点现象该这么处理?某个value特别大把网卡给打死了怎么办?等等等等,可以深挖的东西其实有很多。。。。。
但是如果你掌握好了这套东西的回答,那么你在面试的时候,如果面试官没有全都问到,你可以自己主动合盘脱出。比如你可以说,我们线上的缓存,做了啥啥机制,防止雪崩、防止穿透、保证双写时的数据一致性、保证并发竞争时的数据一致性,我们线上咋部署的,啥架构,怎么玩儿的。这套东西你可以自己说出来,展示一下你对缓存这块的掌握。
6.分布式系统的面试连环炮
面试题
- 什么是分布式系统?
- 为什么要进行系统拆分?如何进行系统拆分?拆分后不用dubbo可以吗?dubbo和thrift有什么区别呢?
- 分布式服务框架
- Dubbo的工作原理?
- Dubbo支持哪些通信协议?
- Dubbo负载均衡策略和集群容错策略?
- Dubbo的SPI思想是什么?
什么是分布式系统
现在互联网公司,一般都是做分布式的系统,大家都不是做底层的分布式系统,分布式存储系统,hadoop hdfs,分布式计算系统,hadoop mapreduce,spark,分布式流式计算系统,storm。
分布式业务系统,把原来用java开发的一个大块系统,给拆分成多个子系统,多个子系统之间互相调用,形成一个大系统的整体。假设原来你做了一个OA系统,里面包含了权限模块、员工模块、请假模块、财务模块,一个工程,里面包含了一堆模块,模块与模块之间会互相去调用,1台机器部署。
现在如果你把他这个系统给拆开,权限系统,员工系统,请假系统,财务系统,4个系统,4个工程,分别在4台机器上部署
一个请求过来,完成这个请求,这个员工系统,调用权限系统,调用请假系统,调用财务系统,4个系统分别完成了一部分的事情,最后4个系统都干完了以后,才认为是这个请求已经完成了
单体架构与分布式架构:
为什么要把系统拆分成分布式?为什么要使用Dubbo?
发展变迁
早些年,我印象中在2010年初的时候,整个IT行业,很少有人谈分布式,更不用说微服务,虽然很多BAT等大型公司,因为系统的复杂性,很早就是分布式架构,大量的服务,只不过微服务大多基于自己搞的一套框架来实现而已。
但是确实,那个年代,大家很重视ssh2,很多中小型公司几乎大部分都是玩儿struts2、spring、hibernate,稍晚一些,才进入了spring mvc、spring、mybatis的组合。那个时候整个行业的技术水平就是那样,当年oracle很火,oracle管理员很吃香,oracle性能优化啥的都是IT男的大杀招啊。连大数据都没人提,当年OCP、OCM等认证培训机构,火的不行。
但是确实随着时代的发展,慢慢的,很多公司开始接受分布式系统架构了,这里面尤为对行业有至关重要影响的,是阿里的dubbo,某种程度上而言,阿里在这里推动了行业技术的前进。
正是因为有阿里的dubbo,很多中小型公司才可以基于dubbo,来把系统拆分成很多的服务,每个人负责一个服务,大家的代码都没有冲突,服务可以自治,自己选用什么技术都可以,每次发布如果就改动一个服务那就上线一个服务好了,不用所有人一起联调,每次发布都是几十万行代码,甚至几百万行代码了。
直到今日,我很高兴的看到分布式系统都成行业面试标配了,任何一个普通的程序员都该掌握这个东西,其实这是行业的进步,也是所有IT码农的技术进步。所以既然分布式都成标配了,那么面试官当然会问了,因为很多公司现在都是分布式、微服务的架构,那面试官当然得考察考察你了。
为什么要将系统拆分成分布式?
假设不拆分
要是不拆分,一个大系统几十万行代码,20个人维护一份代码,简直是悲剧啊。代码经常改着改着就冲突了,各种代码冲突和合并要处理,非常耗费时间;经常我改动了我的代码,你调用了我,导致你的代码也得重新测试,麻烦的要死;然后每次发布都是几十万行代码的系统一起发布,大家得一起提心吊胆准备上线,几十万行代码的上线,可能每次上线都要做很多的检查,很多异常问题的处理,简直是又麻烦又痛苦;而且如果我现在打算把技术升级到最新的spring版本,还不行,因为这可能导致你的代码报错,我不敢随意乱改技术。
假设一个系统是20万行代码,其中小A在里面改了1000行代码,但是此时发布的时候是这个20万行代码的大系统一块儿发布。就意味着20万上代码在线上就可能出现各种变化,20个人,每个人都要紧张地等在电脑面前,上线之后,检查日志,看自己负责的那一块儿有没有什么问题。
小A就检查了自己负责的1万行代码对应的功能,确保ok就闪人了;结果不巧的是,小A上线的时候不小心修改了线上机器的某个配置,导致另外小B和小C负责的2万行代码对应的一些功能,出错了
几十个人负责维护一个几十万行代码的单块应用,每次上线,准备几个礼拜,上线 -> 部署 -> 检查自己负责的功能
最近从2013年到现在,5年的时间里,2013年以前,基本上都是BAT的天下;2013年开始,有几个小巨头开始快速的发展,上市,几百亿美金,估值都几百亿美金;2015年,出现了除了BAT以外,又有几个互联网行业的小巨头出现。
有某一个小巨头,现在估值几百亿美金的小巨头,5年前刚开始搞的时候,核心的业务,几十个人,维护一个单块的应用,维护单块的应用,在从0到1的环节里,是很合适的,因为那个时候,是系统都没上线,没什么技术挑战,大家有条不紊的开发。ssh + mysql + tomcat,可能会部署几台机器吧。
结果不行了,后来系统上线了,业务快速发展,10万用户 -> 100万用户 -> 1000万用户 -> 上亿用户了
拆分后
拆分了以后,整个世界清爽了,几十万行代码的系统,拆分成20个服务,平均每个服务就1~2万行代码,每个服务部署到单独的机器上。20个工程,20个git代码仓库里,20个码农,每个人维护自己的那个服务就可以了,是自己独立的代码,跟别人没关系。再也没有代码冲突了,爽。每次就测试我自己的代码就可以了,爽。每次就发布我自己的一个小服务就可以了。技术上想怎么升级就怎么升级,保持接口不变就可以了
所以简单来说,一句话总结,如果是那种代码量多达几十万行的中大型项目,团队里有几十个人,那么如果不拆分系统,开发效率极其低下,问题很多。但是拆分系统之后,每个人就负责自己的一小部分就好了,可以随便玩儿随便弄。分布式系统拆分之后,可以大幅度提升复杂系统大型团队的开发效率。
但是同时,也要提醒的一点是,系统拆分成分布式系统之后,大量的分布式系统面临的问题也是接踵而来,所以后面的问题都是在围绕分布式系统带来的复杂技术挑战在说。
如何进行系统的拆分
这个问题说大可以很大,可以扯到领域驱动模型设计上去,说小了也很小,我不太想给大家太过于学术的说法,因为你也不可能背这个答案,过去了直接说吧。还是说的简单一点,大家自己到时候知道怎么回答就行了。
系统拆分分布式系统,拆成多个服务,拆成微服务的架构,拆很多轮的。上来一个架构师第一轮就给拆好了,第一轮;团队继续扩大,拆好的某个服务,刚开始是1个人维护1万行代码,后来业务系统越来越复杂,这个服务是10万行代码,5个人;第二轮,1个服务 -> 5个服务,每个服务2万行代码,每人负责一个服务
如果是多人维护一个服务,<=3个人维护这个服务;最理想的情况下,几十个人,1个人负责1个或2~3个服务;某个服务工作量变大了,代码量越来越多,某个同学,负责一个服务,代码量变成了10万行了,他自己不堪重负,他现在一个人拆开,5个服务,1个人顶着,负责5个人,接着招人,2个人,给那个同学带着,3个人负责5个服务,其中2个人每个人负责2个服务,1个人负责1个服务
我个人建议,一个服务的代码不要太多,1万行左右,两三万撑死了吧
大部分的系统,是要进行多轮拆分的,第一次拆分,可能就是将以前的多个模块该拆分开来了,比如说将电商系统拆分成订单系统、商品系统、采购系统、仓储系统、用户系统,等等吧。
但是后面可能每个系统又变得越来越复杂了,比如说采购系统里面又分成了供应商管理系统、采购单管理系统,订单系统又拆分成了购物车系统、价格系统、订单管理系统。
扯深了实在很深,所以这里先给大家举个例子,你自己感受一下,核心意思就是根据情况,先拆分一轮,后面如果系统更复杂了,可以继续分拆。你根据自己负责系统的例子,来考虑一下就好了。
拆分后不用dubbo可以吗?
当然可以了,大不了最次,就是各个系统之间,直接基于spring mvc,就纯http接口互相通信呗,还能咋样。但是这个肯定是有问题的,因为http接口通信维护起来成本很高,你要考虑超时重试、负载均衡等等各种乱七八糟的问题,比如说你的订单系统调用商品系统,商品系统部署了5台机器,你怎么把请求均匀地甩给那5台机器?这不就是负载均衡?你要是都自己搞那是可以的,但是确实很痛苦。
所以dubbo说白了,是一种rpc框架,就是本地就是进行接口调用,但是dubbo会代理这个调用请求,跟远程机器网络通信,给你处理掉负载均衡了、服务实例上下线自动感知了、超时重试了,等等乱七八糟的问题。那你就不用自己做了,用dubbo就可以了。
Dubbo的工作原理是啥?
工作原理
- 第一层:service层,接口层,给服务提供者和消费者来实现的
- 第二层:config层,配置层,主要是对dubbo进行各种配置的
- 第三层:proxy层,服务代理层,透明生成客户端的stub和服务单的skeleton
- 第四层:registry层,服务注册层,负责服务的注册与发现
- 第五层:cluster层,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务
- 第六层:monitor层,监控层,对rpc接口的调用次数和调用时间进行监控
- 第七层:protocol层,远程调用层,封装rpc调用
- 第八层:exchange层,信息交换层,封装请求响应模式,同步转异步
- 第九层:transport层,网络传输层,抽象mina和netty为统一接口
- 第十层:serialize层,数据序列化层
工作流程
- 第一步,provider向注册中心去注册
- 第二步,consumer从注册中心订阅服务,注册中心会通知consumer注册好的服务
- 第三步,consumer调用provider
- 第四步,consumer和provider都异步的通知监控中心
注册中心宕机了后可以继续通信么?
可以,因为刚开始初始化的时候,消费者会将提供者的地址等信息拉取到本地缓存,所以注册中心挂了可以继续通信
Dubbo支持哪些通信协议?支持哪些序列化协议?
Dubbo通信协议
dubbo协议
dubbo://192.168.0.1:20188
默认就是走dubbo协议的,单一长连接,NIO异步通信,基于hessian作为序列化协议
适用的场景就是:传输数据量很小(每次请求在100kb以内),但是并发量很高
为了要支持高并发场景,一般是服务提供者就几台机器,但是服务消费者有上百台,可能每天调用量达到上亿次!此时用长连接是最合适的,就是跟每个服务消费者维持一个长连接就可以,可能总共就100个连接。然后后面直接基于长连接NIO异步通信,可以支撑高并发请求。
否则如果上亿次请求每次都是短连接的话,服务提供者会扛不住。
而且因为走的是单一长连接,所以传输数据量太大的话,会导致并发能力降低。所以一般建议是传输数据量很小,支撑高并发访问。
rmi协议
走java二进制序列化,多个短连接,适合消费者和提供者数量差不多,适用于文件的传输,一般较少用
hessian协议
走hessian序列化协议,多个短连接,适用于提供者数量比消费者数量还多,适用于文件的传输,一般较少用
http议
走json序列化
webservice
走SOAP文本序列化
dubbo支持的序列化协议
所以dubbo实际基于不同的通信协议,支持hessian、java二进制序列化、json、SOAP文本序列化多种序列化协议。但是hessian是其默认的序列化协议。
dubbo负载均衡策略和集群容错策略都有哪些?
dubbo负载均衡策略
random loadbalance
默认情况下,dubbo是random load balance随机调用实现负载均衡,可以对provider不同实例设置不同的权重,会按照权重来负载均衡,权重越大分配流量越高,一般就用这个默认的就可以了。
roundrobin loadbalance
还有roundrobin loadbalance,这个的话默认就是均匀地将流量打到各个机器上去,但是如果各个机器的性能不一样,容易导致性能差的机器负载过高。所以此时需要调整权重,让性能差的机器承载权重小一些,流量少一些。
跟运维同学申请机器,有的时候,我们运气,正好公司资源比较充足,刚刚有一批热气腾腾,刚刚做好的一批虚拟机新鲜出炉,配置都比较高。8核+16g,机器,2台。过了一段时间,我感觉2台机器有点不太够,我去找运维同学,哥儿们,你能不能再给我1台机器,4核+8G的机器。我还是得要。
leastactive loadbalance
这个就是自动感知一下,如果某个机器性能越差,那么接收的请求越少,越不活跃,此时就会给不活跃的性能差的机器更少的请求
consistanthash loadbalance
一致性Hash算法,相同参数的请求一定分发到一个provider上去,provider挂掉的时候,会基于虚拟节点均匀分配剩余的流量,抖动不会太大。如果你需要的不是随机负载均衡,是要一类请求都到一个节点,那就走这个一致性hash策略。
dubbo集群容错策略
failover cluster模式
失败自动切换,自动重试其他机器,默认就是这个,常见于读操作
failfast cluster模式
一次调用失败就立即失败,常见于写操作
failsafe cluster模式
出现异常时忽略掉,常用于不重要的接口调用,比如记录日志
failbackc cluster模式
失败了后台自动记录请求,然后定时重发,比较适合于写消息队列这种
forking cluster
并行调用多个provider,只要一个成功就立即返回
broadcacst cluster
逐个调用所有的provider
dubbo动态代理策略
默认使用javassist动态字节码生成,创建代理类
但是可以通过spi扩展机制配置自己的动态代理策略
Dubbo的SPI思想是什么?
spi,简单来说,就是service provider interface,说白了是什么意思呢,比如你有个接口,现在这个接口有3个实现类,那么在系统运行的时候对这个接口到底选择哪个实现类呢?这就需要spi了,需要根据指定的配置或者是默认的配置,去找到对应的实现类加载进来,然后用这个实现类的实例对象。
接口A -> 实现A1,实现A2,实现A3
配置一下,接口A = 实现A2
在系统实际运行的时候,会加载你的配置,用实现A2实例化一个对象来提供服务
比如说你要通过jar包的方式给某个接口提供实现,然后你就在自己jar包的META-INF/services/目录下放一个跟接口同名的文件,里面指定接口的实现里是自己这个jar包里的某个类。ok了,别人用了一个接口,然后用了你的jar包,就会在运行的时候通过你的jar包的那个文件找到这个接口该用哪个实现类。
这是jdk提供的一个功能。
比如说你有个工程A,有个接口A,接口A在工程A里是没有实现类的 -> 系统在运行的时候,怎么给接口A选择一个实现类呢?
你就可以自己搞一个jar包,META-INF/services/,放上一个文件,文件名就是接口名,接口A,接口A的实现类=com.zhss.service.实现类A2。让工程A来依赖你的这个jar包,然后呢在系统运行的时候,工程A跑起来,对接口A,就会扫描自己依赖的所有的jar包,在每个jar里找找,有没有META-INF/services文件夹,如果有,在里面找找,有没有接口A这个名字的文件,如果有在里面找一下你指定的接口A的实现是你的jar包里的哪个类?
SPI机制,一般来说用在哪儿?插件扩展的场景,比如说你开发的是一个给别人使用的开源框架,如果你想让别人自己写个插件,插到你的开源框架里面来,扩展某个功能。
经典的思想体现,大家平时都在用,比如说jdbc
java定义了一套jdbc的接口,但是java是没有提供jdbc的实现类
但是实际上项目跑的时候,要使用jdbc接口的哪些实现类呢?一般来说,我们要根据自己使用的数据库,比如msyql,你就将mysql-jdbc-connector.jar,引入进来;oracle,你就将oracle-jdbc-connector.jar,引入进来。
在系统跑的时候,碰到你使用jdbc的接口,他会在底层使用你引入的那个jar中提供的实现类
但是dubbo也用了spi思想,不过没有用jdk的spi机制,是自己实现的一套spi机制。
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
Protocol接口,dubbo要判断一下,在系统运行的时候,应该选用这个Protocol接口的哪个实现类来实例化对象来使用呢?
他会去找一个你配置的Protocol,他就会将你配置的Protocol实现类,加载到jvm中来,然后实例化对象,就用你的那个Protocol实现类就可以了
微内核,可插拔,大量的组件,Protocol负责rpc调用的东西,你可以实现自己的rpc调用组件,实现Protocol接口,给自己的一个实现类即可。
这行代码就是dubbo里大量使用的,就是对很多组件,都是保留一个接口和多个实现,然后在系统运行的时候动态根据配置去找到对应的实现类。如果你没配置,那就走默认的实现好了,没问题。
@SPI("dubbo")
public interface Protocol {
int getDefaultPort();
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
@Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
void destroy();
}
在dubbo自己的jar里,在/META_INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol文件中:
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
http=com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
所以说,这就看到了dubbo的spi机制默认是怎么玩儿的了,其实就是Protocol接口,@SPI(“dubbo”)说的是,通过SPI机制来提供实现类,实现类是通过dubbo作为默认key去配置文件里找到的,配置文件名称与接口全限定名一样的,通过dubbo作为key可以找到默认的实现了就是com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol。
dubbo的默认网络通信协议,就是dubbo协议,用的DubboProtocol
如果想要动态替换掉默认的实现类,需要使用@Adaptive接口,Protocol接口中,有两个方法加了@Adaptive注解,就是说那俩接口会被代理实现。
比如这个Protocol接口搞了俩@Adaptive注解标注了方法,在运行的时候会针对Protocol生成代理类,这个代理类的那俩方法里面会有代理代码,代理代码会在运行的时候动态根据url中的protocol来获取那个key,默认是dubbo,你也可以自己指定,你如果指定了别的key,那么就会获取别的实现类的实例了。
通过这个url中的参数不通,就可以控制动态使用不同的组件实现类
好吧,那下面来说说怎么来自己扩展dubbo中的组件
自己写个工程,要是那种可以打成jar包的,里面的src/main/resources目录下,搞一个META-INF/services,里面放个文件叫:com.alibaba.dubbo.rpc.Protocol,文件里搞一个my=com.zhss.MyProtocol。自己把jar弄到nexus私服里去。
然后自己搞一个dubbo provider工程,在这个工程里面依赖你自己搞的那个jar,然后在spring配置文件里给个配置:
<dubbo:protocol name=”my” port=”20000” />
这个时候provider启动的时候,就会加载到我们jar包里的my=com.zhss.MyProtocol这行配置里,接着会根据你的配置使用你定义好的MyProtocol了,这个就是简单说明一下,你通过上述方式,可以替换掉大量的dubbo内部的组件,就是扔个你自己的jar包,然后配置一下即可。
dubbo里面提供了大量的类似上面的扩展点,就是说,你如果要扩展一个东西,只要自己写个jar,让你的consumer或者是provider工程,依赖你的那个jar,在你的jar里指定目录下配置好接口名称对应的文件,里面通过key=实现类。
然后对对应的组件,用类似dubbo:protocol用你的哪个key对应的实现类来实现某个接口,你可以自己去扩展dubbo的各种功能,提供你自己的实现。
基于Dubbo如何做服务治理、服务降级和重试
服务治理,这个问题如果问你,其实就是看看你有没有服务治理的思想,因为这个是做过复杂微服务的人肯定会遇到的一个问题。
服务降级,这个是涉及到复杂分布式系统中必备的一个话题,因为分布式系统互相来回调用,任何一个系统故障了,你不降级,直接就全盘崩溃?那就太坑爹了吧
失败重试,分布式系统中网络请求如此频繁,要是因为网络问题不小心失败了一次,是不是要重试?
超时重试,同上,如果不小心网络慢一点,超时了,如何重试?
- dubbo工作原理:服务注册,注册中心,消费者,代理通信,负载均衡
- 网络通信、序列化:dubbo协议,长连接,NIO,hessian序列化协议
- 负载均衡策略,集群容错策略,动态代理策略:dubbo跑起来的时候一些功能是如何运转的,怎么做负载均衡?怎么做集群容错?怎么生成动态代理?
- dubbo SPI机制:你了解不了解dubbo的SPI机制?如何基于SPI机制对dubbo进行扩展?
- dubbo的服务治理、降级、重试
服务治理
调用链路自动生成
一个大型的分布式系统,或者说是用现在流行的微服务架构来说吧,分布式系统由大量的服务组成。那么这些服务之间互相是如何调用的?调用链路是啥?说实话,几乎到后面没人搞的清楚了,因为服务实在太多了,可能几百个甚至几千个服务。
那就需要基于dubbo做的分布式系统中,对各个服务之间的调用自动记录下来,然后自动将各个服务之间的依赖关系和调用链路生成出来,做成一张图,显示出来,大家才可以看到对吧。
服务A -> 服务B -> 服务C
-> 服务E
-> 服务D
-> 服务F
-> 服务W
服务访问压力以及时长统计
需要自动统计各个接口和服务之间的调用次数以及访问延时,而且要分成两个级别。一个级别是接口粒度,就是每个服务的每个接口每天被调用多少次,TP50,TP90,TP99,三个档次的请求延时分别是多少;第二个级别是从源头入口开始,一个完整的请求链路经过几十个服务之后,完成一次请求,每天全链路走多少次,全链路请求延时的TP50,TP90,TP99,分别是多少。
这些东西都搞定了之后,后面才可以来看当前系统的压力主要在哪里,如何来扩容和优化啊
其他的
服务分层(避免循环依赖),调用链路失败监控和报警,服务鉴权,每个服务的可用性的监控(接口调用成功率?几个9)99.99%,99.9%,99%
服务降级
比如说服务A调用服务B,结果服务B挂掉了,服务A重试几次调用服务B,还是不行,直接降级,走一个备用的逻辑,给用户返回响应
public interface HelloService {
void sayHello();
}
public class HelloServiceImpl implements HelloService {
public void sayHello() {
System.out.println("hello world......");
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework/schema/beans"
xmlns:xsi="http://www.w3/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech/schema/dubbo"
xsi:schemaLocation="http://www.springframework/schema/beans http://www.springframework/schema/beans/spring-beans.xsd http://code.alibabatech/schema/dubbo http://code.alibabatech/schema/dubbo/dubbo.xsd">
<dubbo:application name="dubbo-provider" />
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:service interface="com.zhss.service.HelloService" ref="helloServiceImpl" timeout="10000" />
<bean id="helloServiceImpl" class="com.zhss.service.HelloServiceImpl" />
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework/schema/beans"
xmlns:xsi="http://www.w3/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech/schema/dubbo"
xsi:schemaLocation="http://www.springframework/schema/beans http://www.springframework/schema/beans/spring-beans.xsd http://code.alibabatech/schema/dubbo http://code.alibabatech/schema/dubbo/dubbo.xsd">
<dubbo:application name="dubbo-consumer" />
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<dubbo:reference id="fooService" interface="com.test.service.FooService" timeout="10000" check="false" mock="return null">
</dubbo:reference>
</beans>
现在就是mock,如果调用失败统一返回null
但是可以将mock修改为true,然后在跟接口同一个路径下实现一个Mock类,命名规则是接口名称加Mock后缀。然后在Mock类里实现自己的降级逻辑。
public class HelloServiceMock implements HelloService {
public void sayHello() {
// 降级逻辑
}
}
失败重试和超时重试
所谓失败重试,就是consumer调用provider要是失败了,比如抛异常了,此时应该是可以重试的,或者调用超时了也可以重试。
<dubbo:reference id="xxxx" interface="xx" check="true" async="false" retries="3" timeout="2000"/>
某个服务的接口,要耗费5s,你这边不能干等着,你这边配置了timeout之后,我等待2s,还没返回,我直接就撤了,不能等你
如果是超时了,timeout就会设置超时时间;如果是调用失败了自动就会重试指定的次数
你就结合你们公司的具体的场景来说说你是怎么设置这些参数的,timeout,一般设置为200ms,我们认为不能超过200ms还没返回
retries,3次,设置retries,还一般是在读请求的时候,比如你要查询个数据,你可以设置个retries,如果第一次没读到,报错,重试指定的次数,尝试再次读取2次
7.分布式系统幂等性与顺序性及分布式锁连环炮
分布式服务接口的幂等性如何设计
什么是幂等性
一个分布式系统中的某个接口,要保证幂等性,该如何保证?这个事儿其实是你做分布式系统的时候必须要考虑的一个生产环境的技术问题。啥意思呢?
你看,假如你有个服务提供一个接口,结果这服务部署在了5台机器上,接着有个接口就是付款接口。然后人家用户在前端上操作的时候,不知道为啥,总之就是一个订单不小心发起了两次支付请求,然后这俩请求分散在了这个服务部署的不同的机器上,好了,结果一个订单扣款扣两次?尴尬了。。。
或者是订单系统调用支付系统进行支付,结果不小心因为网络超时了,然后订单系统走了前面我们看到的那个重试机制,咔嚓给你重试了一把,好,支付系统收到一个支付请求两次,而且因为负载均衡算法落在了不同的机器上,尴尬了。。。
所以你肯定得知道这事儿,否则你做出来的分布式系统恐怕容易埋坑
网络问题很常见,100次请求,都ok;1万次,可能1次是超时会重试;10万,10次;100万,100次;如果有100个请求重复了,你没处理,导致订单扣款2次,100个订单都扣错了;每天被100个用户投诉;一个月被3000个用户投诉
我们之前生产就遇到过,是往数据库里写入数据,重复的请求,就导致我们的数据经常会错,出现一些重复数据,就会导致一些问题
如果是单机的环境,只需要维护一个map或者set即可,每次判断订单ID是否被支付过。
这个不是技术问题,这个没有通用的一个方法,这个是结合业务来看应该如何保证幂等性的,你的经验。
所谓幂等性,就是说一个接口,多次发起同一个请求,你这个接口得保证结果是准确的,比如不能多扣款,不能多插入一条数据,不能将统计值多加了1。这就是幂等性,不给大家来学术性词语了。
保证幂等性
其实保证幂等性主要是三点:
- 对于每个请求必须有一个唯一的标识,举个例子:订单支付请求,肯定得包含订单id,一个订单id最多支付一次,对吧
- 每次处理完请求之后,必须有一个记录标识这个请求处理过了,比如说常见的方案是在mysql中记录个状态啥的,比如支付之前记录一条这个订单的支付流水,而且支付流水采
- 每次接收请求需要进行判断之前是否处理过的逻辑处理,比如说,如果有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,orderId已经存在了,唯一键约束生效,报错插入不进去的。然后你就不用再扣款了。
- 上面只是给大家举个例子,实际运作过程中,你要结合自己的业务来,比如说用redis用orderId作为唯一键。只有成功插入这个支付流水,才可以执行实际的支付扣款。
要求是支付一个订单,必须插入一条支付流水,order_id建一个唯一键,unique key
所以你在支付一个订单之前,先插入一条支付流水,order_id就已经进去了
你就可以写一个标识到redis里面去,set order_id payed,下一次重复请求过来了,先查redis的order_id对应的value,如果是payed就说明已经支付过了,你就别重复支付了
然后呢,你再重复支付这个订单的时候,你写尝试插入一条支付流水,数据库给你报错了,说unique key冲突了,整个事务回滚就可以了
来保存一个是否处理过的标识也可以,服务的不同实例可以一起操作redis。
分布式服务接口请求的顺序如何保证?
其实分布式系统接口的调用顺序,也是个问题,一般来说是不用保证顺序的。但是有的时候可能确实是需要严格的顺序保证。给大家举个例子,你服务A调用服务B,先插入再删除。好,结果俩请求过去了,落在不同机器上,可能插入请求因为某些原因执行慢了一些,导致删除请求先执行了,此时因为没数据所以啥效果也没有;结果这个时候插入请求过来了,好,数据插入进去了,那就尴尬了。
本来应该是先插入 -> 再删除,这条数据应该没了,结果现在先删除 -> 再插入,数据还存在,最后你死都想不明白是怎么回事。所以这都是分布式系统一些很常见的问题
首先,一般来说,我个人给你的建议是,你们从业务逻辑上最好设计的这个系统不需要这种顺序性的保证,因为一旦引入顺序性保障,会导致系统复杂度上升,而且会带来效率低下,热点数据压力过大,等问题。
下面我给个我们用过的方案吧,简单来说,首先你得用dubbo的一致性hash负载均衡策略,将比如某一个订单id对应的请求都给分发到某个机器上去,接着就是在那个机器上因为可能还是多线程并发执行的,你可能得立即将某个订单id对应的请求扔一个内存队列里去,强制排队,这样来确保他们的顺序性。
但是这样引发的后续问题就很多,比如说要是某个订单对应的请求特别多,造成某台机器成热点怎么办?解决这些问题又要开启后续一连串的复杂技术方案。。。曾经这类问题弄的我们头疼不已,所以,还是建议什么呢?
最好是比如说刚才那种,一个订单的插入和删除操作,能不能合并成一个操作,就是一个删除,或者是什么,避免这种问题的产生。
采用MQ以及内存队列来解决
方式1,也是最友好的方式就是使用消息队列和内存队列来解决,首先我们需要做的就是把需要保证顺序的请求,通过Hash算法分发到特定的同一台机器上,然后机器内部在把请求放到内存队列中,线程从内存队列中获取消费,保证线程的顺序性
但是这种方式能解决99%的顺序性,但是接入服务还是可能存在问题,比如把请求 123,弄成231,导致送入MQ队列中顺序也不一致
采用分布式锁来解决
分布式锁能够保证强一致性,但是因为引入这种重量级的同步机制,会导致并发量急剧降低,因为需要频繁的获取锁,释放锁的操作。
如何设计一个类似Dubbo的RPC框架
遇到这类问题,起码从你了解的类似框架的原理入手,自己说说参照dubbo的原理,你来设计一下,举个例子,dubbo不是有那么多分层么?而且每个分层是干啥的,你大概是不是知道?那就按照这个思路大致说一下吧。
- 上来你的服务就得去注册中心注册吧,你是不是得有个注册中心,保留各个服务的信息,可以用zookeeper来做,对吧
- 然后你的消费者需要去注册中心拿对应的服务信息吧,对吧,而且每个服务可能会存在于多台机器上
- 接着你就该发起一次请求了,咋发起?蒙圈了是吧。当然是基于动态代理了,你面向接口获取到一个动态代理,这个动态代理就是接口在本地的一个代理,然后这个代理会找到服务对应的机器地址
- 然后找哪个机器发送请求?那肯定得有个负载均衡算法了,比如最简单的可以随机轮询是不是
- 接着找到一台机器,就可以跟他发送请求了,第一个问题咋发送?你可以说用netty了,nio方式;第二个问题发送啥格式数据?你可以说用hessian序列化协议了,或者是别的,对吧。然后请求过去了。。
- 服务器那边一样的,需要针对你自己的服务生成一个动态代理,监听某个网络端口了,然后代理你本地的服务代码。接收到请求的时候,就调用对应的服务代码,对吧。
说说Zookeeper的使用场景有哪些?
分布式锁这个东西,很常用的,你做java系统开发,分布式系统,可能会有一些场景会用到。最常用的分布式锁就是zookeeper来做分布式锁。
其实说实话,问这个问题,一般就是看看你是否了解zk,因为zk是分布式系统中很常见的一个基础系统。而且问的话常问的就是说zk的使用场景是什么?看你知道不知道一些基本的使用场景。但是其实zk挖深了自然是可以问的很深很深的。
分布式协调
这个其实是zk很经典的一个用法,简单来说,就好比,你A系统发送个请求到mq,然后B消息消费之后处理了。那A系统如何知道B系统的处理结果?用zk就可以实现分布式系统之间的协调工作。A系统发送请求之后可以在zk上对某个节点的值注册个监听器,一旦B系统处理完了就修改zk那个节点的值,A立马就可以收到通知,完美解决。
分布式锁
对某一个数据连续发出两个修改操作,两台机器同时收到了请求,但是只能一台机器先执行另外一个机器再执行。那么此时就可以使用zk分布式锁,一个机器接收到了请求之后先获取zk上的一把分布式锁,就是可以去创建一个znode,接着执行操作;然后另外一个机器也尝试去创建那个znode,结果发现自己创建不了,因为被别人创建了。。。。那只能等着,等第一个机器执行完了自己再执行。
元数据/配置信息管理
zk可以用作很多系统的配置信息的管理,比如kafka、storm等等很多分布式系统都会选用zk来做一些元数据、配置信息的管理,包括dubbo注册中心不也支持zk
HA高可用性
这个应该是很常见的,比如hadoop、hdfs、yarn等很多大数据系统,都选择基于zk来开发HA高可用机制,就是一个重要进程一般会做主备两个,主进程挂了立马通过zk感知到切换到备用进程
分布式锁
面试题
- 一般实现分布式锁都有哪些方式?
- 使用redis如何设计分布式锁?
- 使用zk来设计分布式锁可以吗?
- 这两种分布式锁的实现方式哪种效率比较高?
Redis实现分布式锁
官方叫做RedLock算法,是redis官方支持的分布式锁算法。
这个分布式锁有3个重要的考量点,互斥(只能有一个客户端获取锁),不能死锁,容错(大部分redis节点或者这个锁就可以加可以释放)
第一个最普通的实现方式,如果就是在redis里创建一个key算加锁
SET my:lock 随机值 NX PX 30000,这个命令就ok,这个的NX的意思就是只有key不存在的时候才会设置成功,PX 30000的意思是30秒后锁自动释放。别人创建的时候如果发现已经有了就不能加锁了。
释放锁就是删除key,但是一般可以用lua脚本删除,判断value一样才删除:
关于redis如何执行lua脚本,自行百度
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
为啥要用随机值呢?因为如果某个客户端获取到了锁,但是阻塞了很长时间才执行完,此时可能已经自动释放锁了,此时可能别的客户端已经获取到了这个锁,要是你这个时候直接删除key的话会有问题,所以得用随机值加上面的lua脚本来释放锁。
但是这样是肯定不行的。因为如果是普通的redis单实例,那就是单点故障。或者是redis普通主从,那redis主从异步复制,如果主节点挂了,key还没同步到从节点,此时从节点切换为主节点,别人就会拿到锁。
第二个问题,RedLock算法
- 这个场景是假设有一个redis cluster,有5个redis master实例。然后执行如下步骤获取一把锁:
- 获取当前时间戳,单位是毫秒
- 跟上面类似,轮流尝试在每个master节点上创建锁,过期时间较短,一般就几十毫秒
- 尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n / 2 +1)
- 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
- 要是锁建立失败了,那么就依次删除这个锁
- 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁
ZK实现分布式锁
zk分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。
ZK实现分布式锁,就是不需要执行轮询算法,而是注册监听器,但有人释放锁的时候,会通知需要获取锁的进程。
同时ZK获取锁的时候,其实就是创建了一个临时节点,如果这个临时节点之前不存在,那么就创建成功,也就是说这个锁就是属于该线程的。
同时其它的线程会尝试创建相同名称的一个临时节点,如果已经存在,说明别人已经占有了这把锁,那么就创建失败。
一旦临时节点被删除,zk就通知别人这个锁已经被释放掉了,相当于锁被释放掉了。
假设这个时候,持有锁的服务器宕机了,那么Zookeeper会自动将该锁给释放掉。
ZK实现分布式锁代码
/**
* ZooKeeperSession
* @author Administrator
*
*/
public class ZooKeeperSession {
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
private ZooKeeper zookeeper;
private CountDownLatch latch;
public ZooKeeperSession() {
try {
this.zookeeper = new ZooKeeper(
"192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181",
50000,
new ZooKeeperWatcher());
try {
connectedSemaphore.await();
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("ZooKeeper session established......");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取分布式锁
* @param productId
*/
public Boolean acquireDistributedLock(Long productId) {
String path = "/product-lock-" + productId;
try {
zookeeper.create(path, "".getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
return true;
} catch (Exception e) {
while(true) {
try {
Stat stat = zk.exists(path, true); // 相当于是给node注册一个监听器,去看看这个监听器是否存在
if(stat != null) {
this.latch = new CountDownLatch(1);
this.latch.await(waitTime, TimeUnit.MILLISECONDS);
this.latch = null;
}
zookeeper.create(path, "".getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
return true;
} catch(Exception e) {
continue;
}
}
// 很不优雅,我呢就是给大家来演示这么一个思路
// 比较通用的,我们公司里我们自己封装的基于zookeeper的分布式锁,我们基于zookeeper的临时顺序节点去实现的,比较优雅的
}
return true;
}
/**
* 释放掉一个分布式锁
* @param productId
*/
public void releaseDistributedLock(Long productId) {
String path = "/product-lock-" + productId;
try {
zookeeper.delete(path, -1);
System.out.println("release the lock for product[id=" + productId + "]......");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 建立zk session的watcher
* @author Administrator
*
*/
private class ZooKeeperWatcher implements Watcher {
public void process(WatchedEvent event) {
System.out.println("Receive watched event: " + event.getState());
if(KeeperState.SyncConnected == event.getState()) {
connectedSemaphore.countDown();
}
if(this.latch != null) {
this.latch.countDown();
}
}
}
/**
* 封装单例的静态内部类
* @author Administrator
*
*/
private static class Singleton {
private static ZooKeeperSession instance;
static {
instance = new ZooKeeperSession();
}
public static ZooKeeperSession getInstance() {
return instance;
}
}
/**
* 获取单例
* @return
*/
public static ZooKeeperSession getInstance() {
return Singleton.getInstance();
}
/**
* 初始化单例的便捷方法
*/
public static void init() {
getInstance();
}
}
Redis分布式锁和ZK分布式锁
redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能
zk分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小
另外一点就是,如果是redis获取锁的那个客户端bug了或者挂了,那么只能等待超时时间之后才能释放锁;而zk的话,因为创建的是临时znode,只要客户端挂了,znode就没了,此时就自动释放锁
redis分布式锁大家每发现好麻烦吗?遍历上锁,计算时间等等。。。zk的分布式锁语义清晰实现简单
所以先不分析太多的东西,就说这两点,我个人实践认为zk的分布式锁比redis的分布式锁牢靠、而且模型简单易用
8.分布式Session连环炮
什么是Session
session是啥?浏览器有个cookie,在一段时间内这个cookie都存在,然后每次发请求过来都带上一个特殊的jsessionid cookie,就根据这个东西,在服务端可以维护一个对应的session域,里面可以放点儿数据。
一般只要你没关掉浏览器,cookie还在,那么对应的那个session就在,但是cookie没了,session就没了。常见于什么购物车之类的东西,还有登录状态保存之类的。
但是你单块系统的时候这么玩儿session没问题啊,但是你要是分布式系统了呢,那么多的服务,session状态在哪儿维护啊?
其实方法很多,但是常见常用的是两种
tomcat + redis
这个其实还挺方便的,就是使用session的代码跟以前一样,还是基于tomcat原生的session支持即可,然后就是用一个叫做Tomcat RedisSessionManager的东西,让所有我们部署的tomcat都将session数据存储到redis即可。
在tomcat的配置文件中,配置一下
<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />
<Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"
host="{redis.host}"
port="{redis.port}"
database="{redis.dbnum}"
maxInactiveInterval="60"/>
搞一个类似上面的配置即可,你看是不是就是用了RedisSessionManager,然后指定了redis的host和 port就ok了。
<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />
<Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"
sentinelMaster="mymaster"
sentinels="<sentinel1-ip>:26379,<sentinel2-ip>:26379,<sentinel3-ip>:26379"
maxInactiveInterval="60"/>
还可以用上面这种方式基于redis哨兵支持的redis高可用集群来保存session数据,都是ok的
但我们从Session获取数据,其实tomcat就是会从redis中获取到session了。
但是存在的问题,就是严重依赖于Web容器
Spring Session + redis
分布式会话的这个东西重耦合在tomcat中,如果我要将web容器迁移成jetty,难道你重新把jetty都配置一遍吗?
因为上面那种tomcat + redis的方式好用,但是会严重依赖于web容器,不好将代码移植到其他web容器上去,尤其是你要是换了技术栈咋整?比如换成了spring cloud或者是spring boot之类的。还得好好思忖思忖。
所以现在比较好的还是基于java一站式解决方案,spring了。人家spring基本上包掉了大部分的我们需要使用的框架了,spirng cloud做微服务了,spring boot做脚手架了,所以用sping session是一个很好的选择。
pom.xml
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>1.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.1</version>
</dependency>
spring配置文件
<bean id="redisHttpSessionConfiguration"
class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
<property name="maxInactiveIntervalInSeconds" value="600"/>
</bean>
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="100" />
<property name="maxIdle" value="10" />
</bean>
<bean id="jedisConnectionFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">
<property name="hostName" value="${redis_hostname}"/>
<property name="port" value="${redis_port}"/>
<property name="password" value="${redis_pwd}" />
<property name="timeout" value="3000"/>
<property name="usePool" value="true"/>
<property name="poolConfig" ref="jedisPoolConfig"/>
</bean>
web.xml
<filter>
<filter-name>springSessionRepositoryFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
示例代码
@Controller
@RequestMapping("/test")
public class TestController {
@RequestMapping("/putIntoSession")
@ResponseBody
public String putIntoSession(HttpServletRequest request, String username){
request.getSession().setAttribute("name", “leo”);
return "ok";
}
@RequestMapping("/getFromSession")
@ResponseBody
public String getFromSession(HttpServletRequest request, Model model){
String name = request.getSession().getAttribute("name");
return name;
}
}
上面的代码就是ok的,给sping session配置基于redis来存储session数据,然后配置了一个spring session的过滤器,这样的话,session相关操作都会交给spring session来管了。接着在代码中,就用原生的session操作,就是直接基于spring sesion从redis中获取数据了。
实现分布式的会话,有很多种很多种方式,我说的只不过比较常见的两种方式,tomcat + redis早期比较常用;近些年,重耦合到tomcat中去,通过spring session来实现。
9.Spring的事务连环炮
来源:https://www.bilibili/video/BV1EE411p7dD
什么是事务
事务:是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作,这些操作作为一个整体像系统提交,要么都执行,要么都不执行;事务是一组不可再分割的操作集合(工作逻辑单元)
通俗点说就是为了达到某个目的而做的一系列的操作要么一起成功(事务提交),要么一起失败(事务回滚)
最常见的例子就是转账:
小明给如花转账:
开启事务-------
① 从小明的账户扣除1000元
② 给如花的账户增加1000元
事务提交-------
从上面的例子的任何步骤一旦出现问题,都会导致事务回滚。
从搭讪到结婚就是事务提交,女方要求男方重新追求她一次就是事务回滚~
事务的四大特征
ACID是事务的基本特征:口诀(一原持久隔离)
- 原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
- 一致性(Consistency):事务执行后,数据库状态与其它业务规则保持一致,如转账业务,无论事务执行成功与否,参与转账的两个账号余额之和应该是不变的
- 隔离性(Isolation):隔离性指在并发操作中,不同事务之间应该隔离开来,使每个并发中的事务不会相互干扰。
- 持久性(Durability):一旦事务提交成功,事务中所有的数据操作都必须持久化到数据库中,即使提交事务后,数据库马上崩溃,在数据库重启时,也必须保证通过某种机制恢复数据。
原生JDBC事务操作
try {
// 设置是否自动提交
connection.setAutoCommit(false)
// 数据库操作 insert,update,delete
connectionmit()
} catch(Exception ex) {
// 回滚
connection.rollback()
} finally {
connection.setAutoCommit(true)
}
事务隔离级别
数据库事务的隔离级别有4中,由低到高分别是:Read uncomomitted(读取未被提交的数据)
、Read committed(读取以被提交的数据)
、Repeatable read(可重复读)
、Serializable(完全隔离)
。而且,在事务的并发操作中,可能出现脏读、不可重复读、幻读、事务丢失
四种隔离级别
Read UnCommitted
读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据,会产生脏读
Read Committed
这种使用的概率比较高,因为很多时候我们就以最后一次读取的为准
读提交,顾名思义,就是一个事务要等另一个事务提交后才能读取数据,会产生不可重复读。
Repeatable Read
相当于加锁,MySQL的默认级别
重复读,就是在开始读取数据(事务开启)时,不在允许修改操作,可能会产生幻读。
Serializable
最高的事务隔离级别,在改级别下,事务串行化顺序执行,可以避免脏读,不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用
总结
大多数数据库默认的事务隔离级别是:Read Committed
,比如SqlServer,Oracle
MySQL的默认隔离级别是Repeatable Read
可能出现的问题
脏读
读取了未提交的新事务,然后被回滚了
事务A读取了事务B中尚未提交的数据,如果事务B回滚,则A读取使用了错误的数据
不可重复读
读取了提交的新事物,指更新操作
不可重复读是指对于数据库中某个数据,一个事务范围内多次查询却反悔不同的数值,这是由于在查询间隔,被另一个事务修改并提交了
解决不可重复读的一个解决方案,就是调整隔离级别。
幻读
读取了提交的新事物,指增删操作
在事务A多次读取构成中,事务B对数据进行了新增操作,导致事务A多次读取的数据不一致
对于幻读的另外一个理解,可以为:
幻读就是指新增了数据记录条数,第一次查询数据记录数为1000,再次查询的时候,变成了1001,这个就是幻读
不可重复读和幻读的区别
很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于update和delete,而幻读的重点在于insert
- 不可重复读, 只需要锁住满足条件的记录
避免不可重复读需要锁行就行
- 幻读 要锁住满足条件及其相近的记录
避免幻读则需要锁表
所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题
事务丢失
第一类事务丢失
称为:回滚丢失
对于第一类事务丢失,就是比如A和B同时在执行一个数据,然后B事务已经提交了,然后A事务回滚了,这样B事务的操作就因A事务回滚而丢失了。
第二类事务丢失
称为:覆盖丢失
对于第二类事务丢失,也称为覆盖丢失,就是A和B一起执行一个数据,两个同时取到一个数据,然后B事务首先提交,但是A事务接下来又提交,这样就覆盖了B事务
Spring怎么配置事务
具体说出一些关键的xml元素
准备数据表:
实体类:
public class User {
private Integer id;
private String name;
private Integer money;
}
Dao:
Service:默认一个出错的场景
编程式事务
XML配置事务:在applicationContext.xml中添加事务管理器和事务管理器模板的配置
注解方式配置事务管理器和事务管理器模板
声明式事务
基于AspectJ XML方式
注:基于TransactionProxyFactoryBean,代理的方式是比较古老的方式,我们这里就不叙述了
删除applicationContext.xml中的事务管理模版的配置,就是下面的配置:
<!-- 事务管理的模板 -->
<bean id="transactionTemplate"
class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager" />
</bean>
添加事务定义和AOP配置
<!--基于AspectJ 申明式事务XML配置方式-->
<!-- 定义一个增强 -->
<tx:advice id="txAdvice"
transaction-manager="transactionManager">
<!-- 增强(事务)的属性的配置 -->
<tx:attributes>
<!-- isolation:DEFAULT ,事务的隔离级别。
propagation:事务的传播行为.
read-only:false,不是只读
timeout:-1
no-rollback-for:发生哪些异常不回滚
rollback-for:发生哪些异常回滚事务
-->
<tx:method name="transfer" isolation="DEFAULT" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- aop配置定义切面和切点的信息 -->
<aop:config>
<!-- 定义切点:哪些类的哪些方法应用增强 -->
<aop:pointcut
expression="execution(* com.dk.beans.service..*.*(..))"
id="mypointcut" />
<!-- 定义切面: -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="mypointcut" />
</aop:config>
业务类改成原来的方式
/**
* 沒有事務的转账的业务
* @param fromName
* @param toName
* @param money
*/
public void transfer(String fromName, String toName, Integer money) {
userDAO.out(fromName, money);// 转出钱
int x = 10;
if(x == 10)
throw new RuntimeException("出错啦!");
userDAO.in(toName, money);// 收入钱
}
基于注解的申明式事务: 在配置类上配置@EnableTransactionManagement开启事务。删除注解类中和事务相关的@Bane
在UserService类上方或者方法上方通过@Transactional完成事务配置:
@Service
@Transactional
public class UserService {
}
申明式事务可以按照对应什么开头的方法 给配置事务
<!-- 以insert开头的方法配置事务 -->
<tx:method name="insert*" isolation="DEFAULT" propagation="REQUIRED"/>
Spring事务传播特性
指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。举例子
public class PersonService {
@Transactional
public void laoda(){
System.out.println("老大的方法");
xiaodi();
}
@Transactional
public void xiaodi(){
System.out.println("小弟方法");
}
}
也就是当老大的方法,调用小弟的方法时,小弟的事务该怎么办?
假设老大的方法出现异常,那么小弟的需不需要回滚?
假设小弟的方法出现异常,那么老大的放法需不需要回滚?
死活不要事务的
- PROPAGATION_NEVER:没有就非事务执行,有就抛出异常
- PROPAGATION_NOT_SUPPORTED:没有就非事务执行,有就直接挂起,然后非事务执行
- 这个容易出现死锁
可有可无的
- PROPAGATION_SUPPORTS: 有就用,没有就算了
必须有事务的
- PROPAGATION_REQUIRES_NEW:有没有都新建事务,如果原来有,就将原来的挂起。也就是说事务之间完全隔离,一个事务不影响其它的事务
- PROPAGATION_NESTED: 如果没有,就新建一个事务;如果有,就在当前事务中嵌套其他事务。嵌套事务就是外面的事务出异常,里面的事务全部回滚
- PROPAGATION_REQUIRED: (默认的配置)如果没有,就新建一个事务;如果有,就加入当前事务
- PROPAGATION_MANDATORY: 如果没有,就抛出异常;如果有,就使用当前事务。
10.分布式事务连环炮
- 分布式事务了解吗
- 你如何解决分布式事务问题
- 两阶段提交方案/XA方案
- TCC方案
- 本地消费表
- 可靠消息最终一致性方案
- 最大努力通知方案
- 公司如何处理分布式事务
分布式事务
只要聊到你做了分布式系统,必问分布式事务,你对分布式事务一无所知的话,确实会很坑,你起码得知道有哪些方案,一般怎么来做,每个方案的优缺点是什么。
现在面试,分布式系统成了标配,而分布式系统带来的分布式事务也成了标配了。因为你做系统肯定要用事务吧,那你用事务的话,分布式系统之后肯定要用分布式事务吧。先不说你搞过没有,起码你得明白有哪几种方案,每种方案可能有啥坑?比如TCC方案的网络问题、XA方案的一致性问题
单机系统下的事务
分布式系统下的事务
两阶段提交方案/XA方案
也叫做两阶段提交事务方案,这个举个例子,比如说咱们公司里经常tb是吧(就是团建),然后一般会有个tb主席(就是负责组织团建的那个人)。
tb,team building,团建
第一个阶段,一般tb主席会提前一周问一下团队里的每个人,说,大家伙,下周六我们去滑雪+烧烤,去吗?这个时候tb主席开始等待每个人的回答,如果所有人都说ok,那么就可以决定一起去这次tb。如果这个阶段里,任何一个人回答说,我有事不去了,那么tb主席就会取消这次活动。
第二个阶段,那下周六大家就一起去滑雪+烧烤了
所以这个就是所谓的XA事务,两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复ok,那么就正式提交事务,在各个数据库上执行操作;如果任何一个数据库回答不ok,那么就回滚事务。
这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。如果要玩儿,那么基于spring + JTA就可以搞定,自己随便搜个demo看看就知道了。
这个方案,我们很少用,一般来说某个系统内部如果出现跨多个库的这么一个操作,是不合规的。我可以给大家介绍一下, 现在微服务,一个大的系统分成几百个服务,几十个服务。一般来说,我们的规定和规范,是要求说每个服务只能操作自己对应的一个数据库。
如果你要操作别的服务对应的库,不允许直连别的服务的库,违反微服务架构的规范,你随便交叉胡乱访问,几百个服务的话,全体乱套,这样的一套服务是没法管理的,没法治理的,经常数据被别人改错,自己的库被别人写挂。
如果你要操作别人的服务的库,你必须是通过调用别的服务的接口来实现,绝对不允许你交叉访问别人的数据库!
TCC方案
TCC的全程是:Try、Confirm、Cancel。
这个其实是用到了补偿的概念,分为了三个阶段:
- Try阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留
- Confirm阶段:这个阶段说的是在各个服务中执行实际的操作
- Cancel阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作
给大家举个例子吧,比如说跨银行转账的时候,要涉及到两个银行的分布式事务,如果用TCC方案来实现,思路是这样的:
- Try阶段:先把两个银行账户中的资金给它冻结住就不让操作了
- Confirm阶段:执行实际的转账操作,A银行账户的资金扣减,B银行账户的资金增加
- Cancel阶段:如果任何一个银行的操作执行失败,那么就需要回滚进行补偿,就是比如A银行账户如果已经扣减了,但是B银行账户资金增加失败了,那么就得把A银行账户资金给加回去
这种方案说实话几乎很少用人使用,我们用的也比较少,但是也有使用的场景。因为这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大,非常之恶心。
比如说我们,一般来说跟钱相关的,跟钱打交道的,支付、交易相关的场景,我们会用TCC,严格严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性,在资金上出现问题
比较适合的场景:这个就是除非你是真的一致性要求太高,是你系统中核心之核心的场景,比如常见的就是资金类的场景,那你可以用TCC方案了,自己编写大量的业务逻辑,自己判断一个事务中的各个环节是否ok,不ok就执行补偿/回滚代码。
而且最好是你的各个业务执行的时间都比较短。
但是说实话,一般尽量别这么搞,自己手写回滚逻辑,或者是补偿逻辑,实在太复杂了,那个业务代码很难维护。
本地消费表
这个大概意思是这样的
- A系统在自己本地一个事务里操作同时,插入一条数据到消息表
- 接着A系统将这个消息发送到MQ中去
- B系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息
- B系统执行成功之后,就会更新自己本地消息表的状态以及A系统消息表的状态
- 如果B系统处理失败了,那么就不会更新消息表状态,那么此时A系统会定时扫描自己的消息表,如果有没处理的消息,会再次发送到MQ中去,让B再次处理
- 这个方案保证了最终一致性,哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止
这个方案说实话最大的问题就在于严重依赖于数据库的消息表来管理事务啥的???这个会导致如果是高并发场景咋办呢?咋扩展呢?所以一般确实很少用
可靠消息最终一致性方案
这个的意思,就是干脆不要用本地的消息表了,直接基于MQ来实现事务。比如阿里的RocketMQ就支持消息事务。
大概的意思就是:
- A系统先发送一个prepared消息到mq,如果这个prepared消息发送失败那么就直接取消操作别执行了
- 如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉mq发送确认消息,如果失败就告诉mq回滚消息
- 如果发送了确认消息,那么此时B系统会接收到确认消息,然后执行本地的事务
- mq会自动定时轮询所有prepared消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认消息?那是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,别确认消息发送失败了。
- 这个方案里,要是系统B的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如B系统本地回滚后,想办法通知系统A也回滚;或者是发送报警由人工来手工回滚和补偿
这个还是比较合适的,目前国内互联网公司大都是这么玩儿的,要不你举用RocketMQ支持的,要不你就自己基于类似ActiveMQ?RabbitMQ?自己封装一套类似的逻辑出来,总之思路就是这样子的
最终一致性的解决方案:就是生产把消息发送到消息队列中,然后消息队列对消息进行持久化操作,但消费者没有进行处理的时候,就会不断的重试,知道最终处理完成,这就保证了一致性。
最大努力通知方案
这个方案的大致意思就是:
-
系统A本地事务执行完之后,发送个消息到MQ
-
这里会有个专门消费MQ的最大努力通知服务,这个服务会消费MQ然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统B的接口
-
要是系统B执行成功就ok了;要是系统B执行失败了,那么最大努力通知服务就定时尝试重新调用系统B,反复N次,最后还是不行就放弃
可以在一定程度上允许是少数的分布式事务失败,一般用在对分布式要求不严格的情况下,比如说记录日志或状态
公司如何处理分布式事务
我们某某特别严格的场景,用的是TCC来保证强一致性;然后其他的一些场景基于了阿里的RocketMQ来实现了分布式事务。你找一个严格资金要求绝对不能错的场景,你可以说你是用的TCC方案;如果是一般的分布式事务场景,订单插入之后要调用库存服务更新库存,库存数据没有资金那么的敏感,可以用可靠消息最终一致性方案
友情提示一下,rocketmq 3.2.6之前的版本,是可以按照上面的思路来的,但是之后接口做了一些改变,我这里不再赘述了。
当然如果你愿意,你可以参考可靠消息最终一致性方案来自己实现一套分布式事务,比如基于rabbitmq来玩儿。
老师,我们现在想保证我们的某个系统非常的可靠,任何一个数据都不能错,我们用的是微服务架构,几十个服务。结果我们一盘点,发现,如果到处都要搞的话,一个系统要做几十个分布式事务出来。
我们的经验,我带几十人的team,最大的一个项目,起码几百个服务,复杂的分布式大型系统,里面其实也没几个分布式事务。
你其实用任何一个分布式事务的这么一个方案,都会导致你那块儿代码会复杂10倍。很多情况下,系统A调用系统B、系统C、系统D,我们可能根本就不做分布式事务。如果调用报错会打印异常日志。
每个月也就那么几个bug,很多bug是功能性的,体验性的,真的是涉及到数据层面的一些bug,一个月就几个,两三个?如果你为了确保系统自动保证数据100%不能错,上了几十个分布式事务,代码太复杂;性能太差,系统吞吐量、性能大幅度下跌。
99%的分布式接口调用,不要做分布式事务,直接就是监控(发邮件、发短信)、记录日志(一旦出错,完整的日志)、事后快速的定位、排查和出解决方案、修复数据。
每个月,每隔几个月,都会对少量的因为代码bug,导致出错的数据,进行人工的修复数据,自己临时动手写个程序,可能要补一些数据,可能要删除一些数据,可能要修改一些字段的值。
比你做50个分布式事务,成本要来的低上百倍,低几十倍
trade off,权衡,要用分布式事务的时候,一定是有成本,代码会很复杂,开发很长时间,性能和吞吐量下跌,系统更加复杂更加脆弱反而更加容易出bug;好处,如果做好了,TCC、可靠消息最终一致性方案,一定可以100%保证你那快数据不会出错。
1%,0.1%,0.01%的业务,资金、交易、订单,我们会用分布式事务方案来保证,会员积分、优惠券、商品信息,其实不要这么搞了
库。
如果你要操作别的服务对应的库,不允许直连别的服务的库,违反微服务架构的规范,你随便交叉胡乱访问,几百个服务的话,全体乱套,这样的一套服务是没法管理的,没法治理的,经常数据被别人改错,自己的库被别人写挂。
如果你要操作别人的服务的库,你必须是通过调用别的服务的接口来实现,绝对不允许你交叉访问别人的数据库!
更多推荐
Java架构师面试十大连环炮
发布评论