分布式部署(上篇)

前言

经过前面的开发,KellerNotes 项目已经全部完成,作为个人及团队(500 人以下)的云笔记服务器使用是不成问题的。大家可以在此基础上增删功能,完成属于自己的云笔记。接下来我们进入收尾工作,讲些额外的知识,可以帮助大家了解项目的组件化开发、分布式部署。

更正说明

由于作者水平和眼界有限,在项目开发过程中用到的部分技术比较落后,感谢文涛同学的指出,在本篇的代码中已作出更新,具体如下:

  • jjwt => jjwt-impl:用于 JWT 的生成和解析
  • javax.mail => spring-boot-starter-mail:用于邮件的发送

知识准备

项目的组件化,也就是微服务,当下比较主流的是 Spring Cloud。根据本项目实际情况,使用其中的服务治理组件 Spring Cloud Consul。消息队列使用 RabbitMQ,使用 spring-boot-starter-amqp 实现对 RabbitMQ 的支持。

Consul

Consul 是一个服务网格解决方案,特点是:

  • 动态负载均衡
  • 健康检查
  • 自动化网络配置
  • 强健的生态系统

官网地址:https://www.consul.io/

在这里插入图片描述

Consul 提供了不同操作系统的下载方式,使用 Windows 系统直接下载 exe 文件,双击安装即可:

在这里插入图片描述

Consul 默认启动端口为 8500,安装完毕后,直接在浏览器访问 http://localhost:8500 即可。如图:

在这里插入图片描述

现在还没有配置其他服务,当我们配置一个服务并成功启动后,在这里就能看到服务的运行状态了。

RabbitMQ

RabbitMQ 是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。

官网地址:https://www.rabbitmq.com/

在这里插入图片描述

启动成功后,访问 http://localhost:15672 即可:

在这里插入图片描述

微服务

整体架构

Keller Notes 项目组件化后的结构如下:

在这里插入图片描述

将其按照功能模块细分为 4 个服务——Proxy、User、Note、Log,每个服务负责独立的功能。这些服务均注册在 Conful 中,按需部署不同的数量,通过 Spring Cloud Conful 进行调用。所有服务中日志记录相关的部分都发送到 RabbitMQ 中,由 Log 组件进行处理。

组件说明

​组件这样拆分的好处在于:

  • Proxy
    • 代理服务,客户端只知道代理服务器的存在,不知道其他业务服务器,访问其他业务服务器的请求均有代理服务器进行验证和转发
    • 其他业务服务器只接受来自代理服务器的请求,在网关上做限制,保障了业务服务器的安全性
    • 代理服务器是一个与业务无关的通用服务,可以在其他项目中直接使用
    • 因为现在只有上传头像与用户笔记中的图片,这些文件数量比较少,暂时不考虑单独搭建 FastDFS 等文件服务来专门处理文件读写操作,暂时由代理服务配合 Nginx 文件服务器处理。
  • User
    • 用户信息服务,与具体业务无关,只负责维护用户的基本信息与名片信息
    • 在其他项目中可以直接使用 User 服务实现用户信息管理功能
    • 当然,如果需要做用户权限管理,在此基础上进行修改、扩展就可以了
  • Note
    • 笔记服务,本项目中的业务服务器,负责笔记相关的逻辑处理
    • 纯业务服务器,在不同的项目中有不同的业务处理,可以根据业务的实际情况拆分不同的业务服务器
    • 本项目中业务逻辑比较简单,使用一个业务服务器就可以了,不再做拆分
  • Log
    • 日志服务,负责各种日志的读写操作
    • 各服务需要记录日志的地方,统一发送到 RabbitMQ,由 Log 服务统一处理
    • 这样能保证日志记录有问题时不影响到业务主流程
    • 需要查询日志时,也调用 Log 服务的 RestFul 接口获取查询结果
    • 当业务增加到一定程度时,可以将日志记录单独分一个库

