程序员社区

两万字深度讲解系统设计!超详细解析!面试复习必备!

Table of Contents generated with DocToc

三高

高并发

高并发是现在互联网分布式框架设计必须要考虑的因素之一,它是可以保证系统能被同时并行处理很多请求,对于高并发来说,它的指标有:

  • 响应时间:系统对进来的请求反应的时间,比如你打开一个页面需要1秒,那么这1秒就是响应时间。
  • 吞吐量:吞吐量是指每秒能处理多少请求数量,好比你吃饭,每秒能吃下多少颗米饭。
  • 秒查询率:秒查询率是指每秒响应请求数,和吞吐量差不多。
  • 并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。

高性能

什么是高性能呢?高性能是指程序处理速度非常快,所占内存少,cpu占用率低。高性能的指标经常和高并发的指标紧密相关,想要提高性能,那么就要提高系统发并发能力,两者互相捆绑在一起。应用性能优化的时候,对于计算密集型和IO密集型还是有很大差别,需要分开来考虑。还有可以增加服务器的数量,内存,IO等参数提升系统的并发能力和性能,但不要浪费资源,要考虑硬件的使用率最高才能发挥到极致。

怎么样提高性能呢?

  1. 避免因为IO阻塞让CPU闲置,导致CPU的浪费
  2. 避免多线程间增加锁来保证同步,导致并行系统串行化
  3. 避免创建、销毁、维护太多进程、线程,导致操作系统浪费资源在调度上

高可用

高可用通常来描述一个系统经过专门的设计,从而减少停工时间,而保持其服务的高度可用性。高可用注意如果使用单机,一旦挂机将导致服务不可用,可以使用集群来代替单机,一台服务器挂了,还有其他后备服务器能够顶上。或者使用分布式部署项。比如现在redis的高可用的集群方案有: Redis单副本,Redis多副本(主从),Redis Sentinel(哨兵),Redis Cluster,Redis自研。

网站统计IP PV UV实现原理

  • PV(访问量):Page View, 即页面浏览量或点击量,用户每次刷新即被计算一次。
  • UV(独立访客):Unique Visitor,一般使用cookie标记,访问您网站的一台电脑客户端(比如一台电脑开多个浏览器访问则为多个UV)为一个访客,00:00-24:00内相同的客户端只会被计算一次。
  • IP(独立IP):指独立IP数。00:00-24:00内相同IP地址之被计算一次(多台电脑可能共用一个ip)。

ip、pv、uv的区别:

  • IP(独立IP):某IP地址的计算机访问网站的次数。这种统计方式很容易实现,具有真实性。所以是衡量网站流量的重要指标。
  • PV(访问量):PV反映的是浏览某网站的页面数,所以每刷新一次也算一次。就是说PV与来访者的数量成正比,但PV并不是页面的来访者数量,而是网站被访问的页面数量。
  • UV(独立访客):可以理解成访问某网站的电脑的数量。网站判断来访电脑的身份是通过来访电脑的cookies实现的。如果更换了IP后但不清除cookies,再访问相同网站,该网站的统计中UV数是不变的。

工作流程:

  • 1、编写监控javascript和提供接口。这个接口返回的是监控网站对应的javascript文件,这个文件可以再客户端可以标记和采集访客的信息。
  • 2、网站调用接口。只需将引入javascript到要监控的站点即可,访客访问该站点时,javascript文件就会被加载。
  • 3、标记和采集数据。监控js被加载后就会往浏览器写入cookie标记访客,比如新访客生产一个新cookie和标记访问次数,若是老用户则,读取 cookie信息,计算访问次数和最后访问时间等,这些客户端的信息处理完后,则向指定的服务器发送数据。
  • 4、最后服务器接收javascript提交过来的数据处理入库和后续的数据处理了。

如何进行系统拆分?

可以从纵向和横向来拆分:

  • 纵向拆分:从业务维度进行拆分。标准是按照业务的关联程度来决定,关联比较密切的业务适合拆分为一个微服务,而功能相对比较独立的业务适合单独拆分为一个微服务。
  • 横向拆分:从公共且独立功能维度拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源独立不与其他业务耦合。
  1. 单一职责、高内聚低耦合;
  2. 服务粒度适中;
  3. 考虑团队结构:(康威定律:设计系统的组织其产生系统的设计和架构等价于组织间的沟通结构。就是指每个团队开发设计和测试发布自己团队的微服务时,要互不干扰系统效率才能得到提升,)
  4. 以业务模型切入:(领域模型:对具体某个边界领域的抽象,反应了领域内用户业务需求的本质。领域模型只反应业务与任何技术是现实没有关系的,不仅反应领域间的实体概念还能反应领域间的过程概念)
  5. 演进式的拆分;
  6. 避免环形依赖与双向依赖:(如:商品服务于订单服务相互依赖)

场景题:设计判断论文抄袭的系统

  1. 一类是基于字符串比较的方法;另一类是基于词频统计的方法。
  2. 基于字符串比较的方法也称为数字指纹法,这类方法通过某种选取策略在文档中取一些字符串作为“指纹”,把指纹映射到Hash 表中,最后统计Hash
  3. 表中相同的指纹数目或者比率,作为文本相似度依据。
  4. 基于词频统计的方法也称为基于语义的方法。词频统计法源于信息检索技术中的向量空间模型,该类方法首先都要统计每篇文档中各个单词的出现次数,然后根据单词频度构成文档特征向量,最后采用点积、余弦或者类似方式度量两篇文档的特征向量,以此作为文档相似度的依据。

