微服务架构设计之解耦合

简介: 微服务设计中,需要考虑到服务的解耦,这样可以避免很多问题。

背景

在各个 IT 行业的公司,我们会有大大小小的业务需求。当每个产品的业务功能越来越繁重时,也许用户的需求其实很简单,就想 One Click。但是,其实这一个按钮背后可能有很多的系统交互的操作在进行,这就涉及到业务数据操作的事务,涉及到每个系统的交互逻辑、先后顺序以及数据的一致性。这些都需要在设计的时候,需要考虑到的问题。

浅谈解耦合

业务系统的设计有多重要
在 今天被问微服务,这几点,让面试官刮目相看 一文中,我们讲过微服务设计时的方方面面。其中核心的六个字可能就是:“高内聚,低耦合”。高内聚,我们在那篇文章中已经讲的很清楚了。那么低耦合,这就涉及到我们业务系统的设计了。所谓低耦合,就是要每个业务模块之间的关系降低,减少冗余、重复、交叉的复杂度,模块功能划分也尽可能单一。这样,才能达到低耦合的目的。

在电商行业,主要的功能就是购物,至于其他的,都是为购物作铺垫、营销手段:直播、促销、发优惠券等。从用户的角度来说,其实网上 shopping 的逻辑很简单:选中想要买的,支付 money 就 OK 了。但对于网站,或者说运营服务平台来说,其逻辑远没有那么简单。下面是一个简单的购物流程图:

在这里,我们看到,就这个简单的购物流程,对于用户来说,可能操作很简单:打开网站,登录后选择商品和选中收货地址支付,坐等收货。对于平台,其实它也不简单,包括了很多系统:用户系统、商品系统、仓库系统、订单系统、支付系统、物流系统等等。

不能仅仅因为客户的需求,只是下了一个订单,买了一件商品,那系统就设计一个就认为能解决所有事情,这种认识,可能一开始就是错的。这样的业务设计后,不但导致业务系统的逻辑很笨重,也会导致代码的 code review 非常之复杂。我曾经就亲自目睹过:好几个事情都是一个代码块来处理,甚至都写到几千行,甚至上万行。这样的思路,虽然可以实现暂时的需求。但是从长远角度,这是一个很要命的事情:这样的设计不仅仅说 code review 很吃力,兼容新功能也是很麻烦的,让后来者无法下手。而且长期下去,会导致表的死锁,甚至进入系统瘫痪状态。

如何解耦合

业务的复杂性,其实根本原因是没有把其给拆解化。如果把整个的大业务拆解成若干个小的需求,那对于实现,就显得即一目了然,又能完美兼容其他任何问题。咱们还是拿购物说事,为什么每个购物 app 的系统设计都是这样的套路:选中商品后必须先加入购物车,选好地址信息,然后再统一去提交订单,最后才去支付 money 呢?难道系统直接简单点,选中后就支付不就解决了吗?那么网站何必搞得这么的麻烦,浪费时间、金钱,是为了折腾人?统统都不是。其实这也是网站开发最初想的事情,并不是说一件事情一口气能解决,就鲁莽的直接一口气解决。也许到时候,时间久了,人的精力没那么旺盛,变得虚弱的时候,那一口气就无法完成了。网站也是,一个需求也许可以简单的设计,就能完成。但是如果仅仅想着,现在简单的就完成,那是对以后的不负责任。以后可能会出现一些难以想象的事情,并且难以解决。

上面扯远了,回归到解耦合,解耦合其实有很多办法。比如 Java 中就有很多解决低耦合的方法:监听、观察模式、异步回调、定时任务、消息中间件等等。

1.1 监听

在Java 里,有很多设计模式:工厂模式、单例模式、建造者模式、代理模式、解释器模式、监听模式、观察者模式等等。其中,监听模式是低耦合解决的方案之一。

所谓监听模式:事件源经过事件的封装传给监听器,当事件源触发事件后,监听器接收到事件对象可以回调事件的方法。这其中涉及到三个信息:事件源、事件、监听器。

For example : 模拟某个服务启动后,发送通知信息。

事件源:

package com.damon.event;


import java.util.ArrayList;
import java.util.List;


public class Context {
  private static List<Listener> list=new ArrayList<Listener>();
  public static void addListener(Listener listener){
    list.add(listener);
  }
  public static void removeListener(Listener listener){
    list.remove(listener);
  }
  public static void sendMsg(Event event){
    for(Listener listener:list){
      listener.onChange(event);
    }
  }
}

