一、分布式强一致性和最终一致性选型?

1.为什么会有一致性问题

分布式架构下,原来单体的内部调用,会变成分布式调用。如果一个操作涉及多个微服务的

数据修改,就会产生数据一致性的问题。

如果需要强一致性,那在业务层上就需要使用“两阶段提交”这样的方式。但是好在我们的很多情况下并不需要这么强的一致性,而且强一致性的最佳保证基本都是在底层完成的,且会消耗一定的性能。在我们接触到的大多数业务中,其实只需要最终一致性就够了。

数据一致性有强一致性和最终一致性两种,它们实现方案不一样,实施代价也不一样。

2.分布式事务实现强一致性

对于实时性要求高的强一致性业务场景,你可以采用分布式事务,但分布式事务有性能代

价,在设计时我们需平衡考虑业务拆分、数据一致性、性能和实现的复杂度,尽量避免分布

式事务的产生。

3.领域事件驱动实现分布式事务的最终一致性

领域事件驱动的异步方式是分布式架构常用的设计方法,它可以解决非实时场景的数据最终

一致性问题。基于消息中间件的领域事件发布和订阅,可以很好地解耦微服务。通过削峰填

谷,可以减轻数据库实时访问压力,提高业务吞吐量和处理能力。你还可以通过事件驱动实

现读写分离,提高数据库访问性能。对最终一致性的场景,我建议你采用领域事件驱动的设

计方法。

二、业务补偿设计

1.为什么需要业务补偿

有了对 ACID 和 BASE 的分析,我们知道,在很多情况下,我们是无法做到强一致的 ACID 的。特别是我们需要跨多个系统的时候,而且这些系统还不是由一个公司所提供的。比如,在我们的日常生活中,我们经常会遇到这样的情况,就是要找很多方协调很多事,而且要保证我们每一件事都成功,否则整件事就做不到。

比如,要出门旅游, 我们需要干这么几件事。第一,向公司请假,拿到相应的假期;第二,订飞机票或是火车票;第三,订酒店;第四,租车。这四件事中,前三件必需完全成功,我们才能出行,而第四件事只是一个锦上添花的事,但第四件事一旦确定,那么也会成为整个事务的一部分。这些事都是要向不同的组织或系统请求。我们可以并行地做这些事,而如果某个事有变化,其它的事都会跟着出现一些变化。

设想下面的几种情况。

我没有订到返程机票,那么我就去不了了。我需要把订到的去程机票,酒店、租到的车都给取消了,并且把请的假也取消了。 如果我假也请好了,机票,酒店也订好了,只是车没租到,那么并不影响我出行这个事,整个事还是可以继续的。 如果我的飞机因为天气原因取消或是晚点了,那么我被迫要去调整和修改我的酒店预订和租车的预订。

从人类的实际生活当中,我们可以看出,上述的这些情况都是天天在发生的事情。所以,我们的分布式系统也是一样的,也是需要处理这样的事情——就是当条件不满足,或是有变化的时候,需要从业务上做相应的整体事务的补偿。

一般来说,业务的事务补偿都是需要一个工作流引擎的。亚马逊是一个超级喜欢工作流引擎的公司,这个工作流引擎把各式各样的服务给串联在一起,并在工作流上做相应的业务补偿,整个过程设计成为最终一致性的。

对于业务补偿来说,首先需要将服务做成幂等性的,如果一个事务失败了或是超时了,我们需要不断地重试,努力地达到最终我们想要的状态。然后,如果我们不能达到这个我们想要的状态,我们需要把整个状态恢复到之前的状态。另外,如果有变化的请求,我们需要启动整个事务的业务更新机制。

所以,一个好的业务补偿机制需要做到下面这几点。

