面试宝鉴

面试宝鉴

消息队列

  1. 消息队列使用场景

    解耦、异步、削峰

    解耦: A系统需要发送数据给BCD三个系统,如果A挂掉,其他三个系统的功能都要受影响,这个时候使用MQ,A系统发送消息数据到MQ中,其他的系统从MQ中消费即可,达到解耦的目的.

    异步: 在不需要同步返回结果的时候,使用MQ,直接将消息发给MQ,然后返回成功,会提高用户体验.(A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,如果使用MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms,对于用户而言,其实感觉上就是点个按钮,8ms 以后就直接返回了,)

    削峰: 在系统的运行中,每天正常时间的请求都不多,但是在特定的一个时间点,突然有大量的请求进来,系统直接基于mysql查询,有可能直接导致崩溃,这个时候在中间映引入MQ,每次请求进来以后都保存在MQ中,每次Mysql处理定量的请求,就算在高峰期,也不会崩溃.

  2. 消息队列的缺点

    • 系统可用性降低

      系统引用的外部依赖越多,越容易挂掉,如果MQ挂了,整个系统都崩溃了,所以要保持MQ的高可用.

    • 系统复杂度提高

      加入MQ以后,要保证消息不重复,不丢失,还有消息传递的顺序性,这都是需要解决的.

    • 一致性问题

      A系统处理完了直接返回成功,但是后续的逻辑中,有部分不成功,这就导致数据的不一致.

  3. Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点?

    特性ActiveMQRabbitMQRocketMQKafka
    单机吞吐量万级,比 RocketMQ、Kafka 低一个数量级同 ActiveMQ10 万级,支撑高吞吐10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景
    topic 数量对吞吐量的影响topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topictopic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源
    时效性ms 级微秒级,这是 RabbitMQ 的一大特点,延迟最低ms 级延迟在 ms 级以内
    可用性高,基于主从架构实现高可用同 ActiveMQ非常高,分布式架构非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
    消息可靠性有较低的概率丢失数据基本不丢经过参数优化配置,可以做到 0 丢失同 RocketMQ
    功能支持MQ 领域的功能极其完备基于 erlang 开发,并发能力很强,性能极好,延时很低MQ 功能较为完善,还是分布式的,扩展性好功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用
  4. 所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。

    如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。


如何保证消息队列的高可用?

RabbitMQ的高可用

RabbitMQ有三种模式:单机模式、普通集群模式、镜像集群模式.

  1. 单机模式: 一般用于demo,在生产中不使用

  2. 普通集群模式(无高可用性)

    顾名思义,就是在多台机器上启动多个RabbitMQ实例,每台机器启动一个.创建的queue,只会放在一个RabbitMQ实例上.假如有ABC三个MQ实例,我们需要A实例的数据,但是确连接到了B或者C实例上,这个时候,B或者C就会从A实例拉取数据过来,会有拉取数据的性能开销,而且当A的实例宕机了,其他的实例也就无法获取A的数据了.

  3. 镜像集群模式(高可用)

    在镜像集群模式下,创建的queue,无论元数据还是queue里的消息都会存在于多个实例上,也就是说,每个RabbitMQ节点都有这个queue的一个完整镜像,包含queue的全部数据,每次写消息到queue的时候,都会自动把消息同步到多个实例的queue上.

    要开启镜像集群功能,要再RabbitMQ控制管理后台新增一个策略,即镜像集群模式的策略,指定的时候可以要求数据同步到所有节点或指定数量的节点,再次创建queue的时候应用该策略即可.

Kafka的高可用

首先要了解一下Kafka的架构: 由多个 broker 组成,每个 broker 是一个节点;你创建一个 topic,这个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据。

Kafka 0.8 以前,是没有 HA 机制的,就是任何一个 broker 宕机了,那个 broker 上的 partition 就废了,没法写也没法读,没有什么高可用性可言。

Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。只能读写 leader?很简单,要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题,系统复杂度太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。

如果某个 broker 宕机了,没事儿,那个 broker上面的 partition 在其他机器上都有副本的,如果这上面有某个 partition 的 leader,那么此时会从 follower 中重新选举一个新的 leader 出来,大家继续读写那个新的 leader 即可。

写数据的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,接着其他 follower 自己主动从 leader 来 pull 数据。一旦所有 follower 同步好数据了,就会发送 ack 给 leader,leader 收到所有 follower 的 ack 之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为)

消费的时候,只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到


如何保证消息不被重复消费?(保证消息的幂等性)

为什么会重复消费?

首先,比如 RabbitMQ、RocketMQ、Kafka,都有可能会出现消息重复消费的问题,正常。因为这问题通常不是 MQ 自己保证的,是由我们开发来保证的。挑一个 Kafka 来举个例子,说说怎么重复消费吧。

Kafka 实际上有个 offset 的概念,就是每个消息写进去,都有一个 offset,代表消息的序号,然后 consumer 消费了数据之后,每隔一段时间(定时定期),会把自己消费过的消息的 offset 提交一下,表示“我已经消费过了,下次我要是重启啥的,你就让我继续从上次消费到的 offset 来继续消费吧”。

但是凡事总有意外,比如我们之前生产经常遇到的,就是你有时候重启系统,看你怎么重启了,如果碰到点着急的,直接 kill 进程了,再重启。这会导致 consumer 有些消息处理了,但是没来得及提交 offset,尴尬了。重启之后,少数消息会再次消费一次。

如何保证幂等性?
  • 比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update 一下好吧。
  • 比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。
  • 比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。
  • 比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。

如何处理消息丢失的问题?

数据丢失的问题,可能出现在生产者、MQ、消费者中,以RabbitMQ和Kafka为例:

RabbitMQ
1. 生产者弄丢了数据

生产者将数据发送到RabbitMQ的时候,数据在半路就丢了.

此时可以选择用RabbitMQ提供的事务功能,即生产者发送数据之前开启RabbitMQ事务channel.txSelect,然后发送消息,如果消息没有成功被RabbitMQ接收到,那么生产者就会收到异常报错,这个时候可以回滚事务channel.txRollback,然后重试发送,如果接受到了,才提交事务channel.txCommit.

这里有个问题,开启事务同步以后,基本上吞吐量会下来,太消耗性能.

所以一般来说,确保这个过程的消息不丢失,可以开启confirm模式,在生产者那边开启了confirm模式之后,每次写的消息都会分配一个唯一的id,如果消息写入了MQ中,MQ会回传一个ack消息,告诉我们这个消息OK了.如果RabbitMQ没能处理这个消息,会回调你的一个nack接口,告诉我们这个消息接受失败,需要重试.

事务机制和confirm机制的最大的不痛在于,事务机制是同步的,提交事务以后会阻塞在那边,但是confirm机制是异步的,发送了这个消息以后还可以继续发送下一个消息,所以在生产者方面避免数据丢失,都是用confirm机制实现的.

2. RabbitMQ弄丢了数据

这个情况下必须开启RabbitMQ的持久化,就是消息写入之后会持久化到磁盘,就算MQ自己挂了,恢复之后会自动读取之前存储的数据,一般不会丢失.

设置持久化有两个步骤:

  1. 创建queue的时候将其设置为持久化

    这样就可以保证RabbitMQ持久化queue的元数据,但是它是不会持久化queue里的数据的.

  2. 发送消息的时候将消息的deliveryMode设置为2

    将其设置为2时,此时RabbitMQ就会将消息持久化到磁盘上去.

