文章目录
- 1. 微服务之间如何独立通讯的
- 2. springcloud和dubbo有哪些区别
- 3. 什么是服务熔断,什么是服务降级
- 4. 微服务的优缺点
- 5. 使用中碰到的坑
- 6. dubbo服务注册与发现原理理
- 7. springcloud核⼼组件及其作用,以及springcloud⼯作原理
- 8. 熔断的原理,以及如何恢复?
- 9. 服务雪崩?
- 10. 服务隔离的原理?如何处理服务雪崩的场景?
- 11. 多个消费者调⽤同一接⼝,euruka默认的分配⽅式是什什么?
- 12. 为什么要把系统拆分成分布式的?如何拆分?
- 13. dubbo的工作原理是啥?注册中心挂了可以继续通信吗?
- 14. dubbo都支持哪些通信协议以及序列化协议?
- 15. dubbo支持哪些负载均衡、高可用(集群容错)以及动态代理的策略?
- 16. SPI是啥思想?dubbo的SPI机制是怎么玩儿的?
- 17. 基于dubbo如何做服务治理、服务降级以及重试?(略)
- 18. 分布式系统中接口的幂等性如何保证?比如不能重复扣款?
- 19. 分布式系统中的接口调用如何保证顺序性?
- 20. 如何设计一个类似dubbo的rpc框架?架构上该如何考虑?
- 21. 说说zookeeper一般都有哪些使用场景?(简)
- 22. 分布式锁是啥?对比下redis和zk两种分布式锁的优劣?
- 23. 说说你们的分布式session方案是啥?怎么做的?
- 24. 了解分布式事务方案吗?你们都咋做的?有啥坑?
- 25. 说说一般如何设计一个高并发的系统架构?
- 26. 如何进行分库分表?
- 27. 你们当时是如何把系统不停机迁移到分库分表的?
- 28. 如何设计可以动态扩容缩容的分库分表方案?
- 29. 一个关键的问题!分库分表之后全局id咋生成?
- 30. 说说MySQL读写分离的原理?主从同步延时咋解决?
1. 微服务之间如何独立通讯的
a. Dubbo 使用的是 RPC 通信,⼆进制传输,占⽤带宽小;
b. Spring Cloud 使用的是 HTTP RESTFul 方式。
2. springcloud和dubbo有哪些区别
3. 什么是服务熔断,什么是服务降级
- 服务熔断:
如果检查出来频繁超时,就把consumer调用provider的请求,直接短路掉,不实际调用,⽽是直接返回一个mock 的值。 - 服务降级:
- consumer 端:consumer 如果发现某个provider出现异常情况,⽐如,经常超时(可能是熔断引起的降级),数据错误,这时,consumer可以采取⼀定的策略,降级provider的逻辑,基本的有直接返回固定的数据。
- provider 端:当provider 发现流量激增的时候,为了保护⾃身的稳定性,也可能考虑降级服务。 ⽐如,1,直接给consumer返回固定数据,2,需要实时写⼊数据库的,先缓存到队列里,异步写⼊数据库。
4. 微服务的优缺点
a. 优点:
- i. 单一职责:每个微服务仅负责⾃己业务领域的功能;
- ii. ⾃治:⼀个微服务就是⼀个独立的实体,它可以独⽴部署、升级,服务与服务之间通过REST等形式的标准接⼝进行通信,并且⼀个微服务实例可以被替换成另⼀种实现,⽽对其它的微服务不产生影响。
- iii. 逻辑清晰:微服务单一职责特性使微服务看起来逻辑清晰,易于维护。
- iv. 简化部署:单系统中修改⼀处需要部署整个系统,而微服务中修改一处可单独部署⼀个服务。
- v. 可扩展:应对系统业务增长的方法通常采用横向(Scale out)或纵向(Scale up)的方向进⾏扩展。分布式系统 中通常要采用Scale out的方式进⾏扩展。
- vi. 灵活组合:
- vii. 技术异构:不同的服务之间,可以根据⾃己的业务特点选择不同的技术架构,如数据库等。
b. 缺点:
- i. 复杂度高:
1、 服务调⽤要考虑被调用⽅故障、过载、消息丢失等各种异常情况,代码逻辑更加复杂;
2、 对于微服务间的事务性操作,因为不同的微服务采⽤了不同的数据库,将无法利用数据库本身的事务机制保证⼀致性,需要引入二阶段提交等技术。 - ii. 运维复杂:系统由多个独⽴运行的微服务构成,需要一个设计良好的监控系统对各个微服务的运⾏状态进⾏监控。运维人员需要对系统有细致的了解才能够更好的运维系统。
- iii. 通信延迟:微服务之间调⽤会有时间损耗,造成通信延迟。
5. 使用中碰到的坑
a. 超时:确保Hystrix超时时间配置为长于配置的Ribbon超时时间
b. feign path:feign客户端在部署时若有contextpath应该设置 path="/***"来匹配你的服务名。
c. 版本:springboot和springcloud版本要兼容。
6. dubbo服务注册与发现原理理
7. springcloud核⼼组件及其作用,以及springcloud⼯作原理
springcloud由以下几个核心组件构成:
- Eureka:各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且Eureka Client还可以反过来从Eureka Server拉取注册表,从⽽知道其他服务在哪⾥
- Ribbon:服务间发起请求的时候,基于Ribbon做负载均衡,从⼀个服务的多台机器中选择⼀台
- Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求
- Hystrix:发起请求是通过Hystrix的线程池来走的,不同的服务⾛不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题
- Zuul:如果前端、移动端要调⽤用后端系统,统⼀从Zuul网关进入,由Zuul网关转发请求给对应的服务
8. 熔断的原理,以及如何恢复?
服务的健康状况 = 请求失败数 / 请求总数.
熔断器开关由关闭到打开的状态转换是通过当前服务健康状况和设定阈值比较决定的.
- i. 当熔断器开关关闭时, 请求被允许通过熔断器. 如果当前健康状况⾼于设定阈值, 开关继续保持关闭. 如果当前健康状况低于设定阈值, 开关则切换为打开状态.
- ii. 当熔断器开关打开时, 请求被禁⽌通过.
- iii. 当熔断器开关处于打开状态, 经过⼀段时间后, 熔断器会⾃动进⼊半开状态, 这时熔断器只允许一个请求通过. 当该请求调⽤成功时, 熔断器恢复到关闭状态. 若该请求失败, 熔断器继续保持打开状态, 接下来的请求被禁⽌通过. 熔断器的开关能保证服务调⽤者在调⽤异常服务时, 快速返回结果, 避免⼤量的同步等待. 并且熔断器能在⼀一段时间后继续侦测请求执行结果, 提供恢复服务调用的可能.
9. 服务雪崩?
简介:服务雪崩效应是⼀种因服务提供者的不可用导致服务调⽤者的不可⽤,并将不可用逐渐放⼤的过程.
形成原因:
- i. 服务提供者不可用
- ii. 重试加⼤流量
- iii. 服务调⽤者不可用
采⽤策略:
- i. 流量控制
- ii. 改进缓存模式
- iii. 服务自动扩容
- iv. 服务调⽤者降级服务
10. 服务隔离的原理?如何处理服务雪崩的场景?
Hystrix通过将每个依赖服务分配独⽴的线程池进⾏资源隔离, 从⽽避免服务雪崩.
11. 多个消费者调⽤同一接⼝,euruka默认的分配⽅式是什什么?
- a. RoundRobinRule:
轮询策略
,Ribbon以轮询的⽅式选择服务器,这个是默认值
。所以示例例中所启动的两个服务会被循环访问; - b. RandomRule:随机选择,也就是说Ribbon会随机从服务器列表中选择⼀个进行访问;
- c. BestAvailableRule:最⼤可⽤策略,即先过滤出故障服务器后,选择⼀个当前并发请求数最⼩的;
- d. WeightedResponseTimeRule:带有加权的轮询策略,对各个服务器响应时间进⾏加权处理,然后在采⽤轮询的⽅式来获取相应的服务器;
- e. AvailabilityFilteringRule:可用过滤策略,先过滤出故障的或并发请求⼤于阈值⼀部分服务实例,然后再以线性轮询的⽅式从过滤后的实例清单中选出一个;
- f. ZoneAvoidanceRule:区域感知策略,先使⽤主过滤条件(区域负载器,选择最优区域)对所有实例过滤并返回过滤后的实例清单,依次使⽤次过滤条件列表中的过滤条件对主过滤条件的结果进行过滤,判断最⼩过滤数(默认1)和最⼩过滤百分比(默认0),最后对满⾜条件的服务器器则使用RoundRobinRule(轮询⽅方式)选择⼀个服务器实例。
12. 为什么要把系统拆分成分布式的?如何拆分?
13. dubbo的工作原理是啥?注册中心挂了可以继续通信吗?
dubbo是一共分为10层的:
- 第一层:service层,接口层,给服务提供者和消费者来实现的
- 第二层:config层,配置层,主要是对dubbo进行各种配置的
- 第三层:proxy层,服务代理层,透明生成客户端的stub和服务端的skeleton
- 第四层:registry层,服务注册层,负责服务的注册与发现
- 第五层:cluster层,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务
- 第六层:monitor层,监控层,对rpc接口的调用次数和调用时间进行监控
- 第七层:protocol层,远程调用层,封装rpc调用
- 第八层:exchange层,信息交换层,封装请求响应模式,同步转异步
- 第九层:transport层,网络传输层,抽象mina和netty为统一接口
- 第十层:serialize层,数据序列化层
工作流程:
- 第一步,provider向注册中心去注册
- 第二步,consumer从注册中心订阅服务,注册中心会通知consumer注册好的服务
- 第三步,consumer调用provider
- 第四步,consumer和provider都异步的通知监控中心
注册中心挂了可以继续通信吗?
- 可以,因为刚开始初始化的时候,消费者会将提供者的地址等信息拉取到本地缓存,所以注册中心挂了可以继续通信
14. dubbo都支持哪些通信协议以及序列化协议?
(1) dubbo支持不同的通信协议
- 1)dubbo协议
dubbo://192.168.0.1:20188
默认就是走dubbo协议的,单一长连接,NIO异步通信,基于 hessian 作为序列化协议
适用的场景就是:传输数据量很小(每次请求在100kb以内),但是并法量很高
为了要支持高并发场景,一般是服务提供者就几台机器,但是服务消费者有上百台,可能每天调用量达到上亿次!此时用长连接是最合适的,就是跟每个服务消费者维持一个长连接就可以,可能总共就 100 个连接。然后后面直接基于长连接NIO异步通信,可以支撑高并发请求。
否则如果上亿次请求每次都是短连接的话,服务提供者会扛不住
而且因为走得是单一长连接,所以传输数据量太大的话,会导致并发能力降低。所以一般建议是传输数据量很小,支撑高并发访问。 - 2)rmi 协议
走 java 二进制序列化,多个短连接,适合消费者和提供者数量差不多,适用于文件的传输,一般较少用 - 3)hessian协议
走hessian序列化协议,多个短连接,适用于提供者数量比消费者数量还多,适用于文件的传输,一般较少用 - 4)http协议
走json序列化 - 5)webservice
走SOAP文本序列化
(2)dubbo支持的序列化协议
所以dubbo实际基于不同的通信协议,支持hession、java二进制序列化、json、SOAP文本序列化多种序列化协议。但是 hessian 是其默认的序列化协议。
15. dubbo支持哪些负载均衡、高可用(集群容错)以及动态代理的策略?
这种问题说白了就是考察我们对dubbo的熟悉程度:
(1)dubbo工作原理:服务注册,注册中心,消费者,代理通信,负载均衡
(2)网络通信、序列化:dubbo协议,长连接,NIO,hessian序列化协议
(3)负载均衡策略,集群容错策略,动态代理策略:dubbo跑起来的时候一些功能是如何运转的,怎么做负载均衡?怎么做集群容错?怎么生成动态代理?
(4)dubbo SPI机制:你了解不了解dubbo的SPI机制?如何基于SPI机制对dobbo进行扩展?
(1)dubbo负载均衡策略
- 1)random loadbalance
默认情况下,dubbo是random load balance 随机调用实现负载均衡,可以对provider不用实例设置不同的权重,会按照权重来负载均衡,权重越大分配流量越高,一般就用这个默认的就可以了。 - 2)roundrobin loadbalance
还有roundrobin loadbalance,这个的话默认就是均匀地将流量打到各个机器上去,但是如果各个机器的性能不一样,容易导致性能差的机器负载过高。所以此时需要调整权重,让性能差的机器承载权重小一些,流量少一些。 - 3)leastactive loadbalance
这个就是自动感知一下,如果某个机器性能越差,那么接收的请求越少,越不活跃,此时就会给不活跃的性能差的机器更少的请求 - 4)consistanthash loadbalance
一致性Hash算法,相同参数的请求一定分发到一个provider上去,provider挂掉的时候,会基于虚拟节点均匀分配剩余的流量,抖动不会太大。如果你需要的不是随机负载均衡,是
(2)dubbo 集群容错策略
- 1)failover cluster 模式
失败自动切换,自动重试其他机器,默认就是这个,常见于读操作 - 2)failfast cluster 模式
一次调用失败就立即失败,常见于写操作 - 3)failsafe cluster 模式
出现异常时忽略掉,常用于不重要的接口调用,比如记录日志 - 4)failback cluster 模式
失败了后台自动记录请求,然后定时重发,比较适合于写消息队列这种 - 5)forking cluster 模式
并行调用多个provider,只要一个成功就立即返回 - 6)broadcast cluster 策略
逐个调用所有的provider
(3)dubbo动态代理策略
- 默认使用 javassist 动态字节码生成,创建代理类
- 但是可以通过 spi 扩展机制配置自己的动态代理策略
16. SPI是啥思想?dubbo的SPI机制是怎么玩儿的?
spi 是啥?
spi,简单来说,就是 service provider interface,说白了是什么意思呢,比如你有个接口,现在这个接口有 3 个实现类,那么在系统运行的时候对这个接口到底选择哪个实现类呢?这就需要 spi 了,需要根据指定的配置或者是默认的配置,去找到对应的实现类加载进来,然后用这个实现类的实例对象。
举个栗子。
你有一个接口 A。A1/A2/A3 分别是接口A的不同实现。你通过配置 接口 A = 实现 A2,那么在系统实际运行的时候,会加载你的配置,用实现 A2 实例化一个对象来提供服务。
spi 机制一般用在哪儿?插件扩展的场景,比如说你开发了一个给别人使用的开源框架,如果你想让别人自己写个插件,插到你的开源框架里面,从而扩展某个功能,这个时候 spi 思想就用上了。
Java spi 思想的体现
spi 经典的思想体现,大家平时都在用,比如说 jdbc。
Java 定义了一套 jdbc 的接口,但是 Java 并没有提供 jdbc 的实现类。
但是实际上项目跑的时候,要使用 jdbc 接口的哪些实现类呢?一般来说,我们要根据自己使用的数据库,比如 mysql,你就将 mysql-jdbc-connector.jar 引入进来;oracle,你就将 oracle-jdbc-connector.jar 引入进来。
在系统跑的时候,碰到你使用 jdbc 的接口,他会在底层使用你引入的那个 jar 中提供的实现类。
dubbo 的 spi 思想
dubbo 也用了 spi 思想,不过没有用 jdk 的 spi 机制,是自己实现的一套 spi 机制。
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
Protocol 接口,在系统运行的时候,dubbo 会判断一下应该选用这个 Protocol 接口的哪个实现类来实例化对象来使用。
它会去找一个你配置的 Protocol,将你配置的 Protocol 实现类,加载到 jvm 中来,然后实例化对象,就用你的那个 Protocol 实现类就可以了。
上面那行代码就是 dubbo 里大量使用的,就是对很多组件,都是保留一个接口和多个实现,然后在系统运行的时候动态根据配置去找到对应的实现类。如果你没配置,那就走默认的实现好了,没问题。
- dubbo的SPI原理
17. 基于dubbo如何做服务治理、服务降级以及重试?(略)
(1)服务治理
- 1)调用链路自动生成
- 2)服务访问压力以及时长统计
- 3)其他的(服务分层、调用链路失败监控和报警、服务鉴权、可用性监控等)
(2)服务降级
比如说服务A调用服务B,结果服务B挂掉了,服务A重试几次调用服务B,还是不行,直接降级,走一个备用的逻辑,给用户返回响应。
(3)失败重试和超时重试
所谓失败重试,就是consumer要是调用provider要是失败了,比如抛异常了,此时应该是可以重试的,或者调用超时了也可以重试。重试时间和重试次数我们都可以设置。
18. 分布式系统中接口的幂等性如何保证?比如不能重复扣款?
- 分布式系统接口的幂等性问题
所谓幂等性,就是说一个接口,多次发起同一个请求,你这个接口得保证结果是准确的,比如不能多扣款,不能多插入一条数据,不能将统计值多加了1。这就是幂等性。
保证幂等性主要是三点:
(1)对于每个请求必须有一个唯一的标识,举个例子:订单支付请求,肯定得包含订单id,一个订单id最多支付一次。
(2)每次处理完请求之后,必须有一个记录标识这个请求处理过了,比如说常见的方案是在mysql中记录个状态,比如要要支付一个订单,必须插入一条支付流水记录,order_id建一个唯一键,unique key,所以你在支付一个订单之前,先插入一条支付流水,order_id就已经进去了。
(3)然后如果有重复支付的时候,会再次插入订单流水,可是此时数据库会报错,说unique key冲突了,整个事务回滚就可以了。
这里也可以使用redis来作为记录这种支付成功的方式,例如使用redis的set nx来确定是否已经支付。
19. 分布式系统中的接口调用如何保证顺序性?
- 分布式系统接口调用顺序性
两种方案:
- 方案1:基于内存队列:在用户和系统A之间加入一层接入服务,对请求1、2、3分配相同的orderId,接入服务会对orderId使用一致性哈希算法进行负载均衡分配,这样请求1、2、3就会被分配到一台机器上了,然后将请求依次写入到一个内存队列中,由一个线程去顺序执行。它的好处是比分布式锁的性能要好一点(分布式锁可能会频繁的获取释放锁),缺点是没有办法保证百分百的顺序性。
- 方案2:基于分布式锁:请求被负载均衡到不同机器上,不过会有一个执行顺序编号seq=1、2、3,执行前要先获取分布式锁,只有获取锁了才能执行,可是如果seq3获取到锁,会发现seq1、2还没执行,就会释放锁,这样会保证百分百的顺序性,可是阻塞的代价会比较大,所以除非特别需要强顺序性的场景不推荐。
20. 如何设计一个类似dubbo的rpc框架?架构上该如何考虑?
一个简单的回答思路:
(1)首先要有一个注册中心,可以用zookeeper来做。
(2)提供者向注册中心注册服务,消费者从注册中心拿服务,同时本地缓存提供者信息。
(3)发起请求,是基于动态代理的,面向接口获得一个动态代理,这个动态代理就是接口在本地的一个代理,然后这个代理会找到服务对应的机器地址。
(4)然后要将请求发给哪台机器呢?要使用合适的负载均衡算法,如轮询、随机、权重等。
(5)找到对应的机器就可以发送请求了,可以使用netty,nio方式,发送的数据可以通过hessian协议序列化。
(6)服务端(服务提供者)也要针对自己的服务生成一个动态代理,监听某个网络端口,然后代理本地的服务代码。接收到请求的时候,就调用对应的服务代码。
21. 说说zookeeper一般都有哪些使用场景?(简)
答案链接:https://www.jianshu.com/p/418d95f0092c
我们都知道ZK现在在整个技术圈都可以见到其身影,特别是分布式环境,基本上是必不可少的一个组件。 下面就罗列下ZK常用的八大应用场景,帮助你更清楚的了解Zookeeper是用在哪些方面上。
1、数据发布和/订阅(注册中心)
主要的一个场景,比如配置中心。我们会将配置的相关信息都存放在一个中心,这样我们的应用就不用每次修改参数就要进行重启,使用了zk作用配置中心的数据推送更新,这样我们就能方便的进行数据更新,每次将相关数据发布到配置中心,然后由应用服务去订阅,这样就能动态的进行配置数据的更新。
2、负载均衡
可以基于ZK来实现DDNS动态域名解析服务,从而达到域名的动态添加、修改、删除等。能够基于域名服务,进行应用的负载,从而达到请求负载到各个应用中。
3、命名服务
命名服务,主要的应用场景在于rpc服务,比如dubbo等框架,可以将相应的服务注册在zk上,这样服务调用就可以根据其所命名的服务来提供对外服务等。
4、分布式协调/通知
对于一个在多台机器部署运行的应用上,通常都需要一个协调者来控制整个系统的运行流程。比如分布式事务、机器间的互相协调等。这样能将分布式协调的职责能从应用中分离出来,达到减少系统间的耦合性,提高系统的可扩展性。
5、集群管理
在集群环境中,机器和应用都是分散着进行部署,每次进行服务的上下线升级的过程中,都要手动进行集群的管理,这样造成人做的事比较重复性,并且也比较麻烦容易出错。如果能使用zk来协助我们进行服务或机器进群的管理,这样将能帮助我们解决需要繁琐又麻烦的事。
6、Master选举
Master选举,也就是在众多机器或服务中,选举出一个最终“决定权”的领导者,来独立完成一项任务。比如有一项服务是需要对外提供服务,但是要保证高可用,我们就机会进行服务的多项部署,也就是做了一些备份,提高系统的可用性。一旦我们的主服务挂了,我们可以让其它的备份服务进行重新选举,这样我们就能使整个系统不会因服务的挂掉而造成服务不可用。
7、分布式锁
分布式锁是控制分布式系统间同步访问共享资源的一种方式。如果不同的系统或同一个系统的不同主机之间共享了同一个资源,那么访问这些资源的时候,需要使用互斥的手段来防止彼此之间的干扰,以保证一致性,这种情况就需要使用分布式锁。
8、分布式队列
使用zk来实现分布式队列,分为两大类:FIFO先进先出队列、Barrier分布式屏障。FIFO队列是一种很典型的队列模型:先进入队列的请求先完成操作后,才会处理后面的请求;Barrier分布式屏障,则是需要将队列元素都集聚之后才进行统一的执行安排,否则只能等待。
总结
上面是ZK的常用8大应用场景,可能还有其它适合你的业务场景。可以看出ZK在分布式系统中能起到减少系统间的耦合性,也能提高系统的可扩展性。在现在的互联网时代,相信你一定会使用到Zookeeper的!
22. 分布式锁是啥?对比下redis和zk两种分布式锁的优劣?
(1)redis分布式锁
- 普通方式(不常用)
第一个最普通的实现方式,通过set key value NX PX 3000,这种方式加锁,NX的意思就是只有key不存在的时候才会设置成功,PX 3000 的意思就是3秒后自动释放。别人创建的时候如果发现已经有了就不能加锁了。
释放锁就是删除key,但是一般可以用lua脚本删除,判断value一样才删除(因为重复加锁的时候key还是一样的,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还没同步到从节点,此时从节点切换为主节点,别人就会拿到锁。
- RedLoak算法(官方推荐的方式)
这个场景是假设有一个 redis cluster,有5个 redis master 实例。然后执行如下步骤获取一把锁:
- 1)获取当前时间,单位是毫秒
- 2)跟上面类似,轮流尝试在每个master节点上创建锁,过期时间较短,一般就几十毫秒
- 3)尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n / 2 + 1)
- 4)客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
- 5)要是锁建立失败了,那么就依次删除这个锁
- 6)只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁
其实这种方式也说不好好不好,一般分布式锁也不建议这么用
(2)zk分布式锁
- zookeeper分布式锁场景
zk分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时znode,此时创建成功了就可以获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。 - zookeeper分布式锁实现的原理
23. 说说你们的分布式session方案是啥?怎么做的?
单机的话是不存在分布式session问题的,可以分布式系统会有问题,例如我们系统部署在两台机器上,用户登录时请求机器A,登录状态等信息都在机器A上,后面的请求分发到了机器B上,B的session里是没有用户的登录信息的,这肯定是不对的。
如上图,使用redis存储session信息就好了。
24. 了解分布式事务方案吗?你们都咋做的?有啥坑?
- 01_单块系统里的事务
- 02_分布式系统里的事务
- 03_两阶段提交方案 / XA方案
XA方案,也叫两阶段提交事务方案,两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复ok,那么就正式提交事务,在各个数据库上执行操作;如果任何一个数据库回答不ok,那么就回滚事务。
这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。如果要玩儿,那么基于 spring + JTA 就可以搞定。
这个方案,我们很少用,一般来说某个系统内部如果出现跨多个库的这么一个操作,是不合规的。加入一个微服务系统包含几十几百个服务,而我们一般会规范每个服务只能操作自己对应的一个数据库。如果允许交叉访问,是很混乱没办法治理的,如果你要操作别人的服务的库,你必须是调用别的服务的接口来实现,绝对不允许你交叉访问别人的数据库!所以适合XA的场景一般已经不会出现。 - 04_TCC方案(补偿)
try-confirm-cancel
TCC的全程是:Try、Confirm、Cancel。
这个其实是用到了补偿的概念,分为了三个阶段:
1)Try阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留
2)Confirm阶段:这个阶段说的是在各个服务中执行实际的操作
3)Cancel阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作
举个银行转账的例子,当跨银行转账的时候,要涉及两个银行的分布式事务,如果用TCC方案来实现,思路是这样的:
- Try阶段:先把两个银行账户中的资金给它冻结住不让操作了
- Confirm阶段:执行实际的转账操作,A银行账户的资金扣减,B银行账户的资金增加
- Cancel阶段:如果任何一个银行的操作执行失败,那么就需要回滚进行补偿,就是比如A银行如果已经扣减了,但是B银行账户资金增加失败了,那么就得把A银行账户资金给加回去
TCC的使用场景其实并不多,因为这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿的,会造成补偿代码巨大,维护困难。
比较适合的场景:这个就是除非要求强一致性,是系统中核心的场景,比如常见的就是资金类的场景,可以使用TCC方案,自己编写大量的业务逻辑,自己判断一个事务中的各个环节是否ok,不ok就执行补偿 / 回滚代码。
- 05_本地消息表方案
- 1)A系统在自己本地一个事务里操作同时,插入一条数据到消息表
- 2)接着A系统将这个消息发送到MQ中去
- 3)B系统接收到这个消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息
- 4)B系统执行成功之后,就会更新自己本地消息表的状态以及A系统消息表的状态
- 5)如果B系统处理失败了,那么就不会更新消息表状态,那么此时A系统会定时扫描自己的消息表,如果有没处理的消息,会再次发送到MQ中去,让B再次处理
- 6)这个方案保证了最终一致性,哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止
这里zookeeper是可以没有的,比如B成功了调用一个接口来通知下A就好了。
这个方案其实也是存在严重问题的,一般也不会使用,它严重依赖于数据库的消息表来管理实务,这样遇到高并发的场景对数据库的操作会吃不消。
- 06_可靠消息最终一致性方案(主流)
其实可以理解成是本地消息表改造的,不用数据库的消息表了,直接基于MQ来实现事务。比如阿里的RocketMQ就支持消息事务。
大概的思路如下:
- 1)A系统先发送一个 prepared 消息到 mq,如果这个 prepared 消息发送失败那么就直接取消操作
- 2)如果这个消息发送成功了,那么接着执行本地事务,如果成功就告诉mq发送确认消息,如果失败就告诉mq回滚消息
- 3)如果发送了确认消息,那么此时B系统会接收到确认消息,然后执行本地的事务
- 4)mq会自动定时轮询所有prepared消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所以没发送确认消息?那是继续重试还是回滚?一般来说这里就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,别确认消息发送失败了。
- 5)这个方案里,要是系统B的事务失败了,它会自动不断重试直到成功(可以通过zookeeper通知系统A自己失败了重发消息,或者直接从MQ再次取消息),如果实在是不行,那么就是针对重要的资金类业务进行回滚,比如系统B本地回滚后,想办法通知系统A也会滚;或者是发送报警由人工来手工回滚和补偿。(猜测是我们的ylog的作用)
- 07_最大努力通知方案(用的也比较少)
- 1)系统A本地事务执行完之后,发送个消息到MQ
- 2)这里会有个专门消费MQ的最大努力通知服务,这个服务会消费MQ然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统B的接口
- 3)要是系统B执行成功就ok了;要是系统B执行失败了,那么最大努力通知服务就定时尝试重新调用系统B,反复N次,最后还是不行就放弃
- 你们公司是如何处理分布式事务的?
- 强一致性场景:TCC
- 普通场景:可靠消息最终一致性方案
注意:
其实用任何一个分布式事务的解决方案,都会导致你那块的代码复杂10倍。很多情况下,系统A调用系统B、系统C、系统D,我们可能根本就不做分布式事务。一般的业务场景(除了设计金额等需要强一致性高可靠的场景),99%的分布式接口调用,不要做分布式事务,直接就是监控(发邮件、发短信)、记录日志(一旦出错,完整的日志)、事后快速的定位、排查和出解决方案、修复数据。
25. 说说一般如何设计一个高并发的系统架构?
可以从下面几点出发:
(1)系统拆分,将一个系统拆分为多个子系统,用dubbo/spring cloud。然后每个系统连一个数据库,这样本来一个库,现在多个数据库,也是可以抗高并发的。
(2)缓存,必须得用缓存。大部分的高并发场景,就是读多写少,那你完全可以在数据库和缓存里都写一份,然后读的时候大量走缓存不就得了。笔记redis轻轻松松单机几万的并发是没问题的,所以项目中的读请求场景可以使用缓存来解决高并发。
(3)MQ,必须得用MQ。可能你还是会出现高并发写的场景,比如说一个业务操作里要频繁搞数据库几十次,增删改增删改,疯了。那高并发绝对搞挂你的系统,要是用redis来承载写肯定不行,人家是缓存,数据随时就被LRU了,数据格式还无比简单,没有事务支持。所以该用mysql还得用mysql。可是mysql承受不住太高的并发,这里就可以用MQ了,将大量的写请求灌入MQ里,将系统的消费速度控制在mysql承载范围之内。
(4)分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,那么我们可以将一个数据库拆分为多个库,多个库来抗更高的并发;然后将一个表拆分为多个表,每个表的数据量保持少一点,提高sql跑的性能。
(5)读写分离,这个就是说大部分数据库可能也是读多写少,没必要所有请求都集中在一个库上,可以搞个主从架构,主库写入,从库读取,搞一个读写分离。读流量太多的时候,还可以加更多的从库。
(6)Elasticsearch,可以考虑用es。es是分布式的,可以随便扩容,分布式天然就可以支撑高并发,因为动不动就可以扩容加机器来抗更高的并发。那么一些比较简单的查询、统计类的操作,可以考虑用es来承载,还有一些全文搜索类的操作,也可以考虑用es来承载。
26. 如何进行分库分表?
- 分库分表的由来
- 数据库中间件
现在常用的数据库分库中间件有两个 sharding-jdbc(引用jar包依赖的)和 mycat(独立部署的)。
sharding-jdbc这种client层方案的优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要耦合sharding-jdbc的依赖。
mycat这种proxy层方案的缺点在于需要部署,自己运维一套中间件,运维成本高,但是好处在于对于各个项目是透明的,如果遇到升级之类的都是自己中间件那里搞就可以了,耦合度低。
所以一般建议小公司用sharding-jdbc,大公司可以使用mycat。 - 数据库如何拆分
你们是如何对数据库进行垂直拆分或水平拆分的?
水平拆分的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来抗更高的并发,还有就是用多个库的存储容量来进行扩容。
垂直拆分的意思,就是把一个有很多字段的表拆分成多个表,或者是多个库上去。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会被较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。
还有表层面的拆分,就是分表,将一个表变成N个表,就是让每个表的数据量控制在一定范围内,保证SQL的性能。否则单表数据量越大,SQL性能就越差。一般是200万行左右,不要太多,但是也得看具体你怎么操作也可能是500万,或者100万。你的SQL越复杂,就最好让单表行数越少。
无论是分库还是分表,上面说的数据库中间件都是可以支持的。就是基本上那些中间件可以做到你分库分表之后,中间件可以根据你指定的某个字段值,比如说userId,自动路由到对应的库上去,然后再自动路由到对应的表里去。
那么我们的项目要如何分库分表呢?一般来说,垂直拆分,你可以在表层面来做,对一些字段特别多的表做一下拆分;水平拆分,你可以说是并发承载不了,或者数据量太大,容量承载不了;如果分库后每个表还是太大了,就可以再分表保证每个表的数据量不会很大(如上面的图解)。
分库分表一般有两种方式,一种是按照range来分,就是每个库一段连续的数据,这个一般是按照比如时间范围来分,但是一般不用这种,因为很容易产生热点问题,大量的流量都打在最新的数据上了;一般采用按照某个字段hash一下均匀分散。
range来分,好处在于说,后面扩容的时候,就很容易,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点是很多场景中大部分的请求都是访问最新的数据。所以如果场景是均匀的访问现在以及历史数据的话,可以使用range来分。
hash分法,好处在于可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的这么一个过程。
27. 你们当时是如何把系统不停机迁移到分库分表的?
假设现在有一个单库单表的系统,在线上在跑,假设单表有600万数据。
现在想将它分为3个库,每个库里分4个表,每个表要放50万的数据量。
- 01_长时间停机分库分表
- 02_不停机双写方案
双写使我们常用的一种迁移方案:
简单的说就是在线上系统里面,之前写库的地方,增删改操作,除了对老库增删,都加上对辛苦的增删改,这就是所谓双写,同时写俩库,老库和新库。
然后系统部署之后,新库数据差太远,用之前说的导数工具,跑起来读老库数据写新库,写的时候要根据 gmt modified 这类字段判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写。
接着导完一轮之后,有可能数据还是存在不一致,那么就程序自动做一轮校验,比对新老库每个表的每条数据,接着如果有不一样的,就针对那写不一样的,从老库读数据再次写,反复循环,直到两个库每个表的数据都完全一致为止。
接着当数据完全一致了,就ok了,基于仅仅使用分库分表的最新代码,重新部署一次,不就仅仅基于分库分表在操作了吗,还没有几个小时的停机时间,所以现在大多是采用这种方式的。
28. 如何设计可以动态扩容缩容的分库分表方案?
29. 一个关键的问题!分库分表之后全局id咋生成?
- 01_分库分表的id主键问题
- 02_snowflake算法
- (1)数据库自增
这个就是说你的系统里每次得到一个id,都是往一个库的一个表里插入一条没什么业务含义的数据,然后获取一个数据库自增的一个id。拿到这个id之后再往对应的分库分表里取写入。
这个方案的好处就是简单方便,谁都会用;缺点就是单据生成自增id,要是高并发的话,就会有瓶颈的;如果你硬是要改进一下,那么就专门开一个服务出来,这个服务每次都拿到当前id最大值,然后自己递增几个id,一次性返回一批id,然后再把当前最大id值修改成自增几个id之后的一个值;但是无论怎么说都是基于单个数据库。
适合的场景:你分库分表就俩原因,要不就是单库并发太高,要不就是单库数据量太大;除非是并发不高,但是数据量太大导致的分库分表扩容可以使用这个方案,不然是不适用的。 - (2)uuid
好处就是本地生成,不需要基于数据库;不好之处在于uuid太长了,作为主键性能太差了,不适合用于主键。
适合的场景:如果你是要随机生成个什么文件名了,编号之类的,你可以用uuid,但是作为主键是不能用uuid的。UUID.randomUUID().toString().replace("-","") -> sfsdf2423432fsfwe..
- (3) 获取系统当前时间
这个就是获取当前时间即可,但是问题是,并发很高的时候,比如一秒并发几千,会有重复的情况,这个是肯定不合适的。基本就不考虑了。
适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个id,如果业务上可以接受的话也是可以使用的。可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号,订单编号,时间戳 + 用户id + 业务含义编码。 - (4)snowflake 算法
twitter开源的分布式id生成算法,就是一个64位的long型的id,1个bit是不可用的,用其中的41bit作为毫秒数,用10bit作为工作机器id,12bit作为序列号
1bit:不用,因为二进制里第一个bit如果是1,那么都是负数,但是我们生成的id都是正数,所以第一个bit统一都是0
41bit:表示的是时间戳,单位是毫秒。41bit可以表示的数字多达2^41-1, 也就是可以标识2^41-1个毫秒值,换算成年就是表示69年的时间
10bit:记录工作机器id,代表的是这个服务最多可以部署在2^10台机器上,也就是1024台机器,但是10bit里5个bit代表机房id,5个bit代表机器id。意思就是最多代表32个机房,每个机房可以代表32台机器。
12bit:这个是用来记录同一个毫秒内产生的不同id,12bit可以代表的最大正整数是2^12-1=4096,也就是说可以用这个12bit代表的数字来区分同一个毫秒内的4096个不同的id
30. 说说MySQL读写分离的原理?主从同步延时咋解决?
- 01_为什么MySQL要读写分离?
- 02_MySQL主从复制原理
- 03_MySQL主从延迟导致的生产环境的问题