要能清楚地描述出要达到什么样的状态(比如:请假、机票、酒店这三个都必须成功,租车是可选的),以及如果其中的条件不满足,那么,我们要回退到哪一个状态。这就是所谓的整个业务的起始状态定义。 当整条业务跑起来的时候,我们可以串行或并行地做这些事。对于旅游订票是可以并行的,但是对于网购流程(下单、支付、送货)是不能并行的。总之,我们的系统需要努力地通过一系列的操作达到一个我们想要的状态。如果达不到,就需要通过补偿机制回滚到之前的状态。这就是所谓的状态拟合。 对于已经完成的事务进行整体修改,可以考虑成一个修改事务。

其实,在纯技术的世界里也有这样的事。比如,线上运维系统需要发布一个新的服务或是对一个已有的服务进行水平扩展,我们需要先找到相应的机器,然后初始化环境,再部署上应用,再做相应的健康检查,最后接入流量。这一系列的动作都要完全成功,所以,我们的部署系统就需要管理好整个过程和相关的运行状态。

2.业务补偿的设计重点

业务补偿主要做两件事。

努力地把一个业务流程执行完成。如果执行不下去,需要启动补偿机制,回滚业务流程。

所以,下面是几个重点。

因为要把一个业务流程执行完成,需要这个流程中所涉及的服务方支持幂等性。并且在上游有重试机制。 我们需要小心维护和监控整个过程的状态,所以,千万不要把这些状态放到不同的组件中,最好是一个业务流程的控制方来做这个事,也就是一个工作流引擎。所以,这个工作流引擎是需要高可用和稳定的。这就好像旅行代理机构一样,我们把需求告诉它,它会帮我们搞定所有的事。如果有问题,也会帮我们回滚和补偿的。 补偿的业务逻辑和流程不一定非得是严格反向操作。有时候可以并行,有时候,可能会更简单。总之,设计业务正向流程的时候,也需要设计业务的反向补偿流程。 我们要清楚地知道,业务补偿的业务逻辑是强业务相关的,很难做成通用的。 下层的业务方最好提供短期的资源预留机制。就像电商中的把货品的库存预先占住等待用户在 15 分钟内支付。如果没有收到用户的支付,则释放库存。然后回滚到之前的下单操作,等待用户重新下单。

3.重试设计

1.为什么需要设计重试

关于重试,这个模式应该是一个很普遍的设计模式了。当我们把单体应用服务化,尤其是微服务化,本来在一个进程内的函数调用就成了远程调用,这样就会涉及到网络上的问题。

网络上有很多的各式各样的组件,如:DNS 服务、网卡、交换机、路由器、负载均衡等设备,这些设备都不一定是稳定的,在数据传输的整个过程中,只要一个环节出了问题,那么都会导致问题。

所以,我们需要一个重试的机制。

2.重试设计的核心策略

重试的设计重点主要如下:

1.要确定什么样的错误下需要重试

我们需要明白的是,“重试”的语义是我们认为这个故障是暂时的,而不是永久的,所以,我们会去重试。

设计重试时,我们需要定义出什么情况下需要重试,例如,调用超时、被调用端返回了某种可以重试的错误(如繁忙中、流控中、维护中、资源不足等)。

而对于一些别的错误,则最好不要重试,比如:业务级的错误(如没有权限、或是非法数据等错误),技术上的错误(如:HTTP 的 503 等,这种原因可能是触发了代码的 bug,重试下去没有意义)。

2.区分是同步重试还是异步重试

异步重试是指不立刻进行重试,常见的做法有记录错误请求上下文,然后交给job去重试。或者开启一个异步线程去重试,更高级的玩法还有记录到重试队列里面进行重试等等

3.设计重试的时间和重试的次数阀值

关于重试的设计,一般来说,都需要有个重试的最大值,经过一段时间不断的重试后,就没有必要再重试了,应该报故障了

这种在不同的情况下要有不同的考量。有时候,而对一些不是很重要的问题时,我们应该更快失败而不是重试一段时间若干次。比如一个前端的交互需要用到后端的服务。这种情况下,在面对错误的时候,应该快速失败报错(比如:网络错误请重试)。而面对其它的一些错误,比如流控,那么应该使用指数退避的方式,以避免造成更多的流量。

