欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Step By Step实现分布式服务访问框架 博客分类: 编程随笔 分布式服务框架架构java实现 

程序员文章站 2024-02-12 08:44:40
...

Step By Step实现分布式服务访问框架

前言

我们知道应用的架构经历了单体架构->垂直架构->SOA架构->分布式架构,其复杂程度也在不断的增加。

 

系统架构在经历了以上的变更后,为什么会逐步演进到分布式服务化的架构呢?

在原来单体架构下,一个系统包括了交互处理、逻辑处理、数据处理的一套完整的过程。因为这个时候系统的各个层面的需求都不复杂,基本上几个开发人员,甚至1个开发人员就可以完成这样的系统开发。这个时候,单体架构简单的特性就体现的淋漓尽致,可以在一个系统中完成所有的开发。举个例子,可能在一开始,客户只是需要一个能够管理客户数据的小应用,能够帮助客户实现客户数据的查询、维护的操作即可。

但是,随着这个应用的使用。客户的需求也在不断的变化,他可能会想,可以在这个基础上实现一些附加的功能。如,想这些客户推销产品,为已经购买了产品的客户提供售后服务。系统功能随着需求的变更不断的在膨胀,在这个时候。我们就会发现在一个单价架构中,要想实现这么多功能是有多么痛苦。功能增多,意味着需要更多的开发者加入到开发中。一个单体的系统架构,就变成了开发的瓶颈,提交文件变成了一种痛苦的过程。同时,当功能变更后,需要对整个系统进行完整的编译、打包、部署等过程。有可能只是引入了一个新的功能需求,但是也不得不完整重复的去做这样的事情。作为有经验的工程师,我们尝试着把不同的功能按照逻辑实现进行聚类,逐步划分到不同的“模块”中,使得相同功能的实现聚合在一个模块中。同时,把这些模块变成独立的“系统”。除了可以交由不同的开发者进行交付,也可以有效隔离功能变更引起其他模块的编译、打包、部署的操作。只要,“模块”和“模块”之间的依赖不变更,是稳定的即可。这就是垂直架构,将系统的需求按照不同的功能进行划分,在不同功能“模块”中(应该是一个子系统)采用单体架构。最后,再将这些子系统集成在一起。

随着垂直架构的发展,子系统和子系统之间的交互需求逐渐变得旺盛。例如,客户的数据会在多个子系统中用到。因此,负责管理客户数据的子系统,暴露了一个客户数据管理的“服务”,从而使得其他子系统方便的通过这个服务使用客户数据。从而,进一步演进得到。客户完全可以基于业务的抽象,将业务过程中需要实现的功能,以服务的形态交由相应的子系统实现。再通过,编排层对服务的编排从而快速完成业务的需求。随着,服务的粒度的拆分以及服务数量的增多,理论上一个业务实现都可以由服务进行编排组合来完成。这个时候,服务的作用体现得越来越重要。这个时候,系统就已经演变为基于SOA的架构了。

SOA架构的解析模型

 

SOA的目标就是实现灵活可变的系统。要达到灵活性,通过三个途径来解决:标准化封装、复用、松耦合可编排。

SOA的技术架构图[待补充],关于SOA架构不在这里作过多的讨论。

其实,把SOA架构和分布式服务架构独立开不是特别合适。SOA架构本身就是一种分布式服务的架构,从SOA的技术组成来看:ESB承担了解决服务的分布调用问题的职责。MQ,负责分布式异步消息通信,以及任务调度的职责。

SOA中的服务组件由SCA架构进行约束,一个SAC约束了SOA中服务的实现规范。

 

基于SCA的组件组装的模型图

 

SOA的架构基础上,服务由SCA组件实现,将所有的功能都以服务的形态提供。使得前端不再感知后端实现,只要通过服务接口来约束好和这两者之间的交互就可以了。交互的时候,通过服务进行交互。这个时候,我们会发现。“子系统”之间的界限已经没有了,甚至是模糊的。界限已经被服务的界限划分代替,这个粒度变得更加的细粒度。也就意味着,具备了更多的灵活度。无论是开发过程,还是在交互过程。更细的粒度,带来了依赖的进一步降低。