必须要同时设置这两个持久化才OK.

注意,哪怕是给RabbitMQ开启了持久化机制也有可能,数据在没有持久化的时候,MQ就挂了,这样就会丢失一点数据,这个时候可以跟confirm机制配合起来,只有消息被持久化到磁盘以后,才会通知生产者ack.

3. 消费端弄丢了数据

RabbitMQ如果丢失了数据,主要是因为消费的时候,刚消费到,还没处理,结果进程挂了,这个时候就出现了数据丢失的问题,此时RabbitMQ认为消费了,但其实数据丢失了.

这个时候可以用RabbitMQ提供的ack机制,简单来说,必须关闭RabbitMQ的自动ack,可以通过一个api来调用即可,每次代码里处理完逻辑以后,程序中ack,这样一来,只要没有处理完,就不会ack,这个时候RabbitMQ会认为我们还没有处理完,会把这个消息分配给别的consumer处理,消息不会丢失.

Kafka
1.消费端弄丢了数据

唯一可能导致消费者弄丢数据的情况,就是说,你消费到了这个消息,然后消费者那边自动提交了 offset,让 Kafka 以为你已经消费好了这个消息,但其实你才刚准备处理这个消息,你还没处理,你自己就挂了,此时这条消息就丢了.

这不是跟 RabbitMQ 差不多吗,大家都知道 Kafka 会自动提交 offset,那么只要关闭自动提交 offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。但是此时确实还是可能会有重复消费,比如你刚处理完,还没提交 offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就好了。

2. Kafka弄丢了数据

这块比较常见的一个场景,就是 Kafka 某个 broker 宕机,然后重新选举 partition 的 leader。大家想想,要是此时其他的 follower 刚好还有些数据没有同步,结果此时 leader 挂了,然后选举某个 follower 成 leader 之后,不就少了一些数据?这就丢了一些数据.