如果超过重试次数,或是一段时间,那么重试就没有意义了。这个时候,说明这个错误不是一个短暂的错误,那么我们对于新来的请求,就没有必要再进行重试了,这个时候对新的请求直接返回错误就好了。但是,这样一来,如果后端恢复了,我们怎么知道呢,此时需要使用我们的熔断设计了。这个在后面会说。

4.设计重试的幂等

重试还需要考虑被调用方是否有幂等的设计。如果没有,那么重试是不安全的,可能会导致一个相同的操作被执行多次。

5.设计重试指数级退避

在重试过程中,每一次重试失败时都应该休息一会儿再重试,这样可以避免因为重试过快而导致网络上的负担加重。

在重试的设计中,我们一般都会引入,Exponential Backoff 的策略,也就是所谓的 " 指数级退避 "。在这种情况下,每一次重试所需要的休息时间都会成倍增加。这种机制主要是用来让被调用方能够有更多的时间来从容处理我们的请求。这其实和 TCP 的拥塞控制有点像。

我们定义一个 Exponential Backoff 的函数,其返回 2 的指数。这样,每多一次重试就需要多等一段时间。如:第一次等 200ms,第二次要 400ms,第三次要等 800ms……

public static long getWaitTimeExp(int retryCount) {

long waitTime = ((long) Math.pow(2, retryCount) );

return waitTime;

}

下面是真正的重试逻辑。我们可以看到,在成功的情况下,以及不属于我们定义的错误下,我们是不需要重试的,而两次重试间需要等的时间是以指数上升的。

public static void doOperationAndWaitForResult() {

// Do some asynchronous operation.

long token = asyncOperation();

int retries = 0;

boolean retry = false;

do {

// Get the result of the asynchronous operation.

Results result = getAsyncOperationResult(token);

if (Results.SUCCESS == result) {

retry = false;

} else if ( (Results.NOT_READY == result) ||

(Results.TOO_BUSY == result) ||

(Results.NO_RESOURCE == result) ||

(Results.SERVER_ERROR == result) ) {

retry = true;

} else {

retry = false;

}

if (retry) {

long waitTime = Math.min(getWaitTimeExp(retries), MAX_WAIT_INTERVAL);

// Wait for the next Retry.

Thread.sleep(waitTime);

}

} while (retry && (retries++ < MAX_RETRIES));

}

6.重试的设计代码侵入性小

重试的代码比较简单也比较通用,完全可以不用侵入到业务代码中。这里有两个模式。一个是代码级的,像 Java 那样可以使用 Annotation 的方式(在 Spring 中你可以用到这样的注解),如果没有注解也可以包装在底层库或是 SDK 库中不需要让上层业务感知到。另外一种是走 Service Mesh 的方式(关于 Service Mesh 的方式,我会在后面的文章中介绍)。

7.缓存上下文

对于有事务相关的操作。我们可能会希望能重试成功,而不至于走业务补偿那样的复杂的回退流程。对此,我们可能需要一个比较长的时间来做重试,但是我们需要保存请求的上下文,这可能对程序的运行有比较大的开销,因此,有一些设计会先把这样的上下文暂存在本机或是数据库中,然后腾出资源来做别的事,过一会再回来把之前的请求从存储中捞出来重试。

3.重试的开源解决方案

1.Spring 的重试策略

Spring Retry 是一个单独实现重试功能的项目,我们可以通过 Annotation 的方式使用。具体如下。

@Service

public interface MyService {

@Retryable(

value = { SQLException.class },

maxAttempts = 2,

backoff = @Backoff(delay = 5000))

void retryService(String sql) throws SQLException;

...

}

配置 @Retryable 注解,只对 SQLException 的异常进行重试,重试两次,每次延时 5000ms。相关的细节可以看相应的文档。我在这里,只想让你看一下 Spring 有哪些重试的策略。