设计一个即时聊天的系统

  1. 用户通过客户端进入系统,向服务器发出消息,请求登陆。
  2. 服务器收到请求后,向客户端返回应答消息,表示同意接受该用户加入,并顺带将自己服务线程所在的监听端口号告诉用户。
  3. 客户端按照服务器应答中给出的端口号与服务器建立稳定的连接。
  4. 服务器通过该连接将当前在线用户的列表信息传给新加入的客户端。
  5. 客户端获得了在线用户列表,就可以独立自主地与在线的其他用户通信了。
  6. 当用户退出系统时要及时地通知服务器。

分布式系统事务一致性解决方案

分布式系统事务一致性解决方案
MQ(事务消息)

举个例子,Bob向Smith转账,那我们到底是先发送消息,还是先执行扣款操作?

好像都可能会出问题。如果先发消息,扣款操作失败,那么Smith的账户里面会多出一笔钱。反过来,如果先执行扣款操作,后发送消息,那有可能扣款成功了但是消息没发出去,Smith收不到钱。除了上面介绍的通过异常捕获和回滚的方式外,还有没有其他的思路呢?

下面以阿里巴巴的RocketMQ中间件为例,分析下其设计和实现思路。

RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事物,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。细心的读者可能又发现问题了,如果确认消息发送失败了怎么办?RocketMQ会定期扫描消息集群中的事物消息,这时候发现了Prepared消息,它会向消息发送者确认,Bob的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。如下图:

两万字深度讲解系统设计!超详细解析!面试复习必备!插图

设计高并发的系统?

  1. HTML 页面静态化
    访问频率较高但内容变动较小,使用网站 HTML 静态化方案来优化访问速度。将社区
    内的帖子、文章进行实时的静态化,有更新的时候再重新静态化也是大量使用的策略。
    优势:
    一、减轻服务器负担。
    二、加快页面打开速度, 静态页面无需 访问 数据库,打开速度较动态页面有明显提高;
    三、很多搜索引擎都会优先收录静态页面,不仅被收录的快,还收录的全,容易被搜
    索引擎找到;
    四、HTML 静态页面不会受程序相关漏洞的影响,减少攻击 ,提高安全性。
  2. 图片服务器和应用服务器相分离
    现在很多的网站上都会用到大量的图片,而图片是网页传输中占主要的数据量,也是影
    响网站性能的主要因素。因此很多网站都会将图片存储从网站中分离出来,另外架构一个
    或多个服务器来存储图片,将图片放到一个虚拟目录中,而网页上的图片都用一个 URL 地
    址来指向这些服务器上的图片的地址,这样的话网站的性能就明显提高了。
    优势:
    一、分担 Web 服务器的 I/O 负载-将耗费资源的图片服务分离出来,提高服务器的性能
    和稳定性。
    二、 能够专门对图片服务器进行优化-为图片服务设置有针对性的 缓存方案,减少带宽
    成本,提高访问速度。
    三、 提高网站的可扩展性-通过增加图片服务器,提高图片吞吐能力。
  3. 数据库
    见“数据库部分的—如果有一个特别大的访问量到数据库上,怎么做优化?”。
  4. 缓存
    尽量使用缓存,包括用户缓存,信息缓存等,多花点内存来做缓存,可以大量减少与
    数据库的交互,提高性能。
    假如我们能减少数据库频繁的访问,那对系统肯定大大有利的。比如一个电子商务系
    统的商品搜索,如果某个关键字的商品经常被搜,那就可以考虑这部分商品列表存放到缓
    存(内存中去),这样不用每次访问数据库,性能大大增加。
  5. 镜像
    镜像是冗余的一种类型,一个磁盘上的数据在另一个磁盘上存在一个完全相同的副本
    即为镜像。
  6. 负载均衡
    在网站高并发访问的场景下,使用负载均衡技术(负载均衡服务器)为一个应用构建
    一个由多台服务器组成的服务器集群,将并发访问请求分发到多台服务器上处理,避免单
    一服务器因负载压力过大而响应缓慢,使用户请求具有更好的响应延迟特性。
  7. 并发控制
    加锁,如乐观锁和悲观锁。
  8. 消息队列
    通过 mq 一个一个排队方式,跟 12306 一样。