所以此时一般是要求起码设置如下 4 个参数:

  • 给 topic 设置 replication.factor 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。
  • 在 Kafka 服务端设置 min.insync.replicas 参数:这个值必须大于 1,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。
  • 在 producer 端设置 acks=all:这个是要求每条数据,必须是写入所有 replica 之后,才能认为是写成功了
  • 在 producer 端设置 retries=MAX(很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了。

我们生产环境就是按照上述要求配置的,这样配置之后,至少在 Kafka broker 端就可以保证在 leader 所在 broker 发生故障,进行 leader 切换时,数据不会丢失。


如何保证消息的顺序性?

顺序错乱的场景:
  • RabbitMQ: 一个queue,多个consumer.比如:生产者向RabbitMQ发送了三条数据,顺序依次是data1/data2/data3,压入的是RabbitMQ的一个内存队列.有三个消费者分别从MQ中消费这三条数据中的一条,结果消费者2先执行完操作,把data2存入数据库,然后是data1/data3,这样子顺序就乱了.

  • Kafka: 我们建了一个topic,有三个partition.生产者在写的时候其实可以指定一个key,比如说我们指定了某个订单id作为key,那么这个订单相关的数据,一定会被分发到用于一个partition中去,而且这个partition中的数据一定是有顺序的.

    消费者从partition中取出来的时候,也是有顺序的.接着我们在消费者中使用多线程来并发处理消息,这样的话顺序就乱了.

解决方案
  • RabbitMQ

    拆分多个queue,每个queue一个consumer,consumer内部用内存队列做排队,然后分发给底层不同的worker来处理.

  • Kafka

    写N个内存queue,具有相同的key的数据都到同一个内存queue,然后对于N个线程,每个线程分别消费一个内存queue即可.


如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?

场景假设
消费端出故障了,所以大量的消息积压在MQ中.

这个时候,不能傻傻的等MQ消费玩,需要紧急扩容:

  • 先修复consumer的问题,确保其消费速度,然后将现有consumer都停掉
  • 新建一个topic,partition是原来的10倍,临时建立好原来10倍的queue数量.
  • 写一个临时分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀写入临时建立好的10倍数量的queue
  • 临时征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的数据,按照正常10倍的速度来消费.
  • 等快速消费完积压数据之后,回复原先部署的架构,用原来的consumer来消费消息
mq中的消息过期失效了(消息积压过多,部分消息过期了)

熬夜写程序手动查出来,然后从新灌入mq中补回来.

mq快写满了

只能临时下程序直接消费消息以后丢弃,避免崩溃.


自己写一个消息队列,如何架构设计?

  • 首先该mq要支持可伸缩性,在需要的时候加速扩容,可以增加吞吐量和容量.类似kafka的设计:broker->topic->partition,每个partition放一个机器,存一部分数据,要扩容的时候给topic增加partition就可以
  • 要有数据可持久化的机制,也就是数据要保存在磁盘上,且在保存落地的时候,顺序写,这样就没有磁盘随机读写的寻址开销,性能也就好一点.
  • mq要有可用性,也就会要有多副本
  • 支持数据0丢失

搜索引擎

es的架构原理(如何实现分布式?)

ElasticSearch的设计理念就是分布式搜索引擎,底层是基于lucene的.核心思想就是在多台机器上启动多个es实例,组成一个es集群.

es中存储数据的基本单位是索引,一个索引相当于mysql中的一张表:

index–>type–>mapping–>document–>field

index相当于数据库中的 一个表,而type相当于某个种类的表,mapping相当于就是这个type的表结构定义(在ex7.x以后,type和mapping这两个概念被移除了),往index中写一条数据,叫一条document,相当于mysql表中的一行数据,而每个document有多个field,每个fiedl代表了这个document中的一个字段的值.

es将索引拆分成多个shard,每个shard都存储部分数据.

每个shard都有一个primary shard,负责写入数据,还有几个replica shard,primary shard写入数据以后,就会将数据同步到其他几个replica shard上去.这样当有个机器宕机时,也没有关系,还有别的数据副本在别的机器上.

es集群多个节点会自动选举一个节点为master节点,这个master节点负责其他节点的管理工作,如:维护索引的元数据、切换primary shardreplica shard的身份等,要是master节点宕机了,会重新选举一个节点为master节点.

如果某个非master节点宕机了,那么此节点上的primary chard就没了,master会让primary shard对应的其他机器上的replica shard切换为primary shard,如果宕机的机器修复了,修复后的节点也不再是primart shard,会变成replica shard,并同步后续修改的数据.


es 写入数据的工作原理是什么啊?es 查询数据的工作原理是什么啊?底层的 lucene 介绍一下呗?倒排索引了解吗?

es写数据的过程
  • 客户端选择一个node发送请求过去,这个node就是coordinating node(协调节点)
  • coordinating node对document进行路由,将请求转发给对应的node(有primart shard)
  • 实际的node上的primary shard处理请求,然后将数据同步到replica shard.
  • coordinating node如果发现primary node和所有的replica node都完成之后,就返回结果响应给客户端
es读数据的过程
  • 客户端发送一个请求到coordinate node
  • 协调节点将搜索请求转发到所有的shard对应的primart shard或者replica shard上.
  • query phase: 每个shard将自己的搜索结果(其实就是一些doc id)返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果
  • fetch phase: 接着由协调节点根据doc id去各个节点上拉取实际的document数据,最终返回给客户端
写数据底层原理

先写入内存buffer,在buffer里的时候数据是搜索不到的,同时将数据写入translog日志文件.

如果buffer快满了,或者到一定时间,就会将内存buffer数据refresh到一个新的segment file中,但是此时数据不是直接进入segment file磁盘文件,而是先进入os cache.这个过程就是refresh.

每隔1秒钟,es将buffer钟的数据写入一个新的segment file,每秒钟会产生一个新的磁盘文件segment file,这个文件中就存储最近一秒内buffer中写入的数据.

但是如果buffer里面此时没有数据,那就不会执行refresh操作,如果buffer里面有数据,默认一秒钟执行一次refresh操作,刷入一个新的segment file中.

操作系统里面,磁盘文件其实都有一个东西叫os cache,即操作系统缓存,就是说数据写入磁盘文件之前,会先进入os cache,先进入操作系统级别的一个内存缓存中去.只要buffer中的数据被refresh操作刷入到os cache中,这个数据就可以被搜索到了.

为什么叫es是准实时的?

NRT,全称near real-time.默认是每隔1秒refresh一次,写入的数据一秒钟以后才能被看到,所以是准实时的.可以通过es的refresh api或者java api,手动执行一次refresh操作,即手动将buffer中的数据刷入os cache中,让数据立马就可以被搜索到.只要数据被输入os cache中,buffer就会被清空了,因为不需要保留buffer了,数据在translog里面已经持久化到磁盘一份了.

重复上面的步骤,新的数据不断进入buffer和translog,不断将buffer数据写入一个又一个新的segment file中去,每次refresh完buffer清空,translog保留.随着这个过程推进,translog会变的越来越大,当translog达到一定长度的时候,就会触发commit操作.

commit操作发生的第一步,就是将buffer中现有数据refreshos cache中去,清空buffer.然后将一个commit point写入磁盘文件,里面标识着这个commit point对应的所有segment file,同时强行将os cache目前所有的数据都fsync到磁盘文件中去.最后清空现有的translog日志文件,重启一个translog,此时commit操作完成.

这个commit操作叫flush.默认30分钟自动执行一次flush,但如果translog过大,也会触发flush.flush操作就对应着commit的全过程,我们可以通过es api,手动执行flush操作,手动将os cache中的数据fsync强刷到磁盘上去.

translog日志文件的作用是什么?

执行commit操作之前,数据要么是停留在buffer中,要么是停留在os cache中,无论是buffer还是os cache都是内存,一旦这台机器死了,内存中的数据就全丢了.所以需要将数据对应的操作写入一个专门的日志文件translog中,一旦此时机器宕机,再次重启的时候,es会自动读取translog日志文件中的数据,回复到内次buff和os cache中去.

translog其实也是先写入os cahce的,默认每隔5秒刷一次到磁盘中去,所以默认情况下,可能会有5秒的数据仅仅停留在buffer或者translog文件的os cache中,如果此时挂了,会丢失5秒的数据.但是这样性能比较好,最多丢5秒的数据.也可以将translog设置成每次写操作必须是直接fsync到磁盘,但是性能会差很多.

删除/更新数据底层原理

如果是删除操作,commit的时候会生成一个.del文件,里面将某个doc标志为deleted状态,那么搜索的时候根据.del文件就知道这个doc是否被删除了.

如果是更新操作,就是将原来的doc标志为deleted状态,然后新写入一条数据.

buffer每次refresh一次,就会产生一个segement file文件,所以默认情况下是1秒一个segment file,这样下来segment file会越来越多,此时会定期执行merge,每次merge的时候,会将多个segment file合并成一个,同时这里会将标识为deleted的doc物理删除,然后将新的segment file写入磁盘,这里会写一个commit point,标识所有新的segment file,然后打开segment file供搜索使用,同时删除旧的segment file.

底层lucene

lucene就是一个jar包,里面包含了封装好的各种建立倒排索引的算法代码.我们用java开发的时候,引入lucene jar,然后基于lucene的api开发即可.

通过lucene,我们可以将已有的数据建立索引,lucene会在本地磁盘上面,给我们组织索引的数据结构.

倒排索引

在搜索引擎中,每个文档都有一个文档ID,文档内容被表示为一系列关键词的集合.

倒排索引就是关键词到文档ID的映射,每个关键词都对应着一些列的文件,这些文件中都出现了关键词.

比如:

有以下文档:

DocIdDoc
1谷歌地图之父跳槽 Facebook
2谷歌地图之父加盟 Facebook
3谷歌地图创始人拉斯离开谷歌加盟 Facebook
4谷歌地图之父跳槽 Facebook 与 Wave 项目取消有关
5谷歌地图之父拉斯加盟社交网站 Facebook

对文档进行分词之后,得到以下倒排索引

WordIdWordDocIds
1谷歌1,2,3,4,5
2地图1,2,3,4,5
3之父1,2,4,5
4跳槽1,4
5Facebook1,2,3,4,5
6加盟2,3,5
7创始人3
8拉斯3,5
9离开3
104

如何数据量很大情况下es的查询效率?

优化方式一:filessystem cache

写入es的数据,实际都是写到磁盘文件中的,查询的时候,操作系统会将磁盘文件中的数据自动缓存到filessystem cache里面.

es的搜索引擎严重依赖filessystem cache,如果filessystem cache内存更大,可以容纳所有的索引数据文件,这样搜索的时候会走内存,速度非常快.(查询是,走磁盘是秒级,走内存是毫秒级).

因此,要提高查询效率,首先要保证机器内存,至少可以容纳总数据量的一半,其次,可以选择es+mysql或者es+hbase的方式来保存数据:在es中只存入部分字段,其他字段数据存在mysql或者hbase中,这样在查询的时候,es查询出对应的文档id,然后根据id去数据库查询全部数据再返回即可.

优化方式二:数据预热

即在后台搞一个系统,每隔一定时间,把查询最多的文档数据,刷入filesystem cache中去,这样在访问的时候会快一点.

优化方式三:冷热分离

将大量访问很少,频率很低的数据,单独写一个索引;将访问量很高很频繁的数据单独写另一个索引,这样在访问的时候,基本上热数据就会存在于filesystem cache中.

优化方式四:document模型设计

在es中,尽量不要用关联查询等复杂的操作,因此在设计document模型的时候,就完成各种负载的操作,避免join/nested等复杂操作.

优化方式五:分页性能优化

首先es的分页是比较坑的,如果是每页10条数据,现在要查询第100页,实际上会把每个shard上存储的前1000条数据都查到一个协调节点上,然后对这些数据再合并、处理,再获取到第100页的十条数据.因此分页查的时候,页码越深,查询越慢.

优化办法:

  • 不允许深度分页

  • 不做分页,做类似app推荐商品不断下拉出来一页一页

    可以用scroll api做类似微博下拉刷新的效果,在使用的时候,scroll会一次性生成所有数据的快照,每次滑动向后翻页就是通过**游标scroll_id**移动,性能会高很多.这种方式不能随意跳转到某一页

    初始化时必须制定scroll参数,告诉es要保存此次搜索的上下问多久,避免用户持续翻页时间过长而超时失败.

    除了使用scroll api,也可以使用search_after来做,它的思想是使用前一页的结果来帮助检索下一页的数据.


es 生产集群的部署架构是什么?每个索引的数据量大概有多少?每个索引大概有多少个分片?

  • es生产集群部署了5台机器,每台机器6核64G,集群总内存320G
  • 我们 es 集群的日增量数据大概是 2000 万条,每天日增量数据大概是 500MB,每月增量数据大概是 6 亿,15G。目前系统已经运行了几个月,现在 es 集群里数据总量大概是 100G 左右。
  • 目前线上有 5 个索引(这个结合你们自己业务来,看看自己有哪些数据可以放 es 的),每个索引的数据量大概是 20G,所以这个数据量之内,我们每个索引分配的是 8 个 shard,比默认的 5 个 shard 多了 3 个shard。

缓存

为什么要用缓存?

主要有两个用途:高性能、高并发

高性能:

复杂操作查询出来,且不经常变化的数据,可以放缓存,提高读取速度.

高并发:

mysql单机支撑2000QPS就容易报警.如果高峰期上万的请求到数据库,mysql会挂.用缓存则不会,缓存单机支持量秒级几万到十几万.


用了缓存之后的不良后果?

  • 缓存与数据库双写不一致
  • 缓存雪崩、缓存穿透
  • 缓存并发竞争

redis和memcached的区别?

redis支持复杂的数据结构

redis有更多的数据机构,支持丰富的数据操作.

redis原生支持集群模式

在redis3.x版本中,便能支持cluster模式,而memcached没有原生的集群模式,需要客户端的支持来实现集群.

性能区别

redis使用单核,而memcached可以使用多核,所以平均每一个核上redis在存储小数据时比memecached性能高.

而在100K以上的数据中,memcached性能要高于redis.


redis的线程模型是什么?

redis内部使用文件事件处理器file event handler,这个处理器是单线程的,所以redis才是单线程的模型.它采用IO多路复用机制同时监听多个socket,根据socket上的事件来选择对应的事件处理器进行处理.

文件事件处理器的机构包含四个部分:

  • 多个socket
  • IO多路复用程序
  • 文佳事件分派器
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

多个socket可能会并发产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个socket,会将socket产生的事件放入队列中排队,事件分排器每次从队列中取出一个事件,把事件交给对应的事件处理器进行处理.

客户端与redis的一次通行过程:

客户端socket01向redis的server socket请求建立连接,此时server socket会产生一个AE_READABLE事件,IO多路复用程序监听到server socket产生的事件以后,将该事件压入队列中.文件事件分派器从队列中获取该事件,交给连接应答处理器.连接应答处理器会创建一个能与客户端通信的socket01,并将该socket01的AE_READABLE事件与命令请求处理器关联.

假设此时客户端发送了一个set key value请求,此时redis中的socket01会产生AE_READABLE事件,IO多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面的socket01的AE_READABLE事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理.命令请求处理器读取socket01的key value并在自己内存中完成key value的设置,操作完成后,它会将socket01的AE_READABLE事件与命令回复处理器关联.

此时如果客户端准备好接受返回结果了,那么redis中的socket01会产生一个AE_WRITABLE事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对socket01输入对本次操作的一个结果,如ok,之后解除socket01AE_WRITABLE事件与命令回复处理器的关联.

这样便完成了一次通信.


为什么redis单线程模型也能效率这么高?

  • 纯内存操作
  • 核心是基于非阻塞的IO多路复用机制
  • 单线程反而避免了多线程的频繁上下文切换问题

redis有哪些数据类型?都有哪些应用场景?

redis主要有以下几种数据类型:

  • string
  • hash
  • list
  • set
  • sorted set(zset)

string

最简单的类型,使用普通的set和get,做简单的KV缓存

set college szu

hash

类似map的一种结构,可以将结构化的数据缓存在redis里(比如对象),然后每次读写缓存的时候,就可以操作hash里面的某个字段.

hset person name bingo
hset person age 20
hset person id 1
hset person name
person = {
    "name": "bingo",
    "age" : 20,
    "id" : 1
}

list

list是有序列表,可以通过list存储一些列表型的数据结构,类似粉丝列表,文章的评论之类的.

还可以通过lrange命令,读取某个闭区间内的元素,可以基于list实现分页查询,基于redis实现简单的高性能分页,可以做类似微博的下拉不断分页的效果.

# 0位置开始,-1位置结束,-1表示列表的最后一个位置,即查看所有
lrange mylist 0 -1

也可以用list实现简单的消息队列.

lpush mylist 1
lpush mylist 2
lpush mylist 3 4 5

rpop mylist

set

set是无序集合,自动去重

可以基于set操作交集,比如可以把两个人的粉丝列表放在一个交集,看看两人的共同好友是谁.

#操作set
#添加元素
sad mySet 1

#查看全部元素
smembers mySet

#判断是否包含某个值
sismember mySet 3

#删除某个/些元素
srem mySet 1
srem mySet 2 4

#查看元素个数
scard mySet

#随机删除一个元素
spop mySet

#操作多个set
#将一个set的元素移动到另一个set
smove yourSet mySet 2

#求两个set的交集
sinter yourSet mySet

#求两个set的并集
sunion youSet mySet

#求在yourSet中而不在mySet中的元素
sdiff yourSet mySet

sorted set

sorted set是排序的set,去重但可以排序,写进去的时候给一个分数,自动根据分数排序.

zadd board 85 zhangsan
zadd board 72 lisi
zadd board 96 wangwu
zadd board 63 zhaoliu

#获取排名前三的用户(默认是升序,所以需要rev改为降序)
zrevrange board 0 3

#获取某用户的排名
zrank board zhaoliu

redis的过期策略有哪些?

redis的过期策略是:定期删除+惰性删除

定期删除,指的是redis默认每隔100ms就随机抽取一些设置了过期时间的key,检查是否过期,如果过期了就删除.

但是以上检查会导致很多key到了时间还没有删除,怎么办?

所以就是惰性删除了,在获取某个key的时候,redis会检查一下,这个key如果设置了过期时间,且过期了就会删除,不会返回任何东西.

如果定期删除后遗留的key过期了,也没有被惰性删除,大量过期key堆积在内存,也是不好的,因此还会走内存淘汰机制


内存淘汰机制有哪些?

redis内存淘汰机制有以下几个:

  • noeviction: 当内存不足以容纳新写入数据时,新写入数据会报错.
  • allkeys-lru: 当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(最常用的)
  • allkeys-random: 当内存不足以容纳新写入数据时,在键空间中,随机移除某个key.
  • volatile-lru: 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key.
  • volatile-random: 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key.
  • volatile-ttl: 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除.

利用JDK实现简单的LRU

class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int CACHE_SIZE;
    
    /**
     *传递进来最多能缓存多少数据
     *
     */
    public LRUCache(int cacheSize) {
        //true表示让linkedHashMap按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部.
        super((int)Math.ceil(cachesize / 0.75) + 1, 0.75f, true);
        CACHE_SIZE = cacheSize;
    }
    
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    	//当map中的数据量大于指定的缓存个数的时候,就自动删除老的数据
        return size() > CACHE_SIZE;
    }
    
}