NeverRetryPolicy:只允许调用 RetryCallback 一次,不允许重试。 AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环。 SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为 3 次,RetryTemplate 默认使用的策略。 TimeoutRetryPolicy:超时时间重试策略,默认超时时间为 1 秒,在指定的超时时间内允许重试。 CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置 3 个参数 openTimeout、resetTimeout 和 delegate;关于熔断,会在后面描述。 CompositeRetryPolicy:组合重试策略。有两种组合方式,乐观组合重试策略是指只要有一个策略允许重试即可以,悲观组合重试策略是指只要有一个策略不允许重试即不可以。但不管哪种组合方式,组合中的每一个策略都会执行。

关于 Backoff 的策略如下。

NoBackOffPolicy:无退避算法策略,即当重试时是立即重试; FixedBackOffPolicy:固定时间的退避策略,需设置参数 sleeper 和 backOffPeriod,sleeper 指定等待策略,默认是 Thread.sleep,即线程休眠,backOffPeriod 指定休眠时间,默认 1 秒。 UniformRandomBackOffPolicy:随机时间退避策略,需设置 sleeper、minBackOffPeriod 和 maxBackOffPeriod。该策略在 [minBackOffPeriod, maxBackOffPeriod] 之间取一个随机休眠时间,minBackOffPeriod 默认为 500 毫秒,maxBackOffPeriod 默认为 1500 毫秒。 ExponentialBackOffPolicy:指数退避策略,需设置参数 sleeper、initialInterval、maxInterval 和 multiplier。initialInterval 指定初始休眠时间,默认为 100 毫秒。maxInterval 指定最大休眠时间,默认为 30 秒。multiplier 指定乘数,即下一次休眠时间为当前休眠时间 *multiplier。 ExponentialRandomBackOffPolicy:随机指数退避策略,引入随机乘数,之前说过固定乘数可能会引起很多服务同时重试导致 DDos,使用随机休眠时间来避免这种情况。

三、服务状态选型和设计

在幂等设计中,为了过滤掉已经处理过的请求,其中需要保存处理过的状态,为了把服务做成无状态的,我们引入了第三方的存储。我认为,只有清楚地了解了状态这个事,我们才有可能设计出更好或是更有弹力的系统架构。

服务状态的定义

所谓“状态”,就是为了保留程序的一些数据或是上下文。比如之前幂等性设计中所说的需要保留每一次请求的状态,或是像用户登录时的 Session,我们需要这个 Session 来判断这个请求的合法性,还有一个业务流程中需要让多个服务组合起来形成一个业务逻辑的运行上下文 Context。这些都是所谓的状态。

1.无状态的服务

一直以来,无状态的服务都被当作分布式服务设计的最佳实践和铁律。因为无状态的服务对于扩展性和运维实在是太方便了。没有状态的服务,可以随意地增加和减少结点,同样可以随意地搬迁。而且,无状态的服务可以大幅度降低代码的复杂度以及 Bug 数,因为没有状态,所以也没有明显的“副作用”。

基本上来说,无状态的服务和“函数式编程”的思维方式如出一辙。在函数式编程中,一个铁律是,函数是无状态的。换句话说,函数是 immutable 不变的,所有的函数只描述其逻辑和算法,根本不保存数据,也不会修改输入的数据,而是把计算好的结果返回出去,哪怕要把输入的数据重新拷贝一份并只做少量的修改

1.无状态的服务对三方存储的需求

现实世界是一定会有状态的。这些状态可能表现在如下的几个方面。

程序调用的结果。

服务组合下的上下文。

服务的配置。

为了做出无状态的服务,我们通常需要把状态保存到其他的地方。比如,不太重要的数据可以放到 Redis 中,重要的数据可以放到 MySQL 中,或是像 ZooKeeper/Etcd 这样的高可用的强一致性的存储中,或是分布式文件系统中。