具体的每个组件功能如下:

  • Proxy:代理服务
    • 路由代理
    • 用户身份验证
    • RabbitMq 消息队列维护
    • 文件上传
  • User:用户服务
    • 登录、注册
    • JWT 签发
    • 用户信息维护
    • 用户信息统计
  • Note:笔记服务
    • 笔记信息维护
    • 笔记本信息维护
  • Log:日志服务
    • 日志记录
    • 日志查询

公共组件

组件拆分后,需要将项目中的工具包 common 打成一个独立的 jar 包,以供各个组件使用。

创建 common 项目

创建新的 Spring Boot 项目,选择如下依赖:

在这里插入图片描述

创建完毕后,添加项目中用到的其他依赖,如:fastjson、thumbnailator 等,完整的项目依赖如下:

                <!-- Spring Boot -->
                <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
                <!-- MyBatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>

                <!-- fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.51</version>
        </dependency>

        <!-- 图片压缩 -->
        <dependency>
            <groupId>net.coobird</groupId>
            <artifactId>thumbnailator</artifactId>
            <version>0.4.8</version>
        </dependency>

        <!-- 图片元数据读取 -->
        <dependency>
            <groupId>com.drewnoakes</groupId>
            <artifactId>metadata-extractor</artifactId>
            <version>2.11.0</version>
        </dependency>

        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.1</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.1</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.1</version>
            <scope>runtime</scope>
        </dependency>

        <!-- RabbitMQ -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
                <!-- MySql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--Spring Cloud Consul-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
            <version>2.1.1.RELEASE</version>
        </dependency>
        <!--Spring Cloud Consul 健康检查依赖包-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>
common 代码结构

将所有组件公用的代码整理出来,基本上是原项目中 common 包下的代码。部分代码做了改动,以适应微服务架构需要;部分代码重新进行整理,剥离出其中与业务相关的部分。代码结构如下:

  • config:配置文件相关
    • Constants.java:项目常量,存放项目中定义的一些常量
    • MqConstants.java:RabbitMq 队列常量,存放固定的队列名
  • entity:公用的实体类相关
    • EmailLog.java:邮件发送记录实体类
    • ErrorLog.java:错误日志实体类
    • LoginLog.java:用户登录日志实体类
    • RegisterLog.java:用户注册记录实体类
  • http:http 相关
    • HttpsClientRequestFactory.java:Https 请求工厂,用于支持 Https 请求
    • RequestUtil.java:请求工具类,用于处理 HttpServletRequest 中的各种信息
    • Response.java:统一应答格式
    • ResponseUtils.java:应答工具类,用于解析应答、捕获 RestTemplate 处理过程中的的异常
    • RestTemplateUtils.java:RestTemplate 工具类,生成配置好的 HttpHttps 格式的 RestTemplate 实例
    • ResultData.java:应答中的统一数据格式
    • ServerRouter.java:服务路由,提供各个服务的名称及获取各服务地址的方法
  • mybatis:MyBatis 通用 Mapper 相关
    • annotation:注解相关
      • FieldAttribute.java:字段注解,标示成员变量为数据库字段,并设置相应的约束信息,如:长度、非空、查询字段、索引字段等
      • KeyAttribute.java:主键注解,标示成员变量为主键,支持标注为自增主键
      • SortAttribute.java:排序注解,标示成员变量为排序字段
      • TableAttribute.java:数据表注解,标示实体类对应的表名称、说明信息
    • provider:Sql 语句生成相关
      • BaseCreateProvider.java:数据表创建语句的生成器,支持生成索引
      • BaseDeleteProvider.java:delete 语句生成器,支持根据 ID、主键、自定义条件删除
      • BaseInsertProvider.java:insert 语句生成器,支持自增主键的 insert 操作
      • BaseSelectProvider.java:select 语句生成器,支持分页查询、统计查询、自定义条件查询等
      • BaseUpdateProvider.java:update 语句生成器,支持根据 ID、主键修改数据
    • BaseEntity.java:所有实体类的父类,提供了自定义查询条件、分页查询、排序的扩展
    • BaseException.java:自定义异常,在通用 Mapper 运行过程中抛出的异常
    • BaseMapper.java:所有 Mapper 的父类,提供了通用 Mapper 功能
    • SqlFieldReader.java:Sql 字段解析类,用于解析实体类中的自定义注解,为生成 Sql 语句服务
    • TypeCaster.java:类型转换,用于将 Java 中的数据类型转换成相应的 MySql 数据类型
  • util:工具类相关
    • Console.java:日志输出工具类,用于在控制台、日志文件中输出相关信息
    • DateUtils.java:日期工具类,用于各种日期字符串的处理
    • ErrorLogUtils.java:错误日志工具类,用于处理请求异常、系统异常等异常日志
    • FileUtils.java:文件工具类,用于处理文件名、文件类型、保存文件等
    • ImageUtils.java:图片工具类,用于处理图片信息,如:图片压缩、图片保存等
    • JwtUtils.java:JWT 工具类,用于 JWT 的生成与解析
    • Md5Utils.java:MD5 工具类,用于处理 MD5 字符串
    • MetadataUtils.java:元数据工具类,用于解析图片中的元数据信息
    • ObjectUtils.java:Object 工具类,用于对象的空值判断
    • StringUtils.java:字符串工具类,用于生成指定格式的字符串及对字符串进行各种处理