理解架构的演进,可以从一个小故事来说明。就是村长到国家元首,在负责管理的层面不同。一层一层的下发任务,并且通知到相同层面去执行相同的任务。

分布式架构最重要的一个环节,就是解决应用分布带来的交互问题。

从一个RMI的例子开始

好了,前面啰嗦了那么多,还是从一个实际的例子开始吧。我们知道,分布式服务架构最依赖的就是应用分布后的交互问题。这种交互技术发展至今,已经有了很多成熟的技术,甚至有很多对应的框架。这种分布式的技术包括:RMIjava的远程调用)、Web serviceHassion等等,每种技术都有自己的优缺点。既然,我们是用的是java,那我们就从RMI开始吧。

惯例,让我们先创建一个java工程,基于maven创建。

 

Next

 

保持默认值,Next

 

选择maven-archetype-quickstart archetypeNext

 

填入相应的属性值,值我输入的比较随意,另外请忽略这个错误。因为我前面已经建好了这个maven工程了。完成后,就可以看到工程的全貌了。

其中,是用mavenarchetype已经帮我们建立好了工程的结构,并且还有一个App.javasample

 

下面来创建一个Interface,用来作为RMI对外的接口。

 

暂时就定义一个方法吧,用来查询用户的信息,返回值就直接用String类型。

package test.lottons.remote;

 

import java.rmi.Remote;

import java.rmi.RemoteException;

 

/**

 * 定义一个接口,用来作为RMI远程调用的接口定义,也是clientserver通讯的约束

 * @author lottons@126.com

 *

 */

publicinterface IUserMgr extends Remote {

    /**

     * 接口中的实现方法,通过一个标示查询用户信息

     * @param userId

     * @return

     */

    public String queryUserInfo(String userId) throws RemoteException;

}

接口必须实现java.rmi.Remote。接着,在写一个实现类。

 

package test.lottons.remote;

 

import java.rmi.RemoteException;

import java.rmi.server.UnicastRemoteObject;

 

/**

 * RMI接口的实现类

 * @author lottons@126.com

 * @since 2017-3-26

 *

 */

publicclass UserMgrImpl extends UnicastRemoteObject implements IUserMgr {

 

    /**

     *

     */

    privatestaticfinallongserialVersionUID = 1L;

 

    // UnicastRemoteObject的构造方法抛出了RemoteException异常,因此这里默认的构造方法必须写,必须声明抛出RemoteException异常

    public UserMgrImpl() throws RemoteException {

        super();

    }

 

    public String queryUserInfo(String userId) {

        // 简单的实现,直接返回userid的值

        return"UserID: " + userId;

    }

 

}

接着,在实现服务端,通过网络对外提供UserMgr的服务。

 

package test.lottons.remote;

 

import java.net.MalformedURLException;

import java.rmi.Naming;

import java.rmi.RemoteException;

import java.rmi.registry.LocateRegistry;

 

/**

 * UserMgr服务的提供者

 * @author lottons@126.com

 * @since 2017-3-26

 * 创建RMI注册表,启动RMI服务,并将远程对象注册到RMI注册表中。

 */

publicclass ServiceProvider {

 

    publicstaticvoid main(String[] args) {

        try {

            // 创建远程访问对象

            IUserMgr userMgr = new UserMgrImpl();

            // 本地主机上的远程对象注册表Registry的实例,并指定端口为1021,这一步必不可少(Java默认端口是1099

            // 必不可缺的一步,缺少注册表创建,则无法绑定对象到远程注册表上

            LocateRegistry.createRegistry(1021);

           

            // 把远程对象注册到RMI注册服务器上,并命名为userMgr

            // 绑定的URL标准格式为:rmi://host:port/name

            // rmi://127.0.0.1:1099/userMgr 是服务的访问url,可以将其分成两部分看待

            // 前面的rmi://127.0.0.1:1099/为服务的端点信息,后面的userMgr为服务的命名

            // 这个url就表明在rmi://127.0.0.1:1099/这个端点上,提供了userMgr服务的一个实例,可以被访问

            // url可以拆解为两部分 [instance_address:port]/[service_name]

            Naming.rebind("rmi://127.0.0.1:1021/userMgr", userMgr);

            System.out.println("service running...");

        } catch (RemoteException e) {

            e.printStackTrace();

        } catch (MalformedURLException e) {

            e.printStackTrace();

        }

    }

 

}

