Java分布式专题

296 阅读8分钟

消息队列

为什么使用消息队列?
解耦、异步、削峰

消除队列有什么缺点?

  1. 系统可用性降低,系统引入的外部依赖越多,越容易挂掉
  2. 提高系统复杂度,要考虑消息幂等性与处理消息丢失问题
  3. 一致性问题,部分子系统处理消息失败了

kafka、ActiveMQ、RabbitMQ、RocketMQ优缺点?

  1. RabbitMQ延迟最低
  2. RocketMQ能支持大量topic,而Kafka的topic从几十到几百时,吞吐量会大幅下降
  3. 可用性:Kafka>RabbitMQ>ActiveMQ=RocketMQ
  4. 可靠性上ActiveMQ有较低的概率丢失数据
  5. 建议使用RabbitMQ,它的社区活跃度高。大数据领域的实时计算、日志采集等建议Kafka

RabbitMQ
它是基于主从(非分布式)做高可用性的。它有三种模式:单机模式、普通集群模式、镜像集群模式
普通集群模式无高可用性,你创建的queue,只会放在一个rabbitmq实例上,但是每个实例都同步queue的元数据,通过元数据,可以找到queue所在实例。当你消费时,如果连接到另外一个实例,那么那个实例会从queue所在实例上的拉取数据过来。这方案主要是提高吞吐量,就是让集群中多个节点来服务某个queue的读写操作
镜像集群模式(高可用性),每个rabbitmq节点都有某个queue的一个完整镜像。但它带来两个缺点:

  1. 性能开销大,消息需要要同步到所有机器上
  2. 没有办法线程扩展你的queue,因为一个queue的全部消息都放在一台机器上

Kafka
它是天然的分布式消息队列,就是一个topic数据,分散放在多个机器上,每个机器存放部分数据。
写数据时,生产者写leader,然后leader将数据落地本地磁盘,其它follower自己主动从leader来pull数据,一旦所有follower同步好数据,发送ack给leader,leader收到所有follower的ack之后,就会返回写成功的消息给生产者。
消费时,只会从leader去读,但是只有当一个消息已经被所有follower都同步成功返回ack时,这个消息才会被消费者读取。

如何保证MQ的幂等性?
产生原因,如kafka为每条数据分配一个offset,代表数据的序号,如果消费者消费了offset=153的这条数据,准备提交给broker,此时消费者重启了。消息过的数据的offset并没有提交,kafka就不知道你已经消费了,下次仍会投递这条消息。
解决方案,消息加上一个全局唯一的id,保存到redis中,下次去redis中查询是否消费过。

如何保证消息的可靠性传输,如何处理消息丢失问题?

  • 对于rabbitmq:
    1. 生产者开启confim机制
    2. 中间件rabbitmq开启持久化机制,设置持久化有两个步骤,一是创建queue时设置为持久化,保证rabbitmq持久化queue的元数据,二是发送消息时将消息的deliveryMode设置为2,就是将消息设置为持久化。
    3. 消费者使用手动ack机制
  • 对于kafka,存在kafka的leader机器宕机了,将follower切换为leader之后,出现数据丢失了,原因是follower刚好还有此数据没有同步完成。一般要求起码设置如下4个参数,保证kafka broker端可以保证在leader所在broker发生故障进行切换时,数据不会丢失:
    1. topic设置repliaction.factor值大于1,要求每个partition必须至少有2个副本
    2. 在kafka服务端设置min.insync.replicas大于1,要求一个leader至少感知到有至少一个follower还跟自己保持联系
    3. producer端设置acks=all,写入所有replica之后,才能认为是写成功了
    4. producer端设置retries=MAX,要求一旦写入失败,就无限重试

如何保证MQ消息的顺序性?
kafka在生产者写的时候可指定一个key,如订单id作为key,那么一定会被发到同一个partition中去,这个partition中数据一定有顺序的。一个partition需要由同一个消费者线程消费保证消费顺序不被打乱
rabbitmq可用consumer的内存队列做排队,然后分发给不同的worker来处理

如何解决消息队列的延时以及过期失效问题?
写一个临时的分发数据的程序,将消息分发到更多的queue上,然后用部署更多的consumer消费所有queue上的消息。即将queue和consumer都扩大了。如果消息在积压过程中失效了,就只能在深夜使用低峰期时将丢失的数据查出来,重新灌入到mq中去。如果mq导致磁盘满了,无法做其它操作,就只能消费一个丢弃一个,然后到了晚上再补数据。