如何保证redis实现高并发和高可用?

redis实现高并发主要依靠主从架构

一主多从,一般很多项目都够用.单主用来写入数据,多从用来查询数据,多个实例可以提供每秒10W的QPS.

要在高并发的同时,容纳大量的数据,还可以使用redis集群.

在redis主从架构上,加上哨兵,就可以实现任何一个实例宕机,主备切换.


Redis主从架构

单机的redis,能够承载的QPS大概在上万到几万不等.对缓存来说,一般都是用来支撑读高并发的.因此架构做成**主从(master-slave)**架构,一主多从,主负责写,将数据复制到其他的slave节点,从节点负责读.所有的读请求全部走从节点,这样可以很轻松的实现水平扩容,支撑读高并发.

redis replication的核心机制
  • redis采用异步方式复制数据到slave节点,redis2.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的持久化,不建议用slave node作为master node的数据热备,因为如果那样的话,如果关掉master的持久化,可能在master宕机重启的时候数据是空的,然后经过复制,slave node的数据也丢了.

此外,master的各种备份方案也需要做.

redis主从复制的核心原理

当启动一个slave node的时候,它会发送一个PSYNC命令给master node.

如果这是slave node初次链接到master node,那么会触发一次full resynchronization全量复制.此时master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端client新收到的所有写命令缓存在内存中.RDB文件生成完毕后,master会将这个RDB发给slave,slave会先写入本地磁盘,再从本地磁盘加载到内存中,接着master会将内存中缓存的写命令发送到slave,slave也会同步这些数据.slave node如果更master node有网络故障,断开了连接,会自动重连,连接之后master node仅会复制给slave部分缺少的数据.

主从复制的端点续传

从redis2.8开始,就支持主从复制的端点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方继续复制,而不是从头开始复制.

master node会在内存中维护一个backlog,master和slave都会保存一个replica offset和一个master run id,offset就是保存backlog中的.如果master和slave网络连接断掉了,slave会让master从上次replica offset开始继续复制,如果没有找到对应的offset,那么久会执行一次resynchronization

无磁盘化复制

master在内存中直接创建RDB,然后发送给slave,不会在自己本地磁盘落地.只需要在配置文件中开启repl-diskless-sync yes即可

过期key处理

slave不会过期key,只会等待master过期key.如果master过期了一个key,或者通过LRU淘汰了一个key,那么就会模拟一条del命令发送给slave.

复制的完整流程

slave node启动时,会在自己本地保存master node的信息,包括master node的hostip,但是复制流程没有开始.

slave node内部有个定时任务,每秒检查是否有新的master node需要连接和复制,如果发现,就跟master node建立socket网络连接.然后slave node发送ping命令给master node.如果master node配置了requirepass,那么slave node必须发送masterauth的口令过去认证.master node第一次执行全量复制,将所有数据发给slave node.而在后续,master node持续将写命令,异步复制给slave node.

全量复制
  • master执行bgsave,在本地生成一份rdb快照文件
  • master node将rdb快照文件发送给slave node,如果rdb复制时间超过60s(repl-timeout),那么slave node就会认为复制失败,可以适当调大这个参数
  • master node在生成rdb时,会将所有新的写命令缓存在内存中,在slave node保存了rdb之后,再将新的写命令复制给slave node
  • 如果在复制期间,内存缓冲区持续消耗超过64MB,或者一次性超过256MB,那么停止复制,复制失败.
  • slave node接受到rdb之后,清空自己的旧数据,然后重新加载rdb到自己的内存中,同事基于旧的数据版本对外提供服务
  • 如果slave node开启了AOF,那么会立即执行BGREWRITEAOF,重写aof
增量复制
  • 如果全量复制过程中,master-slave网络连接断掉,那么slave重新连接master时,会触发增量复制.
  • master直接从自己的backlog中获取部分丢失的数据,发送给slave node,默认backlog是1MB.
  • master就是根绝slave发送的psync中的offset来从backlog中获取数据的.
heartbeat(心跳)

主从节点互相都会发送heartbeat信息

master默认每隔10s发送一次,slave每隔1s发送一次

异步复制

master 每次接受到写命令之后,现在内部写入数据,然后异步发送给slave node.


redis如何才能做到高可用?

redis的高可用架构,叫做failover,即故障转移,也可以叫做主备切换.

master node在故障时,自动检测,并且将某个slave node自动切换为master node的过程叫做主备切换,这个过程实现了redis主从架构下的高可用.

也可以开启数据持久化来做到高可用


Redis哨兵集群实现高可用

哨兵的介绍

sentinel,中文名是哨兵.哨兵是redis集群机构中非常重要的一个组件,主要有以下功能:

  • 集群监控: 负责监控redis master和slave进程是否正常工作
  • 消息通知: 如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
  • 故障转移: 如果master node挂掉了,会自动转移到slave node上
  • 配置中心: 如果故障转移发生了,通知client客户端新的master地址

哨兵用于实现redis集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,相互协同工作.

  • 故障转移时,判断一个master node是否宕机了,需要大部分哨兵都同意才行,涉及到分布式选举的问题
  • 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的
哨兵的核心知识:
  • 哨兵至少需要3个实例,来保证自己的健壮性
  • 哨兵+redis主从的架构部署,是不保证数据零丢失的,只能保证redis集群的高可用性
  • 对于哨兵+redis主从这种复杂的部署架构,要在测试和生产环境进行充足的测试.
redis哨兵主备切换的数据丢失问题

在主备切换的过程中,可能会导致数据丢失:

  • 异步复制导致的数据丢失: 因为master->slave的复制是异步的,所以可能有部分数据还没有复制到slave,master就宕机了,此时这部分数据就丢失了.

  • 脑裂导致的数据丢失: 某个master所在的机器突然脱离了正常的网络,跟其他的slave不能连接,但是实际上master还运行着.此时哨兵可能就会认为master宕机了,然后开启选举,将其他的slave切换成了master,这个时候,集群里就会有两个master,也就是所谓的脑裂

    此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续向旧的master写数据.因此master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会被清空,重新从新的master复制数据.而新的slave并没有后来client吸入的数据,这部分数据就丢失了.

数据丢失的解决方案

进行如下配置:

min-slaves-to-write 1
min-slaves-max-lag 10

表示要求至少有1个slave,数据复制和同步的延迟不能超过10s.

如果一旦所有的slave复制数据和同步的延迟都超过了了10s,那么这个时候master就不会再接收任何请求了.

sdown和odown转换机制
  • sdown是主观宕机,就一个哨兵如果自己觉得一个master宕机了,就是主观宕机
  • odown是客观宕机,如果quorum数量的额哨兵觉得一个mater宕机了,那么就是客观宕机.

sdown 达成的条件很简单,如果一个哨兵 ping 一个 master,超过了 is-master-down-after-milliseconds 指定的毫秒数之后,就主观认为 master 宕机了;如果一个哨兵在指定时间内,收到了 quorum 数量的 其它哨兵也认为那个 master 是 sdown 的,那么就认为是 odown 了。

哨兵集群的自动发现机制

哨兵相互之间的发现,是通过redis的pub/sub系统实现的,每个哨兵都会往_sentinel_:hello这个channel里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他哨兵的存在.

每隔两秒钟,每个哨兵都会往自己监控的master+slaves对应的_sentinel_:hellochannel里发送一个消息,内容是自己的host、ip和runid还有对这个master的监控配置

每个哨兵也会去监听自己监控的每个master+slaves对应的_sentinel_:hellochannel,然后去感知到同样在监听这个master+slaves的其他哨兵的存在.

每个哨兵还会对其他哨兵交换对master的监控配置,相互进行监控配置的同步.