事件:

package com.damon.event;


public class Event {
  public static final int INSTALLED = 1;
  public static final int STARTED = 2;
  public static final int RESOLVED = 3;
  public static final int STOPPED = 4;


  private int type;
  private Object source;
  
  public Event(int type, Object source) {
    this.type = type;
    this.source = source;
  }
  public int getType() {
    return type;
  }
  public Object getSource() {
    return source;
  }
}

监听器:

package com.damon.event;


public class MyListener implements Listener {


  @Override
  public void onChange(Event event) {
    switch(event.getType()){
      case Event.STARTED :
        System.out.println("started...");
        break;
      case Event.RESOLVED :
        System.out.println("resolved...");
        break;
      case Event.STOPPED :
        System.out.println("stopped...");
        break;
      default:
        throw new IllegalArgumentException();
    }
  }


}

测试:

package com.damon.event;


public class EventTest {


  public static void main(String[] args) {
    Listener listener = new MyListener();
    //加入监听者
    Context.addListener(listener);
    //启动完毕事件触发
    Context.sendMsg(new Event(Event.STARTED, new MyBundle()));
  }
}

在服务启动的操作中,我们不需要等待或者去处理,而是继续其他的逻辑,等到服务启动后,事件监听器监听后会进行相应的操作。这样,就不会在服务启动的过程中,需要等待其启动,因为其启动的时间是无法估量的。所以就很好的解决其耦合性的问题。避免用户在等待过程中,浪费了大量不应该由用户承担的时间成本。毕竟,对于用户来说,时间就是金钱。

1.2 观察者模式

观察者模式,听着跟上面讲的监听模式有点像。但是,还是有区别的。所谓观察者模式:观察者相当于事件监听者,被观察者相当于事件源和事件,执行逻辑时通知观察者即可触发其 update,同时可传被观察者和其参数。看着是不是像简化了事件监听机制的实现。其又可以叫发布-订阅模式,只有两个角色。

For example : 微信群里发布了一条公告:下午三点开会,有些在群里的人接收到了消息去开会,但是有些人未在群里,未收到公告,被领导主动喊去开会。

观察者:

public abstract class Observer {


    protected String name;
    protected Subject subject;

    public Observer(String name, Subject subject) {
        this.name = name;
        this.subject = subject;
    }

    public abstract void update();

}

通知者:

public interface Subject {
    //增加
    public void attach(Observer observer);
    //删除
    public void detach(Observer observer);
    //通知
    public void notifyObservers();

    //状态
    public void setAction(String action);

    public String getAction();

}

具体人:群管理员

public class WechatManager implements Subject {


    //同事好友列表
    private List<Observer> observers = new LinkedList<>();
    private String action;


    //添加
    @Override
    public void attach(Observer observer) {
        observers.add(observer);
    }


    //删除
    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
    }


    //通知
    @Override
    public void notifyObservers() {
        for(Observer observer : observers) {
            observer.update();
        }
    }


    //状态
    @Override
    public String getAction() {
        return action;
    }


    @Override
    public void setAction(String action) {
        this.action = action;
    }

}

具体观察者:群内人员与群外人员

public class InWechatRoomObserver extends Observer {

    public InWechatRoomObserver(String name, Subject subject) {
        super(name, subject);
    }


    @Override
    public void update() {
        System.out.println(subject.getAction() + "\n" + name + "收到公告,去开会了");
    }


}

Test:

public class Test {
    public static void main(String[] args) {
        //群管理员为通知者
        WechatManager ma = new WechatManager();
        
        InWechatRoomObserver in = new InWechatRoomObserver("tom", ma);
        OutWechatRoomObserver out = new OutWechatRoomObserver("damon", ma);

        //群管理员通知
        ma.attach(out);
        ma.attach(in);

        //damon没在群内,未被通知到,所以被领导发现
        ma.detach(out);

        //老板回来了
        ma.setAction("下午三点,大家在大会议室开会");
        //发通知
        ma.notifyObservers();
    }

}

可以看到:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象有待改变的时候,可考虑使用观察者模式。

即使用观察者模式的动机在于:在保证相关业务数据的一致性,我们不希望为了维持一致性而使各个逻辑紧密耦合,这样会给维护、扩展和重用都带来不便,而观察者模式所做的工作就是在解除耦合。

1.3 异步