客户端的实现:

package test.lottons.remote;

 

import java.net.MalformedURLException;

import java.rmi.Naming;

import java.rmi.NotBoundException;

import java.rmi.RemoteException;

 

/**

 * UserMgr服务的消费者实现类

 * @author lottons@126.com

 *

 */

publicclass UserMgrConsumer {

 

    publicstaticvoid main(String[] args) {

        IUserMgr userMgr;

        try {

            // 由于我们已经知道了UserMgr服务的RMI的一个实例,因此这里可以直接通过指定该实例的url来对这个服务实例进行方访问

            userMgr = (IUserMgr) Naming.lookup("rmi://127.0.0.1:1099/userMgr");

            System.out.println(userMgr.queryUserInfo("76632291"));

        } catch (MalformedURLException e) {

            e.printStackTrace();

        } catch (RemoteException e) {

            e.printStackTrace();

        } catch (NotBoundException e) {

            e.printStackTrace();

        }

    }

 

}

因为我们已经知道了UserMgr服务的一个实例url,也即是说服务的发现我们是静态的。是通过一种约束机制来保证服务的实例可以被它的消费者发现并消费,这种情况就是服务的提供者开发完服务并部署后,将服务实例的访问方式告知需要消费该服务的使用者。使用者,直接通过该访问方式访问这个服务实例。

这个过程如下图所示:

 

服务的发现是通过“人”来交互通知的,这种方式在系统量级不大,服务数量不多的情况下是完全可以的。通过文档、邮件等方式,提供了服务的发现和消费。

再回来看我们的程序,现在客户端和服务端都有了,启动测试一下。

 

表明服务端已经启动,再来启动客户端,测试一下远程RMI调用的结果。

得到了返回消息:

 

至此,一个RMI远程通讯的例子就算完成了。

一个并发RMI调用的例子

我们知道,这种远程访问一定会面临多并发的情况。服务提供者提供的服务,一定不止被一个服务消费者所消费。RMI本身是具有多线程能力的,这里我们将客户端稍作改造。

第一步,我们先把调用UserMgr的远程服务的代码抽取到一个方法中(可以使用Eclipse中的重构功能)。

 

 

之后,稍微改造一下代码,实现多线程的调用。

package test.lottons.remote;

 

import java.net.MalformedURLException;

import java.rmi.Naming;

import java.rmi.NotBoundException;

import java.rmi.RemoteException;

 

/**

 * UserMgr服务的消费者实现类

 * @author lottons@126.com

 * @since 2017-3-26

 *

 */

publicclass UserMgrConsumer implements Runnable {

 

    publicstaticvoid main(String[] args) {

        UserMgrConsumer userMgrConsumer = new UserMgrConsumer();

        userMgrConsumer.queryUserInfo();

       

        // 创建一个Runnable实现类的对象

        Runnable userMgrConsumerRunnable = new UserMgrConsumer();

        // userMgrConsumerRunnable作为Thread target创建新的线程

        Thread userMgConsumerThread1 = new Thread(userMgrConsumerRunnable);

        // userMgrConsumerRunnable作为Thread target创建新的线程

        Thread userMgConsumerThread2 = new Thread(userMgrConsumerRunnable);

        // 调用start()方法使得线程进入就绪状态

        userMgConsumerThread1.start();

        // 调用start()方法使得线程进入就绪状态

        userMgConsumerThread2.start();

    }

 

    publicvoid run() {

        queryUserInfo();

    }

   

    publicvoid queryUserInfo() {

        IUserMgr userMgr;

        try {

            // 由于我们已经知道了UserMgr服务的RMI的一个实例,因此这里可以直接通过指定该实例的url来对这个服务实例进行方访问

            userMgr = (IUserMgr) Naming.lookup("rmi://127.0.0.1:1021/userMgr");

            System.out.println(userMgr.queryUserInfo("76632291"));

        } catch (MalformedURLException e) {

            e.printStackTrace();

        } catch (RemoteException e) {

            e.printStackTrace();

        } catch (NotBoundException e) {

            e.printStackTrace();

        }

    }

 

}