slave配置的自动纠正

哨兵会负责自动纠正slave的一些配置,比如slave如过要成为潜在的master候选人,哨兵会确保slave复制现有master的数据;如果slave连接到了一个错误的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数量的哨兵认为odwon,然后选举一个哨兵出来做切换,这个哨兵还得得到majority哨兵的授权才能正式执行切换.

如果 quorum < majority,比如 5 个哨兵,majority 就是 3,quorum 设置为2,那么就 3 个哨兵授权就可以执行切换。

但是如果 quorum >= majority,那么必须 quorum 数量的哨兵都授权,比如 5 个哨兵,quorum 是 5,那么必须 5 个哨兵都同意授权,才能执行切换。

configuration epoch

哨兵会对一套redis master+slaves进行监控,有相应的监控配置

执行切换操作的那个哨兵,会从要切换到新的master那里得到一个configuration epoch,这就是一个version号,每次切换的version号都必须是唯一的.

如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout事件,然后接替继续执行切换,此时会重新获取一个configuration epoch,作为新的version号

configuration传播

哨兵完成切换之后,会在自己本地更新生成最新的master配置,然后同步给其他哨兵,就是通过pus/sub消息机制,在这里,其他的哨兵都是根据版本号的大小来更新自己的master配置的.


redis持久化方式有哪几种?

redis持久化的两种方式:
  • RDB: RDB持久化机制,是对redis中的数据执行周期性的持久化
  • AOF: AOF机制对每条写入命令作为日志,以append-only的模式写入一个日志文件中,在redis重启的时候,可以通过回访AOF日志中的写入指令来重新构建整个数据集.

如果同时使用RDB和AOF两种持久化方式,在redis重启的时候,会使用AOF来重新构建数据,因为AOF中的数据更加完整.

RDB优缺点:
  • RDB会生成多个数据文件,每个数据文件代表了某一个时刻中redis的数据,这种多个数据文件的方式,非常适合做冷备,可以将这种完整的文件发送到远程存储上,以预定好的备份策略来定期备份redis中的数据
  • RDB对redis对外提供的读写服务影响非常小,可以让redis保持高性能.
  • 相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速
  • 缺点是,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒或者数秒.
AOF优缺点:
  • AOF可以更好的保护数据不丢失,一般AOF会每隔一秒,通过一个后台线程执行一次fsync,最多丢失一秒钟的数据
  • AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复
  • AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写.因为在rewritelog的时候,对进行压缩,创建出一份需要恢复数据的最小文件出来.再创建新日志文件的时候,老的日志文件还是照常写入.当心的merge后的日志文件ready的时候,再交换新老旧文件即可.
  • AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复.比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么久可以立即拷贝AOF文件,将最后一条flushall命令删除,然后再将该AOF文件放回去,就可以恢复所有数据.
  • 对同一份数据来说,AOF日志文件通常比RDB数据快照大
  • AOF开启后,支持的写QPS会比RDB支持的QPS低,因为AOF通常会配置成每秒fsync一次日志文件

两种机制应该同事开启,综合使用.


redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?


缓存雪崩(大量key过期直接走DB或者缓存挂了请求走DB)

缓存挂了以后,大量的请求全部落在数据库,数据库报警之后也挂了.这就是缓存雪崩

解决方案:
  • 事前: redis高可用, 主从+哨兵,redis cluster,避免全盘崩溃
  • 事中: 本地ehcache缓存 +hystrix限流&降级,避免mysql被打死
  • 事后: redis持久化,一旦重启,自动从磁盘上加载数据,快速恢复

用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,如果没查到再查 redis。如果 ehcache 和 redis 都没有,再查数据库,将数据库中的结果,写入 ehcache 和 redis 中。

限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?走降级!可以返回一些默认的值,或者友情提示,或者空白的值。

好处:

  • 数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。
  • 只要数据库不死,就是说,对用户来说,2/5 的请求都是可以被处理的。
  • 只要有 2/5 的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次

缓存穿透

请求中有黑客恶意发送缓存中查不到的数据,这样直接查询数据库,导致数据库崩溃

解决方法:

每次从数据库中没有查到,就写一个空值到缓存中,这样下次就会走缓存.


如何保证缓存与数据库的双写一致性?

读请求和写请求串行化

消耗性能,不推荐使用

Cache Aside Pattern
  • 读的时候,先读缓存,缓存没有的话就读数据库,然后取出数据放入缓存

  • 更新的时候,先更新数据库,然后再删除缓存

    因为有时候更新缓存的性能是比较搞的,比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。

有可能删除缓存失败了,导致缓存旧数据,数据库新数据

解决方案:先删除缓存,再修改数据库.如果数据库修改失败了,缓存中是空数据,数据不会不一致.

比较复杂的数据不一致问题

数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了…

这种情况只有数据在并发读写的时候才有可能出现,且并发流量要很高


redis并发竞争怎么解决?

所谓并发竞争,就是多客户端同时并发写一个 key,可能本来应该先到的数据后到了,导致数据版本错了;或者是多客户端同时获取一个 key,修改值之后再写回去,只要顺序错了,数据就错了。

解决办法

可以基于 zookeeper 实现分布式锁。每个系统通过 zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 key,别人都不允许读和写。

你要写入缓存的数据,都是从 mysql 里查出来的,都得写入 mysql 中,写入 mysql 中的时候必须保存一个时间戳,从 mysql 查出来的时候,时间戳也查出来。

每次要写之前,先判断一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。


生产环境中redis的部署

redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。

机器配置:32G内存+8核+1T磁盘,但是分配给redis进程的内存是10G,超过10G可能会有问题,5台机器对外提供读写,一共有50G内存.

缓存中存入商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量.


分库分表

分表

就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。比如按照用户 id 来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用户就操作那个表就好了。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在 200 万以内。

分库

将一个库的数据拆分到多个库中,访问的时候就访问一个库即可.

#分库分表前分库分表后
并发支撑情况MySQL 单机部署,扛不住高并发MySQL从单机到多机,能承受的并发增加了多倍
磁盘使用情况MySQL 单机磁盘容量几乎撑满拆分为多个库,数据库服务器磁盘使用率大大降低
SQL 执行性能单表数据量太大,SQL 越跑越慢单表数据量减少,SQL 执行效率明显提升

分别分表中间件有哪些?

  • cobar

  • TDDL

  • atlas

  • sharding-jdbc

    当当开源产品,属于client方案

  • mycat

    基于cobar改造,属于proxy方案

大公司用mycat,小公司用sharding-jdbc.

水平拆分

把一个表的数据弄到多个库中的多个表中去,但是每个表的表机构都是一样的,只不过数据是不同的.所有库表的数据加起来就是全部数据.水平拆分的意义是扛高并发、扩容.

垂直拆分

把一个有很多字段的表拆分成多个表,或者多个库上去.没个库表的表机构都不一样,都包含部分字段.一般会将较少的访问频率很高的字段放到一个表里,然后将较多的访问频率很高的字段放到另一个表中.

分库分表方式

  • 按照range来分,就是按照每个库一段连续的数据,按照时间范围来划分的.
  • 按照某个字段hash一下均匀分布.

range 来分,好处在于说,扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。

hash 分发,好处在于说,可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表。


如何让系统从未分库分表动态切换到分库分表?

方案一

停机迁移,12点以后用导数工具写数据到分库分表