异步,对于一个系统来说,异步操作可以很好的解耦合,因为每一步操作不需要等待结果即可继续往下进行,不论中间操作是否成功。在 Java 中,常见的异步注解:@Async,解决相应如果需要很多操作,或者操作时耗时很长,而异步进行处理来解决相关问题。有时需要注解 @EnableAsync 配合,然后弄一个异步线程池,来进行线程异步调度管理。

异步线程池初始化 bean :

package com.damon.task;


import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
 * 异步任务执行bean
 *
 * @author Damon
 *
 */
@EnableAsync
@Configuration
public class TaskPoolConfig {


    @Bean("taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(200);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("taskExecutor-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
}
异步调度方法类:

package com.damon.task;


import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


import org.springframework.http.*;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;






import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
 *
 * 远程业务调用封装类
 * 
 * @author Damon
 * @date 2019年3月19日 下午3:29:45
 *
 */
@Component
public class TaskService {


  private Logger logger = LoggerFactory.getLogger(getClass());


  public static Random random = new Random();


    /**
     * @description 异步任务计算耗时
     * @param start 开始时间
     * @param userId 用户id
     * @throws Exception
     */
  @Async("taskExecutor")
  public void doTaskOne(long start, String userId) throws Exception {
    logger.info(" 开始做任务一 to {}", start);
    Thread.sleep(random.nextInt(10000));
    long end = System.currentTimeMillis();
    logger.info("完成任务一,耗时:" + (end - start) + "毫秒");
  }
}

异步可以常见于很多业务,比如异步发送短讯告诉用户,支付成功,异步发送日志到 ELK 系统等。

1.4 定时任务

对于定时任务,就是指制定系统的某个时刻或每隔一段时间去触发一些逻辑执行,这样来保证业务数据的一致性,消息的一致性,或者数据的实时性。

我们常在 Java 里用 @EnableScheduling 来引入定时器,然后定义一个异步定时调度 bean:

package com.damon.task;


import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;


import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;




/**
 *
 * 异步任务执行bean
 * @author Damon
 * @date 2019年7月17日 上午10:35:56
 *
 */
@EnableAsync
@Configuration
public class TaskPoolConfig implements AsyncConfigurer {


    @Bean("asyncTask")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        //线程池维护线程的最少数量
        executor.setCorePoolSize(10);
        //线程池维护线程的最大数量
        executor.setMaxPoolSize(20);
        //  缓存队列
        executor.setQueueCapacity(200);
        //允许的空闲时间
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("asyncTask-");
        //对拒绝task的处理策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }


  @Override
  public Executor getAsyncExecutor() {
    return null;
  }


  @Override
  public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
    return null;
  }
}

同时,定义一个执行类:

/**
 *
 * 执行调度
 * @author Damon
 * @date 2019年3月19日 下午3:29:45
 *
 */
@Component
public class TaskSchedule {


  private Logger logger = LoggerFactory.getLogger(TaskSchedule.class);


  @Autowired
  private RestTemplate restTemplate;

  @Autowired
    private Environment env;

  //@Scheduled(cron = "0 0/1 * * * ?")//每分钟
  @Scheduled(cron = "0 * * * * ?")//每分钟
  public void dynamicResourceListener() {
    logger.info("resourceLimitHandle timer start");
    String namespace = env.getProperty("INFERENCE_JOB_NAMESPACE");
    resourceListenerCallBack(namespace);
  }
  private void resourceListenerCallBack(String namespace) {

  }
}

其中,cron 从左到右(用空格隔开):

秒 分 小时 月份中的日期 月份 星期中的日期 年份

上面的逻辑是每分钟去执行某个逻辑,这样的业务我们也可能存在,For example:股票系统中,建模等数据一般都是用 Oracle 来存储的,有时候业务可能是用 Mysql,这时,需要一个定时任务来跑数据,常见的叫 ETL,所以 ETL 的由来,就是这样来的。这样的操作肯定不能在发生业务操作时来进行,否则会因为业务数据的海量读取,导致 IO 的性能,甚至内存、CPU 都会飙升。再如统计某个业务场景的数据,都可以通过这种解耦合的方式来处理。

1.5 消息中间件

消息中间件的话,这个也是很多的,比如:redis、rocketmq、rabbitmq、zk等等。这些中间件技术都可以再一个复杂的业务流程起到至关重要得作用。

当我们需要做一个秒杀的功能时,可以用 redis 来作分布式锁,这个能起到缓冲系统压力的作用,同时可以做到秒杀锁。