于是,我们为了做成无状态的服务,会导致这些服务需要耦合第三方有状态的存储服务。一方面是有依赖,另一方面也增加了网络开销,导致服务的响应时间也会变慢。

所以,第三方的这些存储服务也必须要做成高可用高扩展的方式。而且,为了减少网络开销,还需要在无状态的服务中增加缓存机制。然而,下次这个用户的请求并不一定会在同一台机器,所以,这个缓存会在所有的机器上都创建,也算是一种浪费吧。

这种“转移责任”的玩法也催生出了对分布式存储的强烈需求。正如之前在《分布式系统架构的本质》系列文章中谈到的关键技术之一的“状态 / 数据调度”所说的,因为数据层的 scheme 众多,所以,很难做出一个放之四海皆准的分布式存储系统。

这也是为什么无状态的服务需要依赖于像 ZooKeeper/Etcd 这样的高可用的有强一致的服务,或是依赖于底层的分布式文件系统(像开源的 Ceph 和 GlusterFS)。而现在分布式数据库也开始将服务和存储分离,也是为了让自己的系统更有弹力。

2.有状态的服务

在今天看来,有状态的服务在今天看上去的确比较“反动”,但是,我们也需要比较一下它和无状态服务的优劣。

正如上面所说的,无状态服务在程序 Bug 上和水平扩展上有非常优秀的表现,但是其需要把状态存放在一个第三方存储上,增加了网络开销,而在服务内的缓存需要在所有的服务实例上都有(因为每次请求不会都落在同一个服务实例上),这是比较浪费资源的。

而有状态的服务有这些好处。

数据本地化(Data Locality)。一方面状态和数据是本机保存,这方面不但有更低的延时,而且对于数据密集型的应用来说,这会更快。

更高的可用性和更强的一致性。也就是 CAP 原理中的 A 和 C(单机应用天然满足A和C)。

为什么会这样呢?因为对于有状态的服务,我们需要对于客户端传来的请求,都必需保证其落在同一个实例上,这叫 Sticky Session 或是 Sticky Connection。这样一来,我们完全不需要考虑数据要被加载到不同的结点上去,而且这样的模型更容易理解和实现。

可见,最重要的区别就是,无状态的服务需要我们把数据同步到不同的结点上,而有状态的服务通过 Sticky Session 做数据分片(当然,同步有同步的问题,分片也有分片的问题,这两者没有谁比谁好,都有 trade-off)。

这种 Sticky Session 是怎么实现的呢?

最简单的实现就是用持久化的长连接。就算是 HTTP 协议也要用长连接。或是通过一个简单的哈希(hash)算法,比如,通过 uid 求模的方式,走一致性哈希的玩法,也可以方便地做水平扩展。

然而,这种方式也会带来问题,那就是,结点的负载和数据并不会很均匀。尤其是长连接的方式,连上了就不断了。所以,玩长连接的玩法一般都会有一种叫“反向压力 (Back Pressure)”。也就是说,如果服务端成为了热点,那么就主动断连接,这种玩法也比较危险,需要客户端的配合,否则容易出 Bug。

如果要做到负载和数据均匀的话,我们需要有一个元数据索引来映射后端服务实例和请求的对应关键,还需要一个路由结点,这个路由结点会根据元数据索引来路由,而这个元数据索引表会根据后端服务的压力来重新组织相关的映射。

当然,我们可以把这个路由结点给去掉,让有状态的服务直接路由。要做到这点,一般来说,有两种方式。一种是直接使用配置,在节点启动时把其元数据读到内存中,但是这样一来增加或减少结点都需要更新这个配置,会导致其它结点也一同要重新读入。

另一种比较好的做法是使用到 Gossip 协议,通过这个协议在各个节点之间互相散播消息来同步元数据,这样新增或减少结点,集群内部可以很容易重新分配(听起来要实现好真的好复杂)。