负载均衡的几种类型

  1. HTTP重定向负载均衡
  • 根据用户的HTTP请求计算一台应用集群中服务器的地址,并将此地址写入HTTP重定向响应中返回给用户,需要浏览器请求两次服务器才能完成。
  1. DNS域名解析负载均衡
  • 利用DNS处理域名解析请求的同时进行负载均衡处理。在DNS中配置多个A记录,每次域名解析请求都会根据负载均衡算法计算一个不同的IP地址返回。

  • 将负载均衡的工作转交给DNS,省掉了网站管理维护负载均衡服务器的麻烦,同时还可以使用智能DNS可以基于地理位置或者ISP来做域名解析,用户将会得到距离最近或者速度最快的一个服务器地址,这样可以加快用户的访问速度,改善性能。

  • 这种方法的缺点,DNS是多级解析,每一级都会缓存DNS记录,如果某个服务器变动了,DNS记录更新的时间将会很长,这个速度取决于域名服务商。

  • 一般大型网站都会使用DNS域名解析,利用域名解析作为一级负载均衡手段。你可以使用 dig <域名> 的方法查看某个域名的A记录,你会发现很多网站会有多条A记录。

  1. 反向代理负载均衡
  • 由于web服务器不直接对外提供访问,因此web服务器不需要使用外部IP,而反向代理服务器则需要配置双网卡和内部外部两套IP地址。

  • 反向代理服务器转发请求是在HTTP协议层面,因此也叫应用层负载均衡,由于应用层在七层网络模型中的第七层,所以一般也称为七层负载均衡。

  • 优点是和反向代理功服务器功能集成在一起,部署简单。

  • 缺点是反向代理服务器是所有请求和响应的中转站,其性能可能会成为瓶颈。

  1. 网络层负载均衡
  • 通过修改请求目标地址进行负载均衡,网络层在七层网络层模型的第四层,所以也叫做四层负载均衡,也叫做IP层负载均衡。

  • 请求达到负载均衡服务器后,由负载均衡服务器在操作系统内核进程获取网络数据包,根据负载均衡算法得到一台真实web服务器的地址,然后修改请求的目的地址到这台真实的web服务器地址,等到web服务器处理完成后,响应数据包回到负载均衡服务器,再将数据包源地址修改为自身的IP(负载均衡服务器的IP)地址发送给用户浏览器

  • 网络层的负载均衡在内核进程完成数据转发,有更好的性能。但是由于响应请求的流量要经过负载均衡服务器,容易成为瓶颈。

  1. 数据链路层负载均衡
  • 数据链路层主要处理 mac 地址,所以使用修改mac地址进行转发请求。负载均衡数据分发过程中不修改IP地址,只修改mac地址,通过配置真实物理服务器集群所有机器虚拟IP和负载均衡服务器IP地址一致,从而达到不修改数据包的源地址和目的地址就可以进行数据分发的目的。由于web服务器的服务器地址IP和数据请求目的IP地址一致,不需要通过负载均衡服务器进行地址转换,可将相应数据包直接返回用户。如果有足够的公有IP,其实web服务器也可以直接使用自己的IP响应请求,不过这样web服务器必须绑定负载均衡的虚拟IP地址(VIP),才能保证web服务器收到来自负载均衡发送的数据包。

  • 这种方式称作三角传输模式,单臂模式,也叫做直接路由方式(DR)。使用DR方式的链路层负载均衡是目前大型网站使用最广的一种负载均衡手段。

设计高负载的系统

  1. 应用无状态
  2. 有效使用缓存
  3. 应用拆分
  4. 数据库拆分
  5. 异步通信
  6. 非结构化数据存储 ( TFS,NOSQL)
  7. 监控、预警系统
  8. 配置统一管理