RequestUtils 改动说明

主要涉及到修改的类是 RequestUtils.java,将原有的 getUrl(HttpServletRequest request,Map<String,Object> params) 方法扩展为三个方法:

  • getUrl:获取到普通用户访问的 Url
  • 从 Header 中读取 JWT,验证用户身份
  • 从 Header 中读取要访问的接口名
  • 从传入的请求参数中读取参数名,构造完整的请求
  • 过滤掉非法的请求参数,添加固定的 kellerUserId 参数作为用户 ID
  • getBaseUrl:获取到不需要登录就可以访问的 Url
  • 规定每个组件中 RequestMapping 为 /base 的接口为不需要登录就能访问的接口
  • 原则上只有用户登录、注册、忘记密码接口等允许不登录就可以访问
  • 从 Header 中读取要访问的接口名,将其缀在 /base 后面,限定其只能访问 /base 开头的接口
  • 从传入的请求参数中读取参数名,构造完整的请求
  • 不对请求的接口地址和参数做特殊处理
  • getAdminUrl:获取到管理员才能访问的接口的 Url
  • 规定每个组件中 RequestMapping 为 /admin 的接口为管理员才能访问的接口
  • 从 Header 中读取 JWT,验证管理员身份
  • 从 Header 中读取要访问的接口名,将其缀在 /admin 后面,限定其只能访问 /admin 开头的接口
  • 从传入的请求参数中读取参数名,构造完整的请求
  • 过滤掉非法的请求参数,添加固定的 kellerAdminId 参数作为管理员 ID

getBaseUrl(HttpServletRequest request, Map<String,Object> params)

    /**
     * 访问无需身份认证的接口(/base)
     * @param request
     * @param params
     * @return
     */
    public static String getBaseUrl(HttpServletRequest request, Map<String,Object> params){
        HashMap<String,String> headers = getHeader(request);
        StringBuilder builder = new StringBuilder();
                // 添加固定接口名前缀 /base
        builder.append(Constants.baseMapping).append("/")
                .append(headers.get(METHOD));
        if(params == null){
            return builder.toString();
        }
        builder.append("?");
        for(String key :params.keySet()){
            builder.append(key)
                    .append("={")
                    .append(key)
                    .append("}&");
        }
        String url = builder.substring(0,builder.length() -1);
        Console.info("getBaseUrl",url);
        return url;
    }