方案二

双写迁移,在线上系统里面,之前所有写操作的地方,包括增删改操作,都加上对新库的增删改,然后系统部署之后,新库数据差太远,用之前说的导数工具,跑起来读老库数据写新库,写的时候要根据 gmt_modified 这类字段判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写。简单来说,就是不允许用老数据覆盖新数据。


如何设计可以动态扩容缩容的分库分表方案?

方案一

停机扩容(不推荐)

方案二

  1. 设定好几台数据库服务器,每台服务器上几个库,每个库多少个表,推荐是 32库 * 32表,对于大部分公司来说,可能几年都够了。
  2. 路由的规则,orderId 模 32 = 库,orderId / 32 模 32 = 表
  3. 扩容的时候,申请增加更多的数据库服务器,装好 mysql,呈倍数扩容,4 台服务器,扩到 8 台服务器,再到 16 台服务器。
  4. 由 dba 负责将原先数据库服务器的库,迁移到新的数据库服务器上去,库迁移是有一些便捷的工具的。
  5. 我们这边就是修改一下配置,调整迁移的库所在数据库服务器的地址。
  6. 重新发布系统,上线,原先的路由规则变都不用变,直接可以基于 n 倍的数据库服务器的资源,继续进行线上系统的提供服务。

分库分表之后,ID主键如何处理?

因为分表之后,每个表都是从1开始累加,这样是不行的,所以需要一个全局唯一的id来支持.

基于数据库的实现方案

数据库自增id

系统里每次得到一个 id,都是往一个库的一个表里插入一条没什么业务含义的数据,然后获取一个数据库自增的一个 id。拿到这个 id 之后再往对应的分库分表里去写入。

适合并发量不高的情况.

设置数据库sequence或者表自增字段步长

可以通过设置数据库 sequence 或者表的自增字段步长来进行水平伸缩。

比如说,现在有 8 个服务节点,每个服务节点使用一个 sequence 功能来产生 ID,每个 sequence 的起始 ID 不同,并且依次递增,步长都是 8

获取系统当前时间

一般如果用这个方案,是将当前时间跟其他的业务字段拼接起来作为一个id.

snowflake算法

snowflake 算法是 twitter 开源的分布式 id 生成算法,采用 Scala 语言实现,是把一个 64 位的 long 型的 id,1 个 bit 是不用的,用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 bit 作为序列号。

  • 1 bit:不用,为啥呢?因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。
  • 41 bit:表示的是时间戳,单位是毫秒。41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2^41 - 1 个毫秒值,换算成年就是表示69年的时间。
  • 10 bit:记录工作机器 id,代表的是这个服务最多可以部署在 2^10台机器上哪,也就是1024台机器。但是 10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 2^5个机房(32个机房),每个机房里可以代表 2^5 个机器(32台机器)。
  • 12 bit:这个是用来记录同一个毫秒内产生的不同 id,12 bit 可以代表的最大正整数是 2^12 - 1 = 4096,也就是说可以用这个 12 bit 代表的数字来区分同一个毫秒内的 4096 个不同的 id

就是说 41 bit 是当前毫秒单位的一个时间戳,就这意思;然后 5 bit 是你传递进来的一个机房id(但是最大只能是 32 以内),另外 5 bit 是你传递进来的机器 id(但是最大只能是 32 以内),剩下的那个 12 bit序列号,就是如果跟你上次生成 id 的时间还在一个毫秒内,那么会把顺序给你累加,最多在 4096 个序号以内。

利用这个工具类,自己搞一个服务,然后对每个机房的每个机器都初始化这么一个东西,刚开始这个机房的这个机器的序号就是 0。然后每次接收到一个请求,说这个机房的这个机器要生成一个 id,你就找到对应的 Worker 生成。


读写分离

如何实现MySQL的读写分离?

就是基于主从架构,搞一个主库,多个从库,写操作只由主库来处理,然后会自动把数据同步给从库.

MySQL主从复制的原理是什么?

主库将变更写入binlog日志,然后从库连接到主库之后,从库有一个IO线程,将主库的binlog日志拷贝到自己本地,写入一个relay中继日志中.接着从库中有一个SQL线程会从中继日志读取binlog,然后执行日志中的内容,也就是在自己本地再执行一遍SQL,这样就可以保证自己跟主库的数据是一样的.

如何解决主从同步延时?

在从库同步主库数据的时候,有一个问题:

由于从库同步数据的过程是串行化的,所以在主库中并行的操作,也会在从库中串行执行.因此,在高并发情况下,从库的数据一定会比主库慢一些,是有延时的;而且如果主库突然宕机,恰好数据还没有同步到从库,那么有些数据可能就丢失了.

MySQL解决数据延时和丢失的机制:

  • 半同步复制

    semi-sync复制,就是主库写入binlog日志以后,就会强制此时立即将数据同步给从库,从库将日志写入自己本地的relaylog之后,会返回一个ack给主库,主库接收到至少一个从库的ack之后才会认为写操作完成了.

  • 并行复制

    从库开启多个线程,并行读取relay log中不同库的日志,然后并行重放不同库的日志.

主从同步延时的解决

一般来说,如果主从延迟较为严重,有以下集中解决方案:

  • 分库,将一个主库拆分为多个主库,每个主库的并发就减少了几倍,此时延时可以忽略不计.
  • 打开MySQL支持的并行复制,多个库并行复制.
  • 优化代码
  • 直连主库(不推荐,这种方式失去了读写分离的意义)

如何设计一个高并发系统?

可以分为以下6点:

  • 系统拆分
  • 缓存
  • MQ
  • 分库分表
  • 读写分离
  • ElasticSearch
系统拆分

将一个系统拆分为多个子系统,用dubbo来实现相互通信,然后每个系统连一个数据库

缓存

大部分的高并发场景都是读多写少,数据在缓存和数据库中都写一份,读的时候走缓存.

MQ

使用MQ,大量的请求灌入MQ,排队消费,消费以后再写入,控制在mysql承载范围内.MQ单机扛几万并发是可以的

分库分表

并发量很大的时候,就要考虑分库分表来扛高并发,分表以后,每个表的数据量少一点,提高sql的性能.

读写分离

数据库可以采用主从架构,主库写入,从库读取,读流量太多的时候,还可以加更多的从库.

ElasticSearch

es是分布式的,可以随便扩容,天然支撑高并发,比较简单的查询和统计类的操作可以考虑用es来承载.


分布式服务架构

Dubbo的工作流程

  • 第一步: provider向注册中心去注册
  • 第二步: consumer从注册中心订阅服务,注册中心会通知consumer注册好的服务
  • 第三步: consumer调用provider
  • 第四步: consumer和provider都异步通知监控中心.

Dubbo支持的通行协议

  • dubbo协议

    默认使用这种,单一长连接,进行NIO异步通信,基于hessian作为序列化协议,适合传输量小,但是并发量高的场景.

  • rmi协议

    使用java二进制序列化,多个短连接,适合消费者和提供者数量差不多的情况,适用文件的传输.

  • hessian协议

    使用hessian序列化协议,多个短连接,适用提供者数量比消费者多的情况,

  • http协议

    使用json序列化

  • webservice

    使用SOAP文本序列化


Hessian的数据结构

