分布式设计
# 分布式系统
# 分布式ID是什么?有哪些解决⽅案?
在开发中,我们通常会需要⼀个唯⼀ID来标识数据,如果是单体架构,我们可以通过数据库的主键,或 直接在内存中维护⼀个⾃增数字来作为ID都是可以的,但对于⼀个分布式系统,就会有可能会出现ID冲 突,此时有以下解决⽅案:
- uuid,这种⽅案复杂度最低,但是会影响存储空间和性能
- 利⽤单机数据库的⾃增主键,作为分布式ID的⽣成器,复杂度适中,ID⻓度较之uuid更短,但是受 到单机数据库性能的限制,并发量⼤的时候,此⽅案也不是最优⽅案
- 利⽤redis、zookeeper的特性来⽣成id,⽐如redis的⾃增命令、zookeeper的顺序节点,这种⽅案 和单机数据库(mysql)相⽐,性能有所提⾼,可以适当选⽤
- 雪花算法,⼀切问题如果能直接⽤算法解决,那就是最合适的,利⽤雪花算法也可以⽣成分布式 ID,底层原理就是通过某台机器在某⼀毫秒内对某⼀个数字⾃增,这种⽅案也能保证分布式架构中 的系统id唯⼀,但是只能保证趋势递增。业界存在tinyid、leaf等开源中间件实现了雪花算法。
雪花算法原理:
第⼀位符号位固定为0,41位时间戳,10位workId,12位序列号,位数可以有不同实现。
优点:每个毫秒值包含的ID值很多,不够可以变动位数来增加,性能佳(依赖workId的实现)。时间戳 值在⾼位,中间是固定的机器码,⾃增的序列在低位,整个ID是趋势递增的。能够根据业务场景数据库 节点布置灵活调整bit位划分,灵活度⾼。
缺点:强依赖于机器时钟,如果时钟回拨,会导致重复的ID⽣成,所以⼀般基于此的算法发现时钟回 拨,都会抛异常处理,阻⽌ID⽣成,这可能导致服务不可⽤。
# 分布式锁的使⽤场景是什么?有哪些实现⽅案?
在单体架构中,多个线程都是属于同⼀个进程的,所以在线程并发执⾏时,遇到资源竞争时,可以利⽤ ReentrantLock、synchronized等技术来作为锁,来控制共享资源的使⽤。 ⽽在分布式架构中,多个线程是可能处于不同进程中的,⽽这些线程并发执⾏遇到资源竞争时,利⽤ ReentrantLock、synchronized等技术是没办法来控制多个进程中的线程的,所以需要分布式锁,意思 就是,需要⼀个分布式锁⽣成器,分布式系统中的应⽤程序都可以来使⽤这个⽣成器所提供的锁,从⽽ 达到多个进程中的线程使⽤同⼀把锁。 ⽬前主流的分布式锁的实现⽅案有两种:
- zookeeper:利⽤的是zookeeper的临时节点、顺序节点、watch机制来实现的,zookeeper分布式 锁的特点是⾼⼀致性,因为zookeeper保证的是CP,所以由它实现的分布式锁更可靠,不会出现混 乱
- redis:利⽤redis的setnx、lua脚本、消费订阅等机制来实现的,redis分布式锁的特点是⾼可⽤, 因为redis保证的是AP,所以由它实现的分布式锁可能不可靠,不稳定(⼀旦redis中的数据出现了 不⼀致),可能会出现多个客户端同时加到锁的情况
# 什么是分布式事务?有哪些实现⽅案?
在分布式系统中,⼀次业务处理可能需要多个应⽤来实现,⽐如⽤户发送⼀次下单请求,就涉及到订单 系统创建订单、库存系统减库存,⽽对于⼀次下单,订单创建与减库存应该是要同时成功或同时失败 的,但在分布式系统中,如果不做处理,就很有可能出现订单创建成功,但是减库存失败,那么解决这 类问题,就需要⽤到分布式事务。常⽤解决⽅案有:
- 本地消息表:创建订单时,将减库存消息加⼊在本地事务中,⼀起提交到数据库存⼊本地消息表, 然后调⽤库存系统,如果调⽤成功则修改本地消息状态为成功,如果调⽤库存系统失败,则由后台 定时任务从本地消息表中取出未成功的消息,重试调⽤库存系统
- 消息队列:⽬前RocketMQ中⽀持事务消息,它的⼯作原理是: a. ⽣产者订单系统先发送⼀条half消息到Broker,half消息对消费者⽽⾔是不可⻅的 b. 再创建订单,根据创建订单成功与否,向Broker发送commit或rollback c. 并且⽣产者订单系统还可以提供Broker回调接⼝,当Broker发现⼀段时间half消息没有收到任 何操作命令,则会主动调此接⼝来查询订单是否创建成功 d. ⼀旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事 务成功结束 e. 如果消费失败,则根据重试策略进⾏重试,最后还失败则进⼊死信队列,等待进⼀步处理
- Seata:阿⾥开源的分布式事务框架,⽀持AT、TCC等多种模式,底层都是基于两阶段提交理论来 实现的
# 如何避免缓存穿透、缓存击穿、缓存雪崩?
缓存雪崩是指缓存同⼀时间⼤⾯积的失效,所以,后⾯的请求都会落到数据库上,造成数据库短时间内 承受⼤量请求⽽崩掉。 解决⽅案:
- 缓存数据的过期时间设置随机,防⽌同⼀时间⼤量数据过期现象发⽣。
- 给每⼀个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓 存。
- 缓存预热互斥锁
缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承 受⼤量请求⽽崩掉。 解决⽅案:
- 接⼝层增加校验,如⽤户鉴权校验,id做基础校验,id<=0的直接拦截;
- 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有 效时间可以设置短点,如30秒(设置太⻓会导致正常情况也没法使⽤)。这样可以防⽌攻击⽤户 反复⽤同⼀个id暴⼒攻击
- 采⽤布隆过滤器,将所有可能存在的数据哈希到⼀个⾜够⼤的 bitmap 中,⼀个⼀定不存在的数据 会被这个 bitmap 拦截掉,从⽽避免了对底层存储系统的查询压⼒ 缓存击穿是指缓存中没有但数据库中有的数据(⼀般是缓存时间到期),这时由于并发⽤户特别多,同 时读缓存没读到数据,⼜同时去数据库去取数据,引起数据库压⼒瞬间增⼤,造成过⼤压⼒。和缓存雪 崩不同的是,缓存击穿指并发查同⼀条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从⽽ 查 数据库。
解决⽅案:
- 设置热点数据永远不过期。加互斥锁
# 布隆过滤器原理,优缺点
- 位图:int[10],每个int类型的整数是4*8=32个bit,则int[10]⼀共有320 bit,每个bit⾮0即1,初始 化时都是0
- 添加数据时:将数据进⾏hash得到hash值,对应到bit位,将该bit改为1,hash函数可以定义多个, 则 ⼀个数据添加会将多个(hash函数个数)bit改为1,多个hash函数的⽬的是减少hash碰撞的概率
- 查询数据:hash函数计算得到hash值,对应到bit中,如果有⼀个为0,则说明数据不在bit中,如果 都为1,则该数据可能在bit中
优点:
- 占⽤内存⼩
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,⼀般⽐较⼩),与数据量⼤⼩⽆关哈 希函数相互之间没有关系,⽅便硬件并⾏运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求⽐较严格的场合有很⼤优势 数据量很⼤时,布 隆过滤器可以表示全集
- 使⽤同⼀组散列函数的布隆过滤器可以进⾏交、并、差运算 缺点:
- 误判率,即存在假阳性(False Position),不能准确判断元素是否在集合中不能获取元素本身
- ⼀般情况下不能从布隆过滤器中删除元素
# 分布式系统中常⽤的缓存⽅案有哪些
- 客户端缓存:⻚⾯和浏览器缓存,APP缓存,H5缓存,localStorage 和 sessionStorage CDN缓 存:内容存储:数据的缓存,内容分发:负载均衡
- nginx缓存:静态资源
- 服务端缓存:本地缓存,外部缓存
- 数据库缓存:持久层缓存(mybatis,hibernate多级缓存),mysql查询缓存 操作系统缓存: PageCache、BufferCache
# 分布式缓存寻址算法
- hash算法:根据key进⾏hash函数运算、结果对分⽚数取模,确定分⽚ 适合固定分⽚数的场景, 扩展分⽚或者减少分⽚时,所有数据都需要重新计算分⽚、存储
- ⼀致性hash:将整个hash值得区间组织成⼀个闭合的圆环,计算每台服务器的hash值、映射到圆环 中。使⽤相同的hash算法计算数据的hash值,映射到圆环,顺时针寻找,找到的第⼀个服务器就是 数据存储的服务器。新增及减少节点时只会影响节点到他逆时针最近的⼀个服务器之间的值 存在 hash环倾斜的问题,即服务器分布不均匀,可以通过虚拟节点解决
- hash slot:将数据与服务器隔离开,数据与slot映射,slot与服务器映射,数据进⾏hash决定存放 的slot,新增及删除节点时,将slot进⾏迁移即可
# 如何进⾏消息队列选型?
- Kafka:
- 优点: 吞吐量⾮常⼤,性能⾮常好,集群⾼可⽤。
- 缺点:会丢数据,功能⽐较单⼀。
- 使⽤场景:⽇志分析、⼤数据采集
- RabbitMQ:
- 优点: 消息可靠性⾼,功能全⾯。
- 缺点:吞吐量⽐较低,消息积累会严重影响性能。erlang语⾔不好定制。
- 使⽤场景:⼩规模场景。
- RocketMQ:
- 优点:⾼吞吐、⾼性能、⾼可⽤,功能⾮常全⾯。
- 缺点:开源版功能不如云上商业版。官⽅⽂档和周边⽣态还不够成熟。客户端只⽀持java。
- 使⽤场景:⼏乎是全场景。
# 消息队列如何保证消息可靠传输
消息可靠传输代表了两层意思,既不能多也不能少。
- 为了保证消息不多,也就是消息不能重复,也就是⽣产者不能重复⽣产消息,或者消费者不能重复 消费消息
- ⾸先要确保消息不多发,这个不常出现,也⽐较难控制,因为如果出现了多发,很⼤的原因是⽣产 者⾃⼰的原因,如果要避免出现问题,就需要在消费端做控制
- 要避免不重复消费,最保险的机制就是消费者实现幂等性,保证就算重复消费,也不会有问题,通 过幂等性,也能解决⽣产者重复发送消息的问题
- 消息不能少,意思就是消息不能丢失,⽣产者发送的消息,消费者⼀定要能消费到,对于这个问 题,就要考虑两个⽅⾯
- ⽣产者发送消息时,要确认broker确实收到并持久化了这条消息,⽐如RabbitMQ的confirm机制, Kafka的ack机制都可以保证⽣产者能正确的将消息发送给broker
- broker要等待消费者真正确认消费到了消息时才删除掉消息,这⾥通常就是消费端ack机制,消费 者接收到⼀条消息后,如果确认没问题了,就可以给broker发送⼀个ack,broker接收到ack后才会 删除消息
# 消息队列有哪些作⽤
- 解耦:使⽤消息队列来作为两个系统之间的通讯⽅式,两个系统不需要相互依赖了
- 异步:系统A给消息队列发送完消息之后,就可以继续做其他事情了
- 流量削峰:如果使⽤消息队列的⽅式来调⽤某个系统,那么消息将在队列中排队,由消费者⾃⼰控 制消费速度
# 死信队列是什么?延时队列是什么?
- 死信队列也是⼀个消息队列,它是⽤来存放那些没有成功消费的消息的,通常可以⽤来作为消息重 试
- 延时队列就是⽤来存放需要在指定时间被处理的元素的队列,通常可以⽤来处理⼀些具有过期性操 作的业务,⽐如⼗分钟内未⽀付则取消订单
# 如何保证消息的⾼效读写?
零拷⻉: kafka和RocketMQ都是通过零拷⻉技术来优化⽂件读写。 传统⽂件复制⽅式: 需要对⽂件在内存中进⾏四次拷⻉。 零拷⻉: 有两种⽅式, mmap和transfile,Java当中对零拷⻉进⾏了封装, Mmap⽅式通过 MappedByteBuffer对象进⾏操作,⽽transfile通过FileChannel来进⾏操作。Mmap 适合⽐较⼩的⽂ 件,通常⽂件⼤⼩不要超过1.5G ~2G 之间。Transfile没有⽂件⼤⼩限制。RocketMQ当中使⽤Mmap⽅ 式来对他的⽂件进⾏读写。 在kafka当中,他的index⽇志⽂件也是通过mmap的⽅式来读写的。在其他⽇志⽂件当中,并没有使⽤ 零拷⻉的⽅式。Kafka使⽤transfile⽅式将硬盘数据加载到⽹卡。
# epoll和poll的区别
- select模型,使⽤的是数组来存储Socket连接⽂件描述符,容量是固定的,需要通过轮询来判断是 否发⽣了IO事件
- poll模型,使⽤的是链表来存储Socket连接⽂件描述符,容量是不固定的,同样需要通过轮询来判 断是否发⽣了IO事件
- epoll模型,epoll和poll是完全不同的,epoll是⼀种事件通知模型,当发⽣了IO事件时,应⽤程序才 进⾏IO操作,不需要像poll模型那样主动去轮询
# TCP的三次握⼿和四次挥⼿
TCP协议是7层⽹络协议中的传输层协议,负责数据的可靠传输。 在建⽴TCP连接时,需要通过三次握⼿来建⽴, 过程是:
- 客户端向服务端发送⼀个SYN
- 服务端接收到SYN后,给客户端发送⼀个SYN_ACK
- 客户端接收到SYN_ACK后,再给服务端发送⼀个ACK 在断开TCP连接时,需要通过四次挥⼿来断开, 过程是:
- 客户端向服务端发送FIN
- 服务端接收FIN后,向客户端发送ACK,表示我接收到了断开连接的请求,客户端你可以不发数据 了,不过服务端这边可能还有数据正在处理
- 服务端处理完所有数据后,向客户端发送FIN,表示服务端现在可以断开连接
- 客户端收到服务端的FIN,向服务端发送ACK,表示客户端也会断开连接了
# 浏览器发出⼀个请求到收到响应经历了哪些步骤?
- 浏览器解析⽤户输⼊的URL,⽣成⼀个HTTP格式的请求
- 先根据URL域名从本地hosts⽂件查找是否有映射IP,如果没有就将域名发送给电脑所配置的DNS进 ⾏域名解析,得到IP地址
- 浏览器通过操作系统将请求通过四层⽹络协议发送出去
- 途中可能会经过各种路由器、交换机,最终到达服务器
- 服务器收到请求后,根据请求所指定的端⼝,将请求传递给绑定了该端⼝的应⽤程序,⽐如8080被 tomcat占⽤了
- tomcat接收到请求数据后,按照http协议的格式进⾏解析,解析得到所要访问的servlet
- 然后servlet来处理这个请求,如果是SpringMVC中的DispatcherServlet,那么则会找到对应的 Controller中的⽅法,并执⾏该⽅法得到结果
- Tomcat得到响应结果后封装成HTTP响应的格式,并再次通过⽹络发送给浏览器所在的服务器
- 浏览器所在的服务器拿到结果后再传递给浏览器,浏览器则负责解析并渲染
# 跨域请求是什么?有什么问题?怎么解决?
跨域是指浏览器在发起⽹络请求时,会检查该请求所对应的协议、域名、端⼝和当前⽹⻚是否⼀致,如 果不⼀致则浏览器会进⾏限制,⽐如在www.baidu.com的某个⽹⻚中,如果使⽤ajax去访问 www.jd.com是不⾏的,但是如果是img、iframe、script等标签的src属性去访问则是可以的,之所以浏 览器要做这层限制,是为了⽤户信息安全。但是如果开发者想要绕过这层限制也是可以的:
- response添加header,⽐如resp.setHeader("Access-Control-Allow-Origin", "*");表示可以访问 所有⽹站,不受是否同源的限制
- jsonp的⽅式,该技术底层就是基于script标签来实现的,因为script标签是可以跨域的
- 后台⾃⼰控制,先访问同域名下的接⼝,然后在接⼝中再去使⽤HTTPClient等⼯具去调⽤⽬标接⼝
- ⽹关,和第三种⽅式类似,都是交给后台服务来进⾏跨域访问
# 零拷⻉是什么
零拷⻉指的是,应⽤程序在需要把内核中的⼀块区域数据转移到另外⼀块内核区域去时,不需要经过先 复制到⽤户空间,再转移到⽬标内核区域去了,⽽直接实现转移。