getUrl(HttpServletRequest request, Map<String,Object> params)getAdminUrl(HttpServletRequest request, Map<String,Object> params)

        /**
     * 访问普通接口
     * @param request
     * @param params
     * @return
     */
        public static String getUrl(HttpServletRequest request,Map<String,Object> params){
        return getUrl(request, params,false);
    }
        /**
     * 访问管理员接口(/admin)
     * @param request
     * @param params
     * @return
     */
    public static String getAdminUrl(HttpServletRequest request,Map<String,Object> params){
        return getUrl(request, params,true);
    }

    private static String getUrl(HttpServletRequest request,Map<String,Object> params,boolean isAdmin){
        //从 JWT 中解析出 UserId
        HashMap<String,String> jwtMap= JwtUtils.readJwt(request.getHeader(TOKEN));

        // 从 JWT 中取出用户类型
        Integer userType = Integer.parseInt(jwtMap.get(JwtUtils.USER_TYPE));
        // 从 JWT 中取出用户ID
        Integer userId = Integer.parseInt(jwtMap.get(JwtUtils.ID));
        // 添加固定的用户 ID,避免客户端通过伪造 ID 参数对接口进行非法访问
        String userIdParamName;
        String mapping = "";
        // 用户类型和用户Id均不能为空
        if(ObjectUtils.isEmpty(userId,userType)){
            return null;
        }
        // 非管理员用户不能访问管理员接口
        if(isAdmin){
            if(userType != Constants.ADMIN_USER_TYPE){
                return null;
            }
            userIdParamName = Constants.ADMIN_ID_KEY;
            mapping = Constants.adminMapping;
        }else {
            userIdParamName = Constants.USER_ID_KEY;
        }
        StringBuilder builder = new StringBuilder();
        HashMap<String,String> headers = getHeader(request);

        builder.append(mapping)
                .append("/")
                .append(headers.get(METHOD))
                .append("?")
                .append(userIdParamName)
                .append("=")
                .append(userId);
        if(params == null){
            return builder.toString();
        }else{
            builder.append("&");
        }
        for(String key :params.keySet()){
            //过滤掉请求中的 userId
            if(Constants.USER_ID_KEY.equals(key)
                    || Constants.ADMIN_ID_KEY.equals(key)){
                continue;
            }
            builder.append(key)
                    .append("={")
                    .append(key)
                    .append("}&");
        }
        String url = builder.substring(0,builder.length() -1);
        Console.info("getUrl",url,isAdmin);
        return url;
    }
组件打包

在 pom.xml 配置文件中指定打包的文件名和保存路径,只打出项目代码部分,不要依赖包,配置如下:

<build>
    <finalName>kellerCommon</finalName>
    <directory>
        /Users/yangkaile/jars/kellerCommon/
    </directory>
</build>

然后使用 mvn package 命令进行打包即可,打包成功后,可以在指定的目录下看到生成的 kellerCommon.jar。

添加到本地 Maven 仓库

可以将 kellerCommon.jar 加入本地 Maven 仓库,供其他组件引用,使用如下命令:

mvn install:install-file -Dfile=/Users/yangkaile/jars/kellerCommon/kellerCommon.jar -DgroupId=com.keller -DartifactId=common -Dversion=0.0.1 -Dpackaging=jar
  • Dfile:文件路径
  • DgroupId:groupId
  • DartifactId:artifactId
  • Dversion:version
  • Dpackaging:打包格式

执行效果如下:

在这里插入图片描述

这样就可以在组件中使用 Maven 方式引入 kellerCommon.jar 了,如:

            <dependency>
            <groupId>com.keller</groupId>
            <artifactId>common</artifactId>
            <version>0.0.1</version>
        </dependency>

源码地址

本篇完整的源码地址:

https://github.com/tianlanlandelan/KellerNotes

注意:后面章节的代码都在 30 篇中包含,直接在 IDEA 中打开 server 文件夹即可。

小结

本篇按照微服务应用的开发方式,将项目公共代码提取出来,并打成 jar 包,添加到本地 Maven,供各个微服务组件使用。并部署好服务治理组件 Consul,用于服务发现和治理、健康检查;部署好消息队列 RabbitMQ,用于组件间的异步通讯。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值