Hessian的对象序列化机制有8种原始类型:

  • 原始二进制数据
  • boolean
  • 64-bit date
  • 64-bit double
  • 32-bit int
  • 64-bit long
  • null
  • UTF-8编码的string

另外还包括三种递归类型:

  • list for lists and arrays
  • map for maps and dictionaries
  • object for objects

还有一种特殊类型:

  • ref : 用来表示对共享对象的引用

为什么PB的效率是最高的?

PB即Protocal Buffer是Google出品的一种轻量且高效的结构化数据存储格式.

之所以PB性能好,有两个原因:第一:它使用proto编译器,自动进行序列化和反序列化,速度非常快;第二:它的数据压缩效果好,它序列化后的数据量体积小.


dubbo负载均衡策略

  • random loadbalance

    默认使用这种策略,可以对provider不同实例设置不同的权重,会按照权重来负载均衡,权重越大分配流量越高.

  • roundrobin loadbalance

    均匀地将流量打到各个机器上.但是如果各个机器的性能不一样.容易导致性能差的机器负载过高.

  • leastactive loadbalance

    自动感知,如果某个机器性能越差,越不活跃,就会给不活跃的机器更少的请求.

  • consistanthash loadbalance

    一致性Hash算法,相同参数的请求一定会分到一个provider上,provider挂掉的时候,会基于虚拟节点均匀分配剩余的流量,抖动不会太大.


dubbo集群容错策略

  • faillover cluster模式

    失败自动切换,自动重试其他机器,默认使用

  • failfast cluster模式

    一次调用失败就立即失败,常见于写操作

  • failsafe cluster模式

    出现异常时忽略掉,常用于不重要的接口调用.比如记录日志

  • filaback cluster模式

    失败了后台自动记录请求,然后定时重发,比较适合于写消息队列这种.

  • forking cluster模式

    并行调用多个provider,只要一个成功就立即返回

  • broadcacst cluster

    逐个调用所有的provider

dubbo动态代理策略

默认使用javassist动态字节码生成,创建代理类.但是可以通过spi扩展机制配置自己的动态代理策略.


如何基于 dubbo 进行服务治理、服务降级、失败重试以及超时重试?

服务治理
  • 调用链路自动生成

    基于dubbo的分布式系统中,对各个服务之间的调用自动记录下来,然后自动将各个服务之间的依赖关系和调用链路生成出来,做成一张图.

  • 服务访问压力以及时长统计

    需要自动统计各个接口和服务之间的调用次数以及访问延时,分成两个级别

    一个级别是接口力度,即每个服务的每个接口每天被调用多少次,TP50/TP90/TP99三个档次的请求延时是多少.

    第二个级别是从源头入口开始,一个完整的请求链路经过几十个服务之后,完成一次请求,每天全链路走多少次,三档延时分别是多少.

  • 服务分层(避免循环依赖)

  • 调用链路失败监控和报警

  • 服务鉴权

  • 每个服务的可用性监控(接口调用成功率)

服务降级

比如说服务 A调用服务 B,结果服务 B 挂掉了,服务 A 重试几次调用服务 B,还是不行,那么直接降级,走一个备用的逻辑,给用户返回响应。

    <dubbo:reference id="fooService" interface="com.test.service.FooService"  timeout="10000" check="false" mock="return null">

我们调用接口失败的时候,可以通过mock统一返回null.

mock的值也可以修改为true,然后再跟接口同一个路径下实现一个Mock类,命名规则是"接口名称+Mock"后缀.然后再Mock类中实现自己的降级逻辑

失败重试和超时重试
<dubbo:reference id="xxxx" interface="xx" check="true" async="false" retries="3" timeout="2000"/>
  • timeout:一般设置为 200ms,我们认为不能超过 200ms 还没返回。
  • retries:设置 retries,一般是在读请求的时候,比如你要查询个数据,你可以设置个 retries,如果第一次没读到,报错,重试指定的次数,尝试再次读取。

分布式服务接口的幂等性如何设计(比如不能重复扣款)?

其实保证幂等性主要是三点:

  • 对于每个请求必须有一个唯一的标识,举个栗子:订单支付请求,肯定得包含订单 id,一个订单 id 最多支付一次,对吧。
  • 每次处理完请求之后,必须有一个记录标识这个请求处理过了。常见的方案是在 mysql 中记录个状态啥的,比如支付之前记录一条这个订单的支付流水。
  • 每次接收请求需要进行判断,判断之前是否处理过。比如说,如果有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,orderId 已经存在了,唯一键约束生效,报错插入不进去的。然后你就不用再扣款了。

实际运作过程中,你要结合自己的业务来,比如说利用 redis,用 orderId 作为唯一键。只有成功插入这个支付流水,才可以执行实际的支付扣款。

要求是支付一个订单,必须插入一条支付流水,order_id 建一个唯一键 unique key。你在支付一个订单之前,先插入一条支付流水,order_id 就已经进去了。你就可以写一个标识到 redis 里面去,set order_id payed,下一次重复请求过来了,先查 redis 的 order_id 对应的 value,如果是 payed 就说明已经支付过了,你就别重复支付了。


分布式锁

Zookeeper都有哪些应用场景?

大致来说,zookeeper应用场景如下:

  • 分布式协调
  • 分布式锁
  • 元数据/配置信息管理
  • HA高可用性
分布式协调

简单来说,A系统发送个请求到mq,然后B系统消费消息之后处理了.那么A如何知道B系统的处理结果?

用 zookeeper 就可以实现分布式系统之间的协调工作。A 系统发送请求之后可以在 zookeeper 上对某个节点的值注册个监听器,一旦 B 系统处理完了就修改 zookeeper 那个节点的值,A 立马就可以收到通知.

分布式锁

个机器接收到了请求之后先获取 zookeeper 上的一把分布式锁,就是可以去创建一个 znode,接着执行操作;然后另外一个机器也尝试去创建那个 znode,结果发现自己创建不了,因为被别人创建了,那只能等着,等第一个机器执行完了自己再执行。

元数据/配置信息管理

可以作为很多系统的配置信息管理工具,入dubbo

HA高可用

hadoop、hdfs、yarn 等很多大数据系统,都选择基于 zookeeper 来开发 HA 高可用机制,就是一个重要进程一般会做主备两个,主进程挂了立马通过 zookeeper 感知到切换到备用进程。


使用redis如何实现分布式锁?

官方叫做RedLock算法.

redis最普通的分布式锁

在redis里创建一个key

SET my:lock 随机值 NX PX 30000
  • NX : 表示只有key不存在的时候才会设置成功(如果key存在,那么设置失败)
  • PX 30000 :意思是30秒后自动释放锁.

释放锁就是删除key,一般可以用lua脚本删除,判断valule一样才删除

RedLock算法

这个场景是假设有一个 redis cluster,有 5 个 redis master 实例。然后执行如下步骤获取一把锁:

  1. 获取当前时间戳,单位是毫秒;
  2. 跟上面类似,轮流尝试在每个 master 节点上创建锁,过期时间较短,一般就几十毫秒;
  3. 尝试在大多数节点上建立一个锁,比如 5 个节点就要求是 3 个节点 n / 2 + 1
  4. 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了;
  5. 要是锁建立失败了,那么就依次之前建立过的锁删除;
  6. 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁

zk分布式锁

zk 分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时 znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个 znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。

redis分布式锁和zk分布式锁的对比
  • edis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
  • zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值