如何设计分布式系统

设计分布式系统主要考虑以下四个大点,两个小点:
四个大点

  • 容错能力(服务隔离、异步调用、请求幂等性、分布式锁)
  • 可伸缩性(有 / 无状态的服务)
  • 一致性(补偿事务、重试)
  • 应对大流量的能力(熔断、降级)

两个小点

  • 解耦(MQ)
  • 线程池

四个大点的具体论述

1、容错能力(服务隔离、异步调用、请求幂等性、分布式锁)

我们都知道,系统出现故障是非常常见的,所以我们应该把处理故障的代码当成正常的功能做在架构里写在代码里,使得故障真正发生时,系统还可以运行

  • 服务隔离:是为了在系统发生故障时缩小其影响范围从而保证只有出问题的服务不可用,其他服务还是可用的。

  • 异步调用:发送方发送请求后,接收方直接返回正在处理,通过轮询或者回调的方式返回结果;异步调用相比于同步调用的最大好处是可以快速响应客户端的请求,至于具体的请求结果,通过异步回调的方式发送给客户端,这种方式在服务端平均处理请求时间过长的业务场景下很好用,避免在高请求流量下的超时、阻塞等问题;

  • 请求幂等:幂等并不是每次请求的结果都一样,而是一次和多次请求某一个资源应该具有同样的副作用,f(x) = f(f(x)),要做到幂等性的交易接口,需要有一个唯一的标识,来标志交易是同一笔交易,这个标识要能做到全局唯一。我们通常保证幂等的判断是从数据库查询有没有相同id的记录,但是在分布式系统环境下,可能有主从同步问题:两个相同的请求request1、request2时间间隔很短,request1请求过来的时候,查询从库发现没有对应记录,则request1开始操作插入主库record1,但是还没有同步到从库;此时request2查询从库(主从还未同步)也发现没有相同id的记录,准备插入有相同id的记录record2,这个时候request1成功插入record1,request2开始插入record2,数据库报错:唯一约束被破坏相关的异常日志;解决这个问题有两种方法:1、读写都强制走主库;2、采用分布式锁,考虑性能问题,一般都选2

  • 分布式锁:分布式系统一般都有多台机器,常规的多线程锁已经无法解决问题;最简单用redis实现:思路很简单,主要用到的redis函数是setnx()。首先是将某一任务标识名UniqueKey(能唯一识别一个请求的标识)作为键存到redis里,并为其设个过期时间,如果是同样的请求过来,先是通过setnx()看看是否能将UniqueKey插入到redis里,可以的话就返回true,不可以就返回false。

分布式锁设计原则:

  • 互斥性,同一时间只有一个线程持有锁
  • 容错性,即使某一个持有锁的线程,异常退出,其他线程仍可获得锁
  • 隔离性,线程只能解自己的锁,不能解其他线程的锁

2、可伸缩性(有 / 无状态的服务)

什么是系统的可伸缩性?
如果你的系统对一个用户来说是快的,但是在用户不断增长的高访问量下就慢了
伸缩性方案
垂直伸缩: 升级到更强大的服务器(多CPU 昂贵大中型机)。
水平伸缩:

  • 状态的扩展(内存或数据库):读写分离、分库分表,静态页面缓存,NOSQL
  • 无状态的扩展(计算可伸缩性):侧重行为计算方面,类似提升CPU处理能力。

3、一致性(补偿事务、重试)

  • 强一致性:数据高度统一,无需担心同一时刻会获得不同的数据。就好像交易系统,存取钱的+/-操作必须是马上一致的。
  • 弱一致性:数据经过一个时间窗口之后,只要多尝试几次,最终的状态是一致的,是最新的数据。比如发了一条微博,改了某些配置,可能不会马上生效,但刷新几次后就可以看到了
  • 重试:由于网络等问题,一些请求无法确定是否成功,这个时候需要重试,即在此发送请求给服务端,处理逻辑是将请求包在一个重试循环里

4、应对大流量的能力(熔断、降级、限流)

  • 熔断(慎用):如果系统中,某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用;熔断主要是应对流量引起的问题,使服务处于关闭、半关闭状态,以保证部分业务成功,或者舍弃次要业务使主业务运行通畅;

  • 限流:熔断的一种,半开状态,只允许少部分的请求,其他的都拒绝,如果设计得当,被拒绝的请求,客户端会通过重试、补偿操作来完成;

  • 降级:暂时牺牲掉一些服务,保障整个系统的服务。比如:如果服务器已经高负载,可以拒绝老的请求,先处理新的请求,或者关闭部分操作以减轻服务器压力;

两个小点:

1、解耦(MQ)
在软件工程中,对象之间的耦合度就是对象之间的依赖性。对象之间的耦合越高,维护成本越高,因此对象的设计应使模块之间的耦合度尽量小。

在软件架构设计中,模块之间的解耦有两种,假设有两个模块A、B,A依赖B:

第一种:模块A和模块B只通过接口交互,只要接口设计不变,那么模块B内部细节的变化不影响模块A对模块B服务能力的消费。
面向接口设计下真正实现了将接口契约的定义和接口的实现彻底分离,实现变化不影响到接口契约,自然不影响到基于接口的交互。
模块A和B之间的松耦合,主要通过合理的模块划分、接口设计来完成。如果出现循环依赖,可以将模块A、B共同依赖的部分移除到另一个模块C中,将A、B之间的相互依赖,转换为A、B同时对C的依赖。
第二种:将同步调用转换成异步消息交互。
比如在买机票系统中,机票支付完成后需要通知出票系统出票、代金券系统发券。如果使用同步调用,那么出票系统、代金券系统宕机是会影响到机票支付系统,如果另一个系统比如专车系统也想要在机票支付完成后向用户推荐专车服务,那么同步调用模式下机票支付系统就需要为此而改动,容易影响核心支付业务的可靠性。
如果我们将同步调用替换成异步消息,机票支付系统发送机票支付成功的消息到消息中间件,出票系统、代金券系统从消息中间件订阅消息。这样一来,出票系统、代金券系统的宕机也就不会对机票支付系统造成任何影响了。专车系统想要知道机票支付完成这一事件,也只需要从消息中间件订阅消息即可,机票支付系统完全不需要做任何改动。
异步消息解耦,适合那些信息流单向流动(类似发布-订阅这样的),实时性要求不高的系统。常见的开源消息队列框架有:Kafka、RabbitMQ、RocketMQ。
2、线程池
线程池:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,且创建过多的线程会耗费系统资源,频繁的上下文切换也影响系统的性能。

发布了4 篇原创文章 · 获赞 0 · 访问量 146

猜你喜欢

转载自blog.csdn.net/jyxp6946/article/details/104675591
今日推荐