运行客户端,得到结果:

 

得到返回,但是看不出来线程的信息,可以引入log4j等日志工具。以下,是我引入log4j之后,通过日志输出的信息。

 

打个包,输出客户端的jar包,在命令行中运行两个UserMgr服务的消费者。

 

服务发现——服务目录服务

前面说过了,客户端想要消费、调用这个服务,在服务发现就必须知道这个服务实例的调用地址。如果事先不知道服务实例的地址,或者当我有多个服务实例的时候,作为消费者该如何去得到这些服务实例的信息呢?

之前说过,服务的发现是一种静态的实现方式。通过服务提供者和服务消费者之间的约定,实现服务的发现。这么做,感觉有点low。而且,每当我有一个消费者的时候。提供者都需要将服务的访问方式告知对方,这对服务提供者来说有点太麻烦了。因此,作为服务提供者。通过一个服务目录的服务,在发布了服务实例后,向这个服务注册一条信息。之后,服务消费者通过这个服务,查询想要访问的服务的访问信息后,实现对服务的调用。

 

我们定义一个接口,作为服务目录的服务定义。

package test.lottons.remote;

 

import java.rmi.Remote;

import java.rmi.RemoteException;

import java.util.ArrayList;

 

/**

 * 作为服务目录服务的接口定义

 * @author lottons@126.com

 * @since 2017/3/26

 *

 */

publicinterface IServiceNaming extends Remote{

    /**

     * 通过服务名查询该服务的实例信息,实例信息只包含有服务实例的url,以string数组返回

     * 如果当前该服务没有服务实例,返回的string数组的产度为0

     * @param serviceName 需要查询的服务名

     * @return返回已经注册的服务实现实例的url地址

     */

    public ArrayList<String> queryServiceInstanceByName(String serviceName) throws RemoteException;

   

    /**

     * 注册一个服务定义的服务实现实例信息,通过服务名和服务实例的url进行注册

     * @param serviceName 需要注册的服务名

     * @param url 服务实现实例的url地址

     */

    publicvoid registerServiceInstance(String serviceName, String url) throws RemoteException;

}

我们再来简单实现这个服务目录服务接口。

package test.lottons.remote;

 

import java.rmi.RemoteException;

import java.rmi.server.UnicastRemoteObject;

import java.util.ArrayList;

import java.util.HashMap;

 

public class ServiceNamingImpl extends UnicastRemoteObject implements IServiceNaming {

   

    /**

     *

     */

    private static final long serialVersionUID = 1L;

 

    protected ServiceNamingImpl() throws RemoteException {

        super();

    }

 

    HashMap<String, ArrayList<String>> serviceMap = new HashMap<String, ArrayList<String>>();

 

    public ArrayList<String> queryServiceInstanceByName(String serviceName) {

        // 通过服务名查询服务实例的url

        ArrayList<String> urlList = serviceMap.get(serviceName);

        // 如果url不为空,则返回

        if (urlList != null) {

            return urlList;

        }

       

        return new ArrayList<String>();

    }

 

    public void registerServiceInstance(String serviceName, String url) {

        ArrayList<String> urlList = serviceMap.get(serviceName);

        if (urlList != null) {

            // 这里是在判断urllist不为空的时候,直接将服务的地址插入到list

            // 实际上这里的处理有些欠妥,因为有可能同一个服务实例反复的进行注册

            // 这里没有控制,存在一个潜在的问题

            urlList.add(url);

        } else {

            urlList = new ArrayList<String>();

            urlList.add(url);

        }

        serviceMap.put(serviceName, urlList);

    }

 

}

         为了实现服务的注册,我们需要一个客户端来实现这个过程。这里,在UserMgr服务的实现类中,增加调用服务注册的过程。

package test.lottons.remote;

 

import java.net.MalformedURLException;

import java.rmi.Naming;

import java.rmi.NotBoundException;

import java.rmi.RemoteException;

import java.rmi.server.UnicastRemoteObject;

 

import org.apache.log4j.Logger;

 

/**

 * RMI接口的实现类

 * @author lottons@126.com

 * @since 2017-3-26

 *

 */