当我们需要在处理一些业务逻辑时,需要告知其他方,这时候可以用 MQ 来作消息处理,防止处理流程的断续。

当我们需要发送一些消息给外部时,但又不希望耽误当前的业务处理,这时候,可以用 MQ 或 redis 来处理消息。

相关实践学习
快速体验阿里云云消息队列RocketMQ版
本实验将带您快速体验使用云消息队列RocketMQ版Serverless系列实例进行获取接入点、创建Topic、创建订阅组、收发消息、查看消息轨迹和仪表盘。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
相关文章
|
设计模式 Java API
微服务架构演变与架构设计深度解析
【11月更文挑战第14天】在当今的IT行业中,微服务架构已经成为构建大型、复杂系统的重要范式。本文将从微服务架构的背景、业务场景、功能点、底层原理、实战、设计模式等多个方面进行深度解析,并结合京东电商的案例,探讨微服务架构在实际应用中的实施与效果。
691 6
|
设计模式 Java API
微服务架构演变与架构设计深度解析
【11月更文挑战第14天】在当今的IT行业中,微服务架构已经成为构建大型、复杂系统的重要范式。本文将从微服务架构的背景、业务场景、功能点、底层原理、实战、设计模式等多个方面进行深度解析,并结合京东电商的案例,探讨微服务架构在实际应用中的实施与效果。
332 1
|
数据管理 物联网 开发者
现代化后端开发中的微服务架构设计与实现
在当今快速发展的软件开发领域,微服务架构已成为构建高效、可扩展和灵活的后端系统的重要方式。本文将探讨微服务架构的设计原则、实现方法以及应用场景,帮助开发者理解如何在项目中成功应用微服务。【7月更文挑战第4天】
179 2
|
消息中间件 供应链 架构师
微服务如何实现低耦合高内聚?架构师都在用的技巧!
本文介绍了微服务的拆分方法,重点讲解了“高内聚”和“低耦合”两个核心设计原则。高内聚强调每个微服务应专注于单一职责,减少代码修改范围,提高系统稳定性。低耦合则通过接口和消息队列实现服务间的解耦,确保各服务独立运作,提升系统的灵活性和可维护性。通过领域建模和事件通知机制,可以有效实现微服务的高效拆分和管理。
447 7
|
设计模式 API 持续交付
深入理解微服务架构:设计模式与实践
【10月更文挑战第19天】介绍了微服务架构的核心概念、设计模式及最佳实践。文章详细探讨了微服务的独立性、轻量级通信和业务能力,并介绍了聚合器、链式和发布/订阅等设计模式。同时,文章还分享了实施微服务的最佳实践,如定义清晰的服务边界、使用API网关和服务发现机制,以及面临的挑战和职业心得。
|
JavaScript Java API
深入解析微服务的架构设计与实践
深入解析微服务的架构设计与实践
216 0
|
设计模式 存储 运维
微服务架构中的服务发现与注册中心设计模式
在现代软件工程实践中,微服务架构已成为构建灵活、可扩展系统的首选方案。本文将深入探讨微服务架构中至关重要的服务发现与注册中心设计模式。我们将从服务发现的基本原理出发,逐步解析注册中心的工作机制,并以Eureka和Consul为例,对比分析不同实现的优劣。文章旨在为开发者提供一套清晰的指导原则,帮助他们在构建和维护微服务系统时做出更明智的技术选择。
|
缓存 前端开发 JavaScript
前端架构思考:代码复用带来的隐形耦合,可能让大模型造轮子是更好的选择-从 CDN 依赖包被删导致个站打不开到数年前因11 行代码导致上千项目崩溃谈谈npm黑洞 - 统计下你的项目有多少个依赖吧!
最近,我的个人网站因免费CDN上的Vue.js包路径变更导致无法访问,引发了我对前端依赖管理的深刻反思。文章探讨了NPM依赖陷阱、开源库所有权与维护压力、NPM生态问题,并提出减少不必要的依赖、重视模块设计等建议,以提升前端项目的稳定性和可控性。通过“left_pad”事件及个人经历,强调了依赖管理的重要性和让大模型代替人造轮子的潜在收益
316 0
|
前端开发 数据处理 Android开发
Android项目架构设计问题之业务间的解耦合如何解决
Android项目架构设计问题之业务间的解耦合如何解决
162 1
|
存储 测试技术 API
如何避免微服务设计中的耦合问题
如何避免微服务设计中的耦合问题
187 4

热门文章

最新文章