搜索引擎

es概念 VS db概念

es db
index 数据库
type
document 行记录
field 字段

elasticsearch

搜索流程

  1. client发送请求到任意一个node,这node成为协调节点
  2. 协调节点将搜索请求转发所有的shard对应的primary shard或replica shard
  3. 每个shard将自己搜索结果的doc id返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果
  4. 最后由协调节点根据doc id,去各个节点拉取实际的document数据,返回给client

写数据

  1. 客户端选择一个node发送请求,这个node成为协调节点
  2. 协调节点对document进行路由,请请求转发对应的node(primary shard)
  3. 实际的node上的primary shard处理请求,然后将数据同步到replic node
  4. 协调节点发现primary和所有replica node都搞定之后,就返回响应结果给客户端

es原理图

在这里插入图片描述

  1. es是准实时的,数据写入1s后可搜索到
  2. es每隔5s写入日志文件,可能丢失5s数据

es在十亿级别的数据如何提高查询效率

  1. es的jvm不要分配太大,留足够的内存给filesystem cache,如果查询数据都在filesystem cache性能就能提高很多(如每次查询5~10s,放在filesystem cache就是50ms)。
  2. 只存要检索的数据到es中,使数据少,都能放到filesystem cache中。其它展示数据可以放到hbase中。
  3. 数据预热,做一个专门的缓存预热子系统,对热数据每隔一段时间访问一下,让数据进入filesystem cache中。这样别人访问,性能一定会好很多
  4. 冷热分离,将冷数据放到另一个索引中。这样可确保热数据在被预热之后,尽量都让他们在filesystem os cache里,不被冷数据刷掉
  5. 尽量不要让es做复杂的关联查询,尽量在document设计时,写入的时候就能完成
  6. es的分页越翻到后面越慢。解决方案:
    1. 不允许深度分页
    2. 使用scroll api或search_after,缺点是不能随意跳到任何一页

Redis

redis过期策略:定期删除+惰性删除
key过期不是立刻删除的,如果此时大量过期key堆积在内存里,导致redis内存耗尽,则走内存淘汰机制
redis内存淘汰默认是最近最少使用算法

缓存雪崩解决方案

产生原因redis挂了或热点数据大量同时过期,如果是后者需要通过让热点数据在不同时间段失效来解决。
事前:redis cluster保证高可用
事中:本地ehcache缓存+hystrix限流和降级,避免mysql被打死
事后:redis持久化,一旦重启能从磁盘上加载数据

缓存穿透

缓存穿透容易被人利用恶意攻击,黑客针对不存在的数据发起大量请求,导致请求都打在了mysql中,可写一个空值到缓存然后设置一个过期时间,可以让下次同一个key查询请求直接走缓存

缓存击穿

某个key访问非常频繁,当key失效时,大量请求走mysql。可基于redis或zookeeper实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求能直接走缓存。

MySQL

如何解决主从同步延时与数据丢失问题

如果从库还没有把主库数据同步过来,主库宕机了就会产生数据丢失,可通过半同步复制解决,即让主库接收到至少一个从库的ack之后才会认为写操作成功
主从数据延时问题,解决办法有:

  1. 并行复制,从库开启多个线程并行读取relay log中不同库的日志,然后并行重放不同库的日志,这是库级别的并行
  2. 分库,将一个主库拆分为多个主库,每个主库的写并发就减少了几倍,此时主从延迟可以忽略不计
  3. 写代码的同学,要注意,插入数据时立马查询可能查不到,如果写代码逻辑是先插入一条数据,再把它查出来,然后更新这条数据。可能会因无法立即查询出来而更新失败
  4. 如果确实要插入数据要立马能查询到,可对这个查询设置直连主库。不过,此时读写分离就失去意义了

高并发

如何设计一个高并发系统

  1. 系统拆分减少压力
  2. 缓存提高读请求并发量
  3. MQ削峰限流提高请求并发量
  4. 分表分库提高sql性能
  5. 读写分离提高减缓数据库压力
  6. 天然分布式的es支撑数据库的部分普通查询