订票系统,某车次只有一张火车票,假定有 1w 个人同时打开 12306 网站来订票,如何解决并发问题?(可扩展到任何高并发网站要考虑的 并发读写问题

使用乐观锁,乐观锁意思是不锁定表的情况下,利用业务的控制来解决并发问题,这样既保证数据的并发 可读性 ,又保证保存数据的 排他性,保证性能的同时解决了并发带来
的脏数据问题。hibernate 中实现乐观锁。(乐观锁,使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。)

分布式与集群的区别是什么

分布式:一个业务分拆多个子业务,部署在不同的服务器上
集群:同一个业务,部署在多个服务器上

实时展现热门文章,比如近8小时点击量最大的文章前100名

  1. 数据接收
  • 客户端会为了减轻服务器的压力而选择延迟合并点击请求进行批量发送
  • 服务器肯定会有多台机器多进程部署来接受点击请求,接收到的请求在进行参数解析后,被发送到存储单元。为了减轻存储的压力,每个进程可能会使用小窗口聚合数据,每隔一小段时间将窗口内的数据聚合起来一起发给存储单元。
  1. 数据存储
  • 使用kafka存,ZeroCopy机制并发量很高,数据持久化在磁盘里成本低。不过kafka的数据一般是有过期时间的,如果想完全记住用户的点击以便做长期的数据分析,需要要使用hdfs了
  1. 分布式TopN算法
  • 用户太多,用户表按用户ID哈希分成了1024张子表。用户表里有一个字段score,表示这个用户的积分数。现在我们要计算前100名积分最多的用户以及积分数,该怎么查询?
  • 如果是单个表,一个SQL也就搞定了,
  • 如果是多个子表,你得在每个子表上都进行一次TopN查询,然后聚合结果再做一次TopN查询。子表查询可以多线程并行,提高聚合效率。
  1. 滑动窗口
  • 8小时的滑动窗口,意味着新的数据源源不断的进来,旧的数据时时刻刻在淘汰,在业务可以差几分钟。
  • 我们对时间片进行了切分,一分钟一个槽来进行计数,过期了8小时,移掉第一个,计算topn的帖子,维护窗口,移除过期的槽,然后统计topn,30s~60s调用一次
  1. 定时任务
  • 每个子节点都会有一个定时任务去负责维持统计窗口,过期失效的统计数据,计算局部的topn热帖。
  • 现在每个子节点都有了各自的局部topn热帖,那么还需要一个主节点去汇总这些局部热点,然后计算去全局热帖。
  1. 点击去重
  • 首先要从客户端下手,客户端本身可以过滤一部分无效点击。同一篇文章在太短的时间内被当前用户反复点击,这个模式还是很好发现的。如果间隔时间比较长,那就是读者的回味点击,属于文章的正向反馈,应该记录下来
  • 服务器还需要防止用户的防刷行为。如果缺失防刷控制,可以通过这种漏洞来使得自己的文章非法获得大量点击,进入热门文章列表,打上热门标签,被海量的用户看到,就会获得较大的经济效益,即使这篇文章内容本身吸引力并不足够。

如何解决电商网站超卖现象

超卖是什么

  • 因为数据库底层的写操作和读操作可以同时进行,虽然写操作默认带有隐式锁(即对同一数据不能同时进行写操作)但是读操作默认是不带锁的,所以当用户1去修改库存的时候,用户2依然可以读到库存为1,导致两个用户同时减一次库存,所以出现了超卖现象。

解决方案

  1. 使用redis预减库存
  2. 当库存大于0,才能更新库存update sk_goods_seckill set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0
  3. 添加唯一索引,UNIQUE KEY u_uid_gid (user_id,goods_id) USING BTREE,防止同一用户同一商品下两次订单
  4. 乐观锁,就是在数据库设计一个版本号的字段,每次修改都使其+1,这样在提交时比对提交前的版本号就知道是不是并发提交了,但是有个缺点就是只能是应用中控制,如果有跨应用修改同一条数据乐观锁就没办法了,这个时候可以考虑悲观锁。
  5. 悲观锁,就是直接在数据库层面将数据锁死,类似于oralce中使用select xxxxx from xxxx where xx=xx for update,这样其他线程将无法提交数据。
  6. 使用消息队列异步下单

如何避免下重复订单

  1. 当进入商品详情页时,去生成一个全局唯一ID(可用雪花算法);
  2. 将这个全局唯一ID和订单信息传给服务器;
  3. 判断这个ID对应的订单号存在,则直接返回;
  4. 生成订单号,保存订单信息;

如何实现下订单后一个小时后未付款的订单自动取消

为了避免轮询并且在服务端主动取消订单,可以使用类似于消息队列的方式,比如 redis 的 pub/sub 服务。

  1. DelayQueue 延时队列,此队列放入的数据需要实现java.util.concurrent.Delayed接口,用户存放待取消订单
  2. redis 分布式缓存,用于存放待取消订单,数据可以长久存放,不受服务启停影响
  3. 监听器,监听某一事件,执行一定的逻辑

有关微服务拆分思想?如何进行微服务的拆分?

  1. 根据业务能力拆分

业务能力是业务架构模型中的一个概念。业务模型经常对应于一个业务对象,比如说:订单管理负责订单,客户管理负责客户。

  1. 根据子域拆分

定义对应于领域驱动设计(DDD)的子域的服务。 一个领域由多个子域组成。每个子域对应了业务的不同组成部分。子域可以被这样分类:

  • 核心: 业务的核心区分点,应用的最有价值的部分
  • 支持: 与业务是做什么的相关,但不是主要区分点。这个可以自己做或者外包。
  • 通用: 不特定于业务,理想情况下使用现成的软件来实现

分布式集群怎么生成唯一id

  1. 数据库自增长序列或字段
  2. UUID
  3. UUID的变种
  4. Redis生成ID:主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现
  5. Twitter的snowflake算法:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。
  6. 利用zookeeper生成唯一ID:zookeeper主要通过其znode数据版本来生成序列号,可以生成32位和64位的数据版本号,客户端可以使用这个版本号来作为唯一的序列号。
  7. MongoDB的ObjectId:MongoDB的ObjectId和snowflake算法类似。它设计成轻量型的,不同的机器都能用全局唯一的同种方法方便地生成它。MongoDB 从一开始就设计用来作为分布式数据库,处理多个节点是一个核心要求。使其在分片环境中要容易生成得多。

如何保证接口的幂等性?

全局唯一ID

如果使用全局唯一ID,就是根据业务的操作和内容生成一个全局ID,在执行操作前先根据这个全局唯一ID是否存在,来判断这个操作是否已经执行。如果不存在则把全局ID,存储到存储系统中,比如数据库、redis等。如果存在则表示该方法已经执行。

从工程的角度来说,使用全局ID做幂等可以作为一个业务的基础的微服务存在,在很多的微服务中都会用到这样的服务,在每个微服务中都完成这样的功能,会存在工作量重复。另外打造一个高可靠的幂等服务还需要考虑很多问题,比如一台机器虽然把全局ID先写入了存储,但是在写入之后挂了,这就需要引入全局ID的超时机制。

使用全局唯一ID是一个通用方案,可以支持插入、更新、删除业务操作。但是这个方案看起来很美但是实现起来比较麻烦,下面的方案适用于特定的场景,但是实现起来比较简单。

去重表

这种方法适用于在业务中有唯一标的插入场景中,比如在以上的支付场景中,如果一个订单只会支付一次,所以订单ID可以作为唯一标识。这时,我们就可以建一张去重表,并且把唯一标识作为唯一索引,在我们实现时,把创建支付单据和写入去去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。

插入或更新

这种方法插入并且有唯一索引的情况,比如我们要关联商品品类,其中商品的ID和品类的ID可以构成唯一索引,并且在数据表中也增加了唯一索引。这时就可以使用InsertOrUpdate操作。在mysql数据库中如下:

insert into goods_category (goods_id,category_id,create_time,update_time) 
       values(#{goodsId},#{categoryId},now(),now()) 
       on DUPLICATE KEY UPDATE
       update_time=now()

多版本控制

这种方法适合在更新的场景中,比如我们要更新商品的名字,这时我们就可以在更新的接口中增加一个版本号,来做幂等

boolean updateGoodsName(int id,String newName,int version);

在实现时可以如下

update goods set name=#{newName},version=#{version} where id=#{id} and version<${version}

状态机控制

这种方法适合在有状态机流转的情况下,比如就会订单的创建和付款,订单的付款肯定是在之前,这时我们可以通过在设计状态字段时,使用int类型,并且通过值类型的大小来做幂等,比如订单的创建为0,付款成功为100。付款失败为99

在做状态机更新时,我们就这可以这样控制

update `order` set status=#{status} where id=#{id} and status<#{status}

mq异步调用失败,如何保证数据一致性?

  1. 按你的使用场景,推送数据必须得在数据创建事务成功之后执行,这里必须有个先后。你可以将推送这个操作异步执行,消息队列有一搬有ack机制,确保消息没丢失。这时候监听消息队列的程序会执行推送,如果推送成功做标记。如果推送失败也标记记录时间,也可以推到另一个消息队列约定多少分钟重试。实在不行就彻底标记失败,或者回滚之前创建的数据。这个才是最终一致性。
  2. 如果是并行的操作,就得使用消息队列的confirm机制了。

分布式锁的几种实现方式

  1. 基于数据库表
  • 要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。

  • 我们对method_name(方法名)做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。当方法执行完毕之后,想要释放锁的话就删除该记录

  1. 基于数据库排他锁
  • 可以通过数据库的排他锁来实现分布式锁
  • 在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁(这里再多提一句,InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给method_name添加索引
  • 值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。
  1. 基于缓存
  • 比如Tair的put方法,redis的setnx方法等。并且,这些缓存服务也都提供了对数据的过期自动删除的支持,可以直接设置超时时间来控制锁的释放。
  1. 基于Zookeeper
  • 每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

乐观锁冲突并发量很大怎么办

用消息队列一个个处理

消息队列

四种MQ区别

两万字深度讲解系统设计!超详细解析!面试复习必备!插图1

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

  1. 集群,以rcoketMQ为例,有多master 模式、多master多slave异步复制模式、多 master多slave同步双写模式。

两万字深度讲解系统设计!超详细解析!面试复习必备!插图2
Producer 与 NameServer集群中的其中一个节点(随机选择)建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Broker Master 建立长连接,且定时向 Broker 发送心跳。Producer 只能将消息发送到 Broker master,但是 Consumer 则不一样,它同时和提供 Topic 服务的 Master 和 Slave建立长连接,既可以从 Broker Master 订阅消息,也可以从 Broker Slave 订阅消息。

如何保证消息不被重复消费

原因:

  • 消费者在消费后,会发送一个确认信息给消息队列,消息队列收到后会将该消息从消息队列中删除,例如RabbitMQ是发送一个ACK确认消息,RocketMQ是返回一个CONSUME_SUCCESS成功标志,kafka每一个消息都有一个offset,kafka消费过消息后,需要提交offset,让消息队列知道自己已经消费过了。因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将该消息分发给其他的消费者。

解决方法:

  1. 消息可以使用唯一id标识
  2. 如果拿到这个消息做数据库的insert操作。给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
  3. 如果拿到这个消息做redis的set的操作,无论set几次结果都是一样的,set操作是算幂等操作。
  4. 使用第三方介质做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将< id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。

如何防止Cookie被篡改

  1. 如果是要防止在传输中被窃取或篡改,请使用 TLS
  2. 如果是要防止客户端篡改,请在 cookie 中加入 MAC(消息验证码),比如可以使用 HMAC-SHA256 算法生成。这样,在从cookies得到数据后,再判断一下这个MAC码就可以知道整个cookies字段是否被篡改过。
  3. 利用动态密码与Session+Cookies双重验证

分布式 ID 生成器

一个唯一 ID 在一个分布式系统中是非常重要的一个业务属性,其中包括一些如订单 ID,消息 ID ,会话 ID,他们都有一些共有的特性:

  • 全局唯一。
  • 趋势递增。
  1. 基于数据库,可以利用 MySQL 中的自增属性 auto_increment 来生成全局唯一 ID,也能保证趋势递增。 但这种方式太依赖 DB,如果数据库挂了那就非常容易出问题。
  2. 本地UUID生成,还可以采用 UUID 的方式生成唯一 ID,由于是在本地生成没有了网络之类的消耗,所有效率非常高。生成的 ID 是无序性的,不能做到趋势递增。由于是字符串并且不是递增,所以不太适合用作主键。
  3. 采用本地时间,这种做法非常简单,可以利用本地的毫秒数加上一些业务 ID 来生成唯一ID,这样可以做到趋势递增,并且是在本地生成效率也很高。当并发量足够高的时候唯一性就不能保证了。
  4. Twitter雪花算法,可以基于 Twitter 的 Snowflake 算法来实现。它主要是一种划分命名空间的算法,将生成的 ID 按照机器、时间等来进行标志。

Redis和RabbitMQ用作消息队列的区别

可靠性

  1. Redis没有相应的机制保证消息的可靠消费,如果发布者发布一条消息,而没有对应的订阅者的话,这条消息将丢失,不会存在内存中。
  2. RabbitMQ具有消息消费确认机制,如果发布一条消息,还没有消费者消费该队列,那么这条消息将一直存放在队列中,直到有消费者消费了该条消息,以此可以保证消息的可靠消费。

消费者负载均衡

  1. Redis发布订阅模式,一个队列可以被多个消费者同时订阅,当有消息到达时,会将该消息依次发送给每个订阅者。
  2. RabbitMQ队列可以被多个消费者同时监控消费,但是每一条消息只能被消费一次,由于rabbitmq的消费确认机制,因此它能够根据消费者的消费能力而调整它的负载。

持久性

  1. Redis的持久化是针对于整个redis缓存的内容,它有RDB和AOF两种持久化方式(redis持久化方式,后续更新),可以将整个redis实例持久化到磁盘,以此来做数据备份,防止异常情况下导致数据丢失。
  2. RabbitMQ队列,消息,都可以选择是否持久化,持久化粒度更小,更灵活;

队列监控

  1. Redis没有所谓的监控平台。
  2. RabbitMQ实现了后台监控平台,可以在该平台上看到所有创建的队列的详细情况,良好的后台管理平台可以方面我们更好的使用。

总结

redis:轻量级,高并发,延迟敏感,即时数据分析、秒杀计数器、缓存等

rabbitmq:重量级,高并发,异步,批量数据异步处理、并行任务串行化,高负载任务的负载均衡等

rabbitmq是一个专门的AMQP协议队列,他的优势就在于提供可靠的队列服务,并且可做到异步,而redis主要是用于缓存的,redis的发布订阅模块,可用于实现及时性,且可靠性低的功能。

CAP原则

CAP原则又称CAP定理,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。

分布式系统的CAP理论:理论首先把分布式系统中的三个特性进行了如下归纳:

  • 一致性(C):所有的节点上的数据时刻保持同步。
  • 可用性(A):每个请求都能接受到一个响应,无论响应成功或失败。
  • 分区容忍性(P):系统应该能持续提供服务,即使系统内部有消息丢失。(分区)

下表是抛弃CAP中任意一项特性的场景说明:

CAP 说明
放弃C 这里所说的放弃一致性,实际上指的是放弃数据的强一致性,而保留数据的最终一致性。这样的系统无法保证数据保持实时的一致性,但是能够承诺的是,数据最终会达到一个一致的状态
放弃A 这里所说的放弃可用性,一旦系统遇到网络分区或其他故障或为了保证一致性时,放弃可用性,那么受到影响的服务需要等待一定的时间,因此在等待期间系统无法对外提供正常的服务,即不可用
放弃P 这里所说的放弃分区容忍性,如果希望能够避免系统出现分区容错性问题,一种较为简单的做法是将所有的数据(或者仅仅是哪些与事务相关的数据)都放在一个分布式节点上。这样做虽然无法100%保证系统不会出错,但至少不会碰到由于网络分区带来的负面影响。但同时需要注意的是,放弃P的同时也就意味着放弃了系统的可扩展性

需要明确的一点是:对于一个分布式系统而言,分区容错性可以说是一个最基本的要求。因为既然是一个分布式系统,那么分布式系统中的组件必然需要被部署到不同的节点,否则也就无所谓的分布式系统了,因此必然出现子网络。而对于分布式系统而言,网络问题又是一个必定会出现的异常情况,因此分区容错性也就成为了一个分布式系统必然需要面对和解决的问题。因此系统架构师往往需要把精力花在如何根据业务特点在C(一致性)和A(可用性)之间寻求平衡。

BASE理论

BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent
(最终一致性)三个短语的简写,是基于CAP定理逐步演化而来的,其核心思想是即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方法来使系统达到最终一致性。

1、基本可用

基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性,但请注意,这绝不等价于系统不可用。一下就是两个"基本可用"的例子。

  • 响应时间上的损失:系统正常0.5秒能返回结果,由于出现故障,响应时间增加到了1~2秒
  • 功能上的损失:高并发情况下,为了保护系统的稳定性(或者保证一致性),部分消费者可能会被引导到一个降级页面(类似"高峰期,稍后再试"页面)

2、软状态

软状态是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同的数据副本之间进行数据同步的过程存在延时。

3、最终一致性

最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

ACID和BASE的区别

ACID追求强一致性,BASE追求最终一致性

一致性协议,从2PC到3PC到Paxos到ZAB

2PC(Two-Phase Commit,两阶段提交)

所谓的两个阶段是指:

  • 第一阶段:准备阶段 (投票阶段)
  • 第二阶段:提交阶段(执行阶段)

1、阶段一:提交事务请求

  • (1)事务询问:协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应
  • (2)执行事务:各参与者节点执行事务操作,并将Undo和Redo信息计入事务日志中
  • (3)各参与者向协调者反馈事务询问的响应:如果参与者成功执行了事务操作,那么就反馈给协调者Yes响应,表示事务可以执行;如果参与者没有成功执行事务,那么就反馈给协调者No响应,表示事务不可以执行。

2、阶段二:执行事务提交

第二阶段有两种情况:

(1)执行事务提交

如果所有参与者的反馈都是Yes响应,那么

  • A、发送提交请求:协调者向所有参与者节点发出Commit请求
  • B、事务提交:参与者接收到Commit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源
  • C、反馈事务提交结果:参与者在完成事务提交之后,向协调者发送ACK信息
  • D、完成事务:协调者接收到所有参与者反馈的ACK消息后,完成事务

(2)中断事务

任何一个参与者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。

  • A、发送回滚请求:协调者向所有参与者节点发出Rollback请求
  • B、事务回滚:参与者接收到rollback请求后,会利用其在阶段一中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放整个事务执行期间占用的资源
  • C、反馈事务回滚结果:参与者在完成事务回滚之后,向协调者发送ACK信息
  • D、中断事务:协调者接收到所有参与者反馈的ACK信息后,完成事务中断

优缺点

  • 优点:原理简单、实现方便
  • 缺点:同步阻塞、单点问题、数据不一致、太过保守

3PC(Three-Phase Commit,三阶段提交)

Three-Phase Commit,三阶段提交,分为CanCommit、PreCommit、do Commit三个阶段。

为了避免在通知所有参与者提交事务时,其中一个参与者crash不一致时,就出现了三阶段提交的方式。三阶段提交在两阶段提交的基础上增加了一个preCommit的过程,当所有参与者收到preCommit后,并不执行动作,直到收到commit或超过一定时间后才完成操作。

1、阶段一CanCommit

  • (1)事务询问:协调者向各参与者发送CanCommit的请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应

  • (2)参与者向协调者反馈询问的响应:参与者收到CanCommit请求后,正常情况下,如果自身认为可以顺利执行事务,那么会反馈Yes响应,并进入预备状态,否则反馈No。

2、阶段二PreCommit

(1)执行事务预提交

如果协调者接收到各参与者反馈都是Yes,那么执行事务预提交

  • A、发送预提交请求:协调者向各参与者发送preCommit请求,并进入prepared阶段
  • B、事务预提交:参与者接收到preCommit请求后,会执行事务操作,并将Undo和Redo信息记录到事务日记中
  • C、各参与者向协调者反馈事务执行的响应:如果各参与者都成功执行了事务操作,那么反馈给协调者Ack响应,同时等待最终指令,提交commit或者终止abort

(2)中断事务

如果任何一个参与者向协调者反馈了No响应,或者在等待超时后,协调者无法接收到所有参与者的反馈,那么就会中断事务。

  • A、发送中断请求:协调者向所有参与者发送abort请求
  • B、中断事务:无论是收到来自协调者的abort请求,还是等待超时,参与者都中断事务

3、阶段三doCommit

(1)执行提交

  • A、发送提交请求:假设协调者正常工作,接收到了所有参与者的ack响应,那么它将从预提交阶段进入提交状态,并向所有参与者发送doCommit请求
  • B、事务提交:参与者收到doCommit请求后,正式提交事务,并在完成事务提交后释放占用的资源
  • C、反馈事务提交结果:参与者完成事务提交后,向协调者发送ACK信息
  • D、完成事务:协调者接收到所有参与者ack信息,完成事务

(2)中断事务

假设协调者正常工作,并且有任一参与者反馈No,或者在等待超时后无法接收所有参与者的反馈,都会中断事务

  • A、发送中断请求:协调者向所有参与者节点发送abort请求
  • B、事务回滚:参与者接收到abort请求后,利用undo日志执行事务回滚,并在完成事务回滚后释放占用的资源
  • C、反馈事务回滚结果:参与者在完成事务回滚之后,向协调者发送ack信息
  • D、中断事务:协调者接收到所有参与者反馈的ack信息后,中断事务。

阶段三可能出现的问题:

协调者出现问题、协调者与参与者之间网络出现故障。不论出现哪种情况,最终都会导致参与者无法及时接收到来自协调者的doCommit或是abort请求,针对这种情况,参与者都会在等待超时后,继续进行事务提交(timeout后中断事务)。

优缺点

  • 优点:降低参与者阻塞范围,并能够在出现单点故障后继续达成一致
  • 缺点:引入preCommit阶段,在这个阶段如果出现网络分区,协调者无法与参与者正常通信,参与者依然会进行事务提交,造成数据不一致。

Paxos算法

Paxos算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。

Paxos 算法解决的问题是在一个可能发生上述异常的分布式系统中如何就某个值达成一致,保证不论发生以上任何异常,都不会破坏决议的一致性。一个典型的场景是,在一个分布式数据库系统中,如果各节点的初始状态一致,每个节点都执行相同的操作序列,那么他们最后能得到一个一致的状态。为保证每个节点执行相同的命令序列,需要在每一条指令上执行一个「一致性算法」以保证每个节点看到的指令一致。

简单说来,Paxos的目的是让整个集群的结点对某个值的变更达成一致。Paxos算法基本上来说是个民主选举的算法——大多数的决定会成个整个集群的统一决定。任何一个点都可以提出要修改某个数据的提案,是否通过这个提案取决于这个集群中是否有超过半数的结点同意(所以Paxos算法需要集群中的结点是单数)

ZAB协议

ZAB(Zookeeper Atomic Broadcast)协议是专门为zookeeper设计的一致性协议。

ZAB协议包括两种基本的模式:消息广播和崩溃恢复

  1. 当整个服务框架在启动过程中,或是当Leader服务器出现网络中断崩溃退出与重启等异常情况时,ZAB就会进入恢复模式并选举产生新的Leader服务器。

  2. 当选举产生了新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后,ZAB协议就会退出崩溃恢复模式,进入消息广播模式。

  3. 当有新的服务器加入到集群中去,如果此时集群中已经存在一个Leader服务器在负责进行消息广播,那么新加入的服务器会自动进入数据恢复模式,找到Leader服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。

以上其实大致经历了三个步骤:

  1. 崩溃恢复:主要就是Leader选举过程。
  2. 数据同步:Leader服务器与其他服务器进行数据同步。
  3. 消息广播:Leader服务器将数据发送给其他服务器。

ZAD和Paxos算法的联系和区别

共同点:

  1. 两者都存在一个类似于Leader进程的角色,由其负责协调多个Follow进程的运行。
  2. Leader进程都会等待超过半数的Follower做出正确的反馈后,才会将一个提案进行提交。
  3. 在ZAB协议中,每个Proposal中都包含了一个epoch值,用来代表当前Leader周期,在Paxos算法中,同样存在这样一个标识,只是名字变成了Ballot。

不同点:

Paxos算法中,一个新的选举产生的主进程会进行两个阶段的工作

  1. 读阶段,新的主进程会通过和所有其他进程进行通信的方式来搜集上一个主进程提出的提案,并将它们提交。
  2. 写阶段,当前主进程开始提出它自己的提案。

ZAB在Paxos基础上额外添加一个同步阶段。同步阶段之前,ZAB协议存在一个和Paxos读阶段类似的发现(Discovery)阶段

  • 同步阶段中,新的Leader会确保存在过半的Follower已经提交了之前Leader周期中的所有事务Proposal
  • 发现阶段的存在,确保所有进程都已经完成对之前所有事物Proposal的提交

ZAB协议主要用于构建一个高可用的分布式数据主备系统,例如ZooKeeper,Paxos算法则是用于构建一个分布式的一致性状态机系统

服务端主动推送怎么实现

  1. ajax:这是通过模拟服务器发起的通信,不是实时通信, 就是需要不断的向服务器发送消息询问,这种方式会对服务器造成极大的性能浪费。
  2. comet:基于 HTTP 长连接的 “服务器推” 技术,能使服务器端主动以异步的方式向客户端程序推送数据,而不需要客户端显式的发出请求
  3. websocket: 浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

RESTful接口设计

  • 本质:一种软件架构风格;
  • 核心:基于资源;
  • 作用:不同的客户端(如Web、微信小程序、APP)可以使用同一套API;
  • 方法类型:POST(新增数据)、DELETE(删除数据)、PUT(更新数据)、GET(得到数据)

注意

  1. API对应的版本号要在URL中体现。
  2. 不同的HTTP动词对应不同的操作。
  3. URL中尽量不要出现动词。

一致性哈希算法

一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,我们把这个由2^32个点组成的圆环称为Hash环。下一步将各个服务器使用Hash进行一个哈希,具体可以选择服务器的IP或主机名作为关键字进行哈希,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。

解决了什么问题?

1.使用hash存在问题,服务器数量改变,所有缓存位置都要改变
2.为了解决容错性和可扩展性,一台宕机,不会影响其他机器,只会影响前面一台
3.使用虚拟节点,解决数据倾斜问题,多了一步虚拟节点到实际节点的映射

RabbitMQ消息丢失解决

RabbitMQ消息丢失分为三种情况
两万字深度讲解系统设计!超详细解析!面试复习必备!插图3

1、消息在传入过程中丢失

生产者开启confirm模式,每次写的消息都会分配一个唯一id,然后写入rabbitmq,rabbitmq会回传ack消息,如果丢失会回调你的一个nack接口,告诉你这个消息接收失败,结合生成者内存维护每个消息id的状态,如果超过一定时间还没接收到这个消息的回调。说明可以重发

2、消息在RabbitMQ弄丢

开启RabbitMQ持久化

3、消息在消费者丢失

关闭RabbitMQ自动ACK,确保完成再ACK,否则不要ACK

如何去设计分布式监控中心?

监控中心最基本四个功能:

  1. 数据采集
  2. 实时绘图
  3. 告警
  4. 数据存储

大概原理:

  • 监控中心的agent需要安装到被监控的主机上,它负责定期收集各项数据,并发送到监控中心server,监控中心server将数据存储到数据库,监控中心web在前端进行绘图。

agent收集数据分为主动和被动两种模式:

  • 主动:agent请求server获取主动的监控项列表,并主动将监控项内需要检测的数据提交给server/proxy
  • server向agent请求获取监控项的数据,agent返回数据。

一般需要监控哪些数据:

  • 可以自己埋点获取监控信息
  • 代码运行时间统计、次数、错误次数
  • 服务器系统信息、JVM 内存/GC信息、线程信息
  • SQL语句执行时长
  • 各个进程的CPU和内存使用情况

现在市面比较流行的监控组件:
按照功能划分:
两万字深度讲解系统设计!超详细解析!面试复习必备!插图4

  • 日志:肯定是ELKB
  • 调用链APM:OpenTracing、Cat、Pinpoint、zipkin

令牌桶和漏桶使用场景?

记住一条,令牌桶会比漏桶更优就行了,99%选用令牌桶。如果请求产生的速率恒定,令牌桶和漏桶效果一样。如果请求分布不均匀,令牌桶比漏桶好。

如何设计一个接口?

接口设计需要考虑哪些方面

  1. 接口的命名。
  2. 请求参数。
  3. 支持的协议。
  4. TPS、并发数、响应时长。
  5. 数据存储。DB选型、缓存选型。
  6. 是否需要依赖于第三方。
  7. 接口是否拆分。
  8. 接口是否需要幂等。
  9. 防刷。
  10. 接口限流、降级。
  11. 负载均衡器支持。
  12. 如何部署。
  13. 是否需要服务治理。
  14. 是否存在单点。
  15. 接口是否资源包、预加载还是内置。
  16. 是否需要本地缓存。
  17. 是否需要分布式缓存、缓存穿透怎么办。
  18. 是否需要白名单。
赞(0) 打赏
未经允许不得转载:IDEA激活码 » 两万字深度讲解系统设计!超详细解析!面试复习必备!

一个分享Java & Python知识的社区