public class UserMgrImpl extends UnicastRemoteObject implements IUserMgr {

 

    /**

     *

     */

    private static final long serialVersionUID = 1L;

   

    private static final Logger LOG = Logger.getLogger(UserMgrImpl.class);

   

    // UnicastRemoteObject的构造方法抛出了RemoteException异常,因此这里默认的构造方法必须写,必须声明抛出RemoteException异常

    public UserMgrImpl() throws RemoteException {

        super();

       

        // 以下是向serviceNaming服务注册本实例的过程

        IServiceNaming serviceNaming;

        try {

            String serviceName = "UserMgrService";

            String url = "rmi://127.0.0.1:1021/userMgr";

            // 由于我们已经知道了UserMgr服务的RMI的一个实例,因此这里可以直接通过指定该实例的url来对这个服务实例进行方访问

            serviceNaming = (IServiceNaming) Naming.lookup("rmi://127.0.0.1:1021/userMgr");

            serviceNaming.registerServiceInstance(serviceName, url);

            LOG.debug("finished register service: " + serviceName + " , url is: " + url);

        } catch (MalformedURLException e) {

            e.printStackTrace();

        } catch (RemoteException e) {

            e.printStackTrace();

        } catch (NotBoundException e) {

            e.printStackTrace();

        }

    }

 

    public String queryUserInfo(String userId) {

        // 简单的实现,直接返回userid的值

        return "UserID: " + userId;

    }

 

}

         同样的,在UserMgr的消费者这里,增加通过ServiceNaming查询服务实例信息,并调用服务的过程。

package test.lottons.remote;

 

import java.net.MalformedURLException;

import java.rmi.Naming;

import java.rmi.NotBoundException;

import java.rmi.RemoteException;

import java.util.ArrayList;

 

import org.apache.log4j.Logger;

 

/**

 * UserMgr服务的消费者实现类

 * @author lottons@126.com

 * @since 2017-3-26

 *

 */

publicclass UserMgrConsumer implements Runnable {

   

    privatestaticfinal Logger LOG = Logger.getLogger(UserMgrConsumer.class);

   

    publicstaticvoid main(String[] args) {       

        // 创建一个Runnable实现类的对象

        Runnable userMgrConsumerRunnable = new UserMgrConsumer();

        // userMgrConsumerRunnable作为Thread target创建新的线程

        Thread userMgConsumerThread1 = new Thread(userMgrConsumerRunnable);

        // userMgrConsumerRunnable作为Thread target创建新的线程

        Thread userMgConsumerThread2 = new Thread(userMgrConsumerRunnable);

        // 调用start()方法使得线程进入就绪状态

        userMgConsumerThread1.start();

        // 调用start()方法使得线程进入就绪状态

        userMgConsumerThread2.start();

    }

 

    publicvoid run() {

        queryUserInfo();

    }

   

    publicvoid queryUserInfo() {

        ArrayList<String> urls = null;

        // 首先从ServicneNaming服务上查询服务的url信息

        IServiceNaming serviceNaming;

        try {

            String serviceName = "UserMgrService";

            // 通过UserMgr的服务名查询该服务的服务实例信息

            serviceNaming = (IServiceNaming) Naming.lookup("rmi://127.0.0.1:1021/serviceNaming");

            urls = serviceNaming.queryServiceInstanceByName(serviceName);

            LOG.debug("query service : " + serviceName + " urls is: " + urls);

        } catch (MalformedURLException e) {

            e.printStackTrace();

        } catch (RemoteException e) {

            e.printStackTrace();

        } catch (NotBoundException e) {

            e.printStackTrace();

        }

       

        if (urls.size() <= 0) {

            return;

        }

       

        IUserMgr userMgr;

        try {

            // 由于我们已经知道了UserMgr服务的RMI的一个实例,因此这里可以直接通过指定该实例的url来对这个服务实例进行方访问

            // 从获取到的服务实例信息的url列表中获取第一条记录,作为调用的地址,对服务进行调用

            userMgr = (IUserMgr) Naming.lookup(urls.get(0));

            LOG.debug(userMgr.queryUserInfo("76632291"));

        } catch (MalformedURLException e) {

            e.printStackTrace();

        } catch (RemoteException e) {

            e.printStackTrace();

        } catch (NotBoundException e) {

            e.printStackTrace();

        }

    }

 

}

         还需要一个ServiceNamingProvider,将ServiceNaming服务运行起来。

package test.lottons.remote;

 

import java.net.MalformedURLException;

import java.rmi.Naming;

import java.rmi.RemoteException;

import java.rmi.registry.LocateRegistry;

 

/**

 * 服务注册的服务启动器,作为底层依赖,首先需要将该服务启动起来。

 * @author lottons@126.com

 * @since 2017/3/26

 *

 */

publicclass ServiceNamingProvider {

 

    publicstaticvoid main(String[] args) {

        try {

            // 创建远程访问对象

            IServiceNaming serviceNaming = new ServiceNamingImpl();

            // ServiceNaming服务注册1021的端口号

            LocateRegistry.createRegistry(1021);

           

            Naming.rebind("rmi://127.0.0.1:1021/serviceNaming", serviceNaming);

            System.out.println("Service Naming service running...");

        } catch (RemoteException e) {

            e.printStackTrace();

        } catch (MalformedURLException e) {

            e.printStackTrace();

        }

    }

 

}

 

         至此,我们已经基本完成了通过服务目录服务来注册和查询服务的过程。现在,我们先把作为基础的服务目录服务启动起来。

 

再把UserMgr的服务启动起来。

 

通过日志,我们可以看到首先是完成了UserMgr服务的注册。

然后我们在启动UserMgr的客户端,来通过服务目录服务查询服务并调用服务。

 

2017-03-26 20:37:48,077 [Thread-1] DEBUG test.lottons.remote.UserMgrConsumer - query service : UserMgrService urls is: [rmi://127.0.0.1:1022/userMgr, rmi://127.0.0.1:1022/userMgr]

2017-03-26 20:37:48,077 [Thread-0] DEBUG test.lottons.remote.UserMgrConsumer - query service : UserMgrService urls is: [rmi://127.0.0.1:1022/userMgr, rmi://127.0.0.1:1022/userMgr]

2017-03-26 20:37:48,078 [Thread-1] DEBUG test.lottons.remote.UserMgrConsumer - Start invoke UserMgrService!

2017-03-26 20:37:48,079 [Thread-0] DEBUG test.lottons.remote.UserMgrConsumer - Start invoke UserMgrService!

2017-03-26 20:37:48,079 [Thread-1] DEBUG test.lottons.remote.UserMgrConsumer - The UserMgrService instance url is: rmi://127.0.0.1:1022/userMgr

2017-03-26 20:37:48,079 [Thread-0] DEBUG test.lottons.remote.UserMgrConsumer - The UserMgrService instance url is: rmi://127.0.0.1:1022/userMgr

2017-03-26 20:37:48,125 [Thread-1] DEBUG test.lottons.remote.UserMgrConsumer - UserID: 76632291

2017-03-26 20:37:48,125 [Thread-0] DEBUG test.lottons.remote.UserMgrConsumer - UserID: 76632291

通过分析日志,我们看到UserMgr的消费者通过服务目录服务查询到了UserMgr的实例信息,并通过实例信息调用到了UserMgr的服务。

整个过程就变成以下的过程:

 

至此,基本上我们可以实现了服务的动态注册。当一个服务实例被部署的时候,该服务实例会动态的向服务目录服务进行注册。而服务消费者则动态的通过服务目录服务,查询需要调用的服务信息,并进行服务的调用。

遗留问题

到目前为止,我们的程序已经是可以实现了服务的动态注册和发现了。

 

但是目前,提供服务注册和服务发现的类还是和“业务”上的服务混在一起的。从架构上来说,功能是耦合的。后续,需要将这两部分剥离开来。形成独立的服务容器框架,提供服务的部署和服务调用。

我们的服务目录中的实现很简单,如果存在多并发的情况会不会导致数据在多并发下的混乱?HashMap是线程非安全的,在考虑服务目录服务的时候要将这些因素考虑进去。

 

暂时想到的就这么多,下一步打算现将服务目录服务剥离出来,形成独立的服务。当然,会综合考虑可靠性、可用性等方面综合对其进行分析和设计,并选取合适的技术来实现。