在有状态的服务上做自动化伸缩的是有一些相关的真实案例的。比如,Facebook 的 Scuba,这是一个分布式的内存数据库,它使用了静态的方式,也就是上面的第一种方式。Uber 的 Ringpop 是一个开源的 Node.js 的根据地理位置分片的路由请求的库(开源地址为:https://github.com/uber-node/ringpop-node )。

还有微软的 Orleans,Halo 4 就是基于其开发的,其使用了 Gossip 协议,一致性哈希和 DHT 技术相结合的方式。用户通过其 ID 的一致性哈希算法映射到一个节点上,而这个节点保存了这个用户对应的 DHT,再通过 DHT 定位到处理用户请求的位置,这个项目也是开源的(开源地址为: https://github.com/dotnet/orleans )。

关于可扩展的有状态服务,这里强烈推荐 Twitter 的美女工程师 Caitie McCaffrey 的演讲 Youtube 视频《Building Scalable Stateful Service》(演讲 PPT),其文字版是在 High Scalability 上的这篇文章《Making the Case for Building Scalable Stateful Services in the Modern Era》

3.服务状态的弹性容错设计

在容错设计中,服务状态是一件非常复杂的事。尤其对于运维来说,因为你要调度服务就需要调度服务的状态,迁移服务的状态就需要迁移服务的数据。在数据量比较大的情况下,这一点就变得更为困难了。

虽然上述有状态的服务的调度通过 Sticky Session 的方式是一种方式,但我依然觉得理论上来说虽然可以这么干,这实际在运维的过程中,这么干还是件挺麻烦的事儿,不是很好的玩法。

很多系统的高可用的设计都会采取数据在运行时就复制的方案,比如:ZooKeeper、Kafka、Redis 或是 ElasticSearch 等等。在运行时进行数据复制就需要考虑一致性的问题,所以,强一致性的系统一般会使用两阶段提交。

这要求所有的结点都需要有一致的结果,这是 CAP 里的 CA 系统。而也有的系统采用的是大多数人一致就可以了,比如 Paxos 算法,这是 CP 系统。

但我们需要知道,即使是这样,当一个结点挂掉了以后,在另外一个地方重新恢复这个结点时,这个结点需要把数据同步过来才能提供服务。然而,如果数据量过大,这个过程可能会很漫长,这也会影响我们系统的可用性。

所以,我们需要使用底层的分布式文件系统,对于有状态的数据不但在运行时进行多结点间的复制,同时为了避免挂掉,还需要把数据持久化在硬盘上,这个硬盘可以是挂载到本地硬盘的一个外部分布式的文件卷。

这样当结点挂掉以后,以另外一个宿主机上启动一个新的服务实例时,这个服务可以从远程把之前的文件系统挂载过来。然后,在启动的过程中就装载好了大多数的数据,从而可以从网络其它结点上同步少量的数据,因而可以快速地恢复和提供服务。

这一点,对于有状态的服务来说非常关键。所以,使用一个分布式文件系统是调度有状态服务的关键。

BFF 应用层服务-前端与中台的解耦合

企业级业务流程往往是多个微服务一起协作完成的,每个单一职责的微服务就像积木块,它

们只完成自己特定的功能。那如何组织这些微服务,完成企业级业务编排和协同呢?你可以在微服务和前端应用之间,增加一层 BFF 微服务(Backend for Frontends)。

BFF主要职责是处理微服务之间的服务组合和编排,微服务内的应用服务也是处理服务的组合和编排,那这二者有什么差异呢?

BFF 位于中台微服务之上,主要职责是微服务之间的服务协调;应用服务主要处理微服务内

的服务组合和编排。在设计时我们应尽可能地将可复用的服务能力往下层沉淀,在实现能力

复用的同时,还可以避免跨中心的服务调用。BFF 像齿轮一样,来适配前端应用与微服务之间的步调。它通过 Façade 服务适配不同的前端,通过服务组合和编排,组织和协调微服务。BFF 微服务可根据需求和流程变化,与前端应用版本协同发布,避免中台微服务为适配前端需求的变化,而频繁地修改和发布版本,从而保证微服务核心领域逻辑的稳定。如果你的 BFF 做得足够强大,它就是一个集成了不同中台微服务能力、面向多渠道应用的业务能力平台。

多中心多活的设计

分布式架构的高可用主要通过多活设计来实现,多中心多活是一个非常复杂的工程,下面我

主要列出以下几个关键的设计。

1. 选择合适的分布式数据库。数据库应该支持多数据中心部署,满足数据多副本以及数据

底层复制和同步技术要求,以及数据恢复的时效性要求。

2. 单元化架构设计。将若干个应用组成的业务单元作为部署的基本单位,实现同城和异地

多活部署,以及跨中心弹性扩容。各单元业务功能自包含,所有业务流程都可在本单元完

成;任意单元的数据在多个数据中心有副本,不会因故障而造成数据丢失;任何单元故障不

影响其它同类单元的正常运行。单元化设计时我们要尽量避免跨数据中心和单元的调用。

单元化架构一般运用于大型saas企业中。

3. 访问路由。访问路由包括接入层、应用层和数据层的路由,确保前端访问能够按照路由

准确到达数据中心和业务单元,准确写入或获取业务数据所在的数据库。

4. 全局配置数据管理。实现各数据中心全局配置数据的统一管理,每个数据中心全局配置

数据实时同步,保证数据的一致性

四、重构

1.重构的定义

其实大多数对系统的改造都是不能叫做重构的,最多只能叫做重写。重写是细粒度的,而重构是粗粒度的。

常见的重构一般包括更换或者引入中间件,删除中间件,服务合并或者拆分,更换通信协议等大动作。

比如

1.之前勇哥组织的跨部门去依赖,将所有对外部系统的接口调用都使用dubbo这种rpc协议进行通信,去除引入jar包的方式直接调用sql去读别人的库,这样讲服务之间的耦合关系降低到了最低。

2.也是之前勇哥实例,将所有对外部读的依赖都采用降级的处理方式,防止服务雪崩

3.也是之前勇哥实例,将精进网关系统抽离出来,作为精进部门独有的网关,去除了对外部系统的依赖,同时下线掉不重要的服务,将他并入到精进系统中,并且拆分出核心的应用比如积分等,作为独立的服务。

2.重构的难点

1.技术难点

需要考虑各种兼容性问题

2.沟通推动

这才是最困难的地方,要同时说服自己的上下级和同级甚至跨部门,没有一定的影响力是很难做到的。

五、open-API的设计

1.依赖第三方

1.领域对象

为数不少的团队都在自己的业务代码中直接使用了第三方代码中的对象,第三方的任何修改都会让你的代码跟着改,你的团队就只能疲于奔命。 解决这个问题最好的办法就是把它们分开,你的领域层只依赖于你的领域对象,第三方发过来的内容先做一次转换,转换成你的领域对象。

这种做法称为防腐层。 当我们把领域模型看成了整个设计的核心,看待其他层的视角也会随之转变,它们只不过是适配到不同地方的一种方式而已,即使三方接口的实现厂商发生了变化,也不会对我方系统造成变动影响。而这种理念的推广,就是一些人在说的六边形架构。

将外部对象转换成我们自己的领域对象,就是适配器接口做的事情。

2.异常处理

任何依赖的第三方接口都是不可信赖的,鬼知道他会有什么莫名其妙的问题,所以一定要进行异常捕获和进行降级处理。

3.日志规范

把它的入参和反参打印出来这是必须的,如果没有接口检测系统,那它的耗时等也是有必要打印出来的。

2.作为接口供应商

1.错误码定义

对应的已知正常的业务异常,通过错误码来进行定义,如身份认证失败,xxx不能为空,xxx已存在。千万不要告诉我,等客服发现问题时你来查日志。因为双方没有那么多时间,然后客服会对你作为供应商的印象大打折扣,损害的是团队和公司的形象。

2.安全问题

1.接口如入参做校验,参数大小做限制等。

2.身份认证等基础验证,一般基于OAuth2协议进行安全认证

3.多租户隔离

一般使用策略模式等设计模式,降低各个租户的耦合性

六、数据库做运算和服务器内存做运算选型

1.服务器内存做运算

Java语言在内存中进行数据运算的优势:

1. 快速:在内存中进行数据运算可以大大提高计算速度,因为内存的读写速度比磁盘快得多。

2. 灵活:Java语言提供了丰富的数据结构和算法库,可以方便地进行各种数据运算和处理,满足不同的需求。

3. 实时性:内存中的数据运算可以实时响应用户的请求,适用于需要即时计算结果的场景。

Java语言在内存中进行数据运算的劣势:

1. 有限的内存空间:内存的容量有限,无法存储大量的数据,当数据量过大时,可能会导致内存溢出或性能下降。

2. 数据持久性:内存中的数据是临时存储的,一旦程序结束或重启,数据将丢失。如果需要长期保存数据,需要将数据写入持久化存储介质,例如数据库。

2.数据库做运算

从数据处理方面来看,SQL 不仅使用更加广泛,一般在性能上也更有优势。SQL 的高性能主要源于以下两方面:

优化引擎。SQL 作为声明式语言,通常用户只需要描述需要完成的任务,而不需要关心具体的实现细节。数据库会根据 SQL 语句的描述自动优化查询计划和执行方式,从而提高查询效率,这就是优化引擎的作用。数据库对于常规运算都有很成熟的算法,很多计算在优化引擎的帮助下可以快速完成。 数据存储。计算和数据存储密不可分,而数据库集二者于一身。有了存储就可以在工程上实施很多提速手段,如索引、缓存、分区、冗余等。特别是以 AP 为主的数据仓库还可以针对计算而设计专门的存储(如列存),存储与算法相互配合就可以实现高效率。

不过,SQL 也有力有不逮的时候。由于 SQL 描述能力的局限,很多复杂查询要采取迂回的方法,写出来很繁琐。更重要的是,一旦 SQL 语句的复杂度上来,优化引擎就很难发挥作用了(猜不出目标只能按照字面表达去执行,性能很差),因此优化引擎仅对简单情况有效。而能让优化器失效的 SQL 复杂度其实很低。

另一方面,SQL 数据类型和算法不够全面,如果要用到超出范围的算法就需要自己实现,如编写自定义函数(UDF)。但当基础数据类型不支持,或需要根据计算特征设计存储时 UDF 也无能为力。

通过MySQL的表进行运算的优势:

1. 数据持久化:MySQL数据库可以将数据持久化保存在磁盘上,即使程序结束或重启,数据也不会丢失。

2. 大数据处理:MySQL可以处理大量的数据,因为它可以利用磁盘的存储空间。

3. 数据安全性:MySQL提供了数据的安全性和完整性保障,例如事务和ACID特性。

通过MySQL的表进行运算的劣势: 1. I/O开销:由于需要读取和写入磁盘,通过MySQL的表进行数据运算的速度相对较慢,特别是对于大量的数据操作。

2. 数据一致性:当多个应用程序同时对MySQL表进行读写操作时,可能会出现数据一致性的问题,需要使用事务进行管理。

3. 查询灵活性:相比于Java语言在内存中进行数据运算,通过MySQL的表进行运算的查询灵活性较低,需要使用SQL语句进行查询和过滤。

3.总结

1.设计角度

为了满足扩展性的需要,一般采用降低数据库与应用的耦合性(数据库仅用于存储),整体架构更加灵活,应用扩展和维护都比较方便。

2.性能角度

如果只是简单的语句如sum,count,group by等,可以使用数据库去做运算,其性能也是非常的高,而不用担心jvm内存溢出等风险,但如果掺杂着复杂的条件判断等,就放在服务器内存去做运算吧。

相关阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: