一个阅读APP微服务项目(二)

/ 项目实战 / 257浏览

指南

工程模块主要划分为2个类型:基础服务业务服务

其中配置中心、注册中心、服务网关为基础服务,图书中心、账户中心、精品页中心为业务服务,这里会侧重说明业务部分。

公共模块 - reading-cloud-common

主要存放Pojo、Constant、工具类等公共资源,作为独立的Jar包供其他工程依赖使用。

相当于单体项目里的 common 包独立出来,实现同等的价值,这样不需要每个微服务项目冗余公共代码资源,需要注意只存放公共代码,从而得到更好的抽离和复用。

配置中心/注册中心 - Alibaba-Nacos

配置中心

从上面的架构图中我们可以得知,几乎所有的工程都要从配置中心获取配置信息。其目的是用来统一管理配置,配置中心可以在微服务等场景下极大地减轻配置管理的工作量,增强配置管理的服务能力。

单体项目的时候,我们把配置信息放到 .yml.properties 文件中,随着项目走的,一个项目可能有几个配置文件。当请求量随着增大,项目可能要部署多个节点了,这时候维护起来会越来越麻烦,也容易出错。发布的工作降低了整体的工作效率,为了能够提升工作效率,配置中心应运而生了,我们可以将配置统一存放在配置中心来进行管理。

目前主流的配置中心有 Apollo、SpringCloud-Config、Nacos 等开源产品,每款配置中心都能满足统一管理配置的需求,本项目的1.0版本中使用 SpringCloud-Config 作为配置中心、Eureka为注册中心,2.0使用了 Nacos,因为它除了可以做配置中心,还可以做服务注册发现,替代了 Eureka 和 SpringCloud-Config 两个产品。

注册中心

注册中心,是一个独立的服务组件,核心功能是服务治理,集中存储、监控、我们的服务信息。

工作过程简单来说,首先服务提供者启动时,向注册中心提供自己的服务信息,然后消费者服务要请求某个接口时,不是直接去请求具体的服务地址,而是在注册中心拉取得到要请求的服务地址,最后再通过这个地址、端口信息远程调用服务。大体过程如下图:

当然服务注册与服务发现的过程并不仅仅只有注册和拉取这两个动作,还有一些其他相关的动作。如注册中心存储数据的缓存更新、提供者服务故障处理、消费者心跳检测等等。

服务网关 - reading-cloud-gateway

API 网关是对外提供服务的一个入口,并且隐藏了内部架构的实现,是微服务架构中必不可少的一个组件。API 网关可以为我们管理大量的 API 接口,负责对接协议适配、安全认证、路由转发、流量限制、日志监控、防止爬虫、等功能。

主流的开源网关有比较早的 Zuul 以及 SpringCloud 自己研发了一个全新的网关 Spring Cloud Gateway。由于 Zuul1 基于 Servlet 构建,使用的是阻塞的 IO,性能并不是很理想。Spring Cloud Gateway 则基于 Spring 5、Spring boot 2 和 Reactor 构建,使用 Netty 作为运行时环境,比较完美的支持异步非阻塞编程。

没使用网关的情况

使用网关后

我想,没有网关和使用网关的区别,看见客户端的表情你就明白了其中的奥义了吧(无论服务端多么复杂...)。

项目采用 SpringCloud Gateway 作为网关实现,主要实现了统一认证、动态路由。

SpringCloud Gateway 两大核心,一个是Predicate,路由匹配,一个是Filter,过滤器。

路由匹配的配置方式有 Fluent API 和 yml 两种方式,这里采用 yml 方式。具体见 reading-cloud-gateway 工程里的配置文件。

SpringCloud Gateway 有全局过滤器和局部过滤器之分,对应的接口为 GatewayFilter 和 GlobalFilter。我们统一认证的实现方式是自定义实现全局过滤器,在过滤器里面可以处理白名单放行、认证校验、动态处理请求参数等。位置:cn.zealon.readingcloud.gateway.filter.AuthFilter

认证校验过程参考 账户中心 - reading-cloud-account 的说明文档,在最下边。

其白名单配置在Nacos中,可通过动态配置进行更新。

图书中心 - reading-cloud-book

图书中心作为基础数据提供图书信息服务,另外就是提供图书详情接口、章节目录、章节阅读等接口了。

数据表结构

PS:只列举了关键表和关键字段

  1. 图书表(book)

  2. 章节表(book_chapter)一对多关系。

接口服务

可以看到如下的几个接口,接口描述使用 swagger 实现。

其中图书查询接口比较简单,看代码很轻易的就能明白,这里重点说明一下章节阅读接口 book/chapter/readChapter

首先分析一下阅读操作的特征:

分析得出,阅读章节的数据结构几乎就是一个双向链表,所以接口可以采用这种模式来存储一本书的阅读数据。

Q:为什么非要使用链表存储呢?阅读当前章节的时候同时查询上一章和下一章不是也可以吗?

A:没错啊,利用当前章节计算上一章和下一章是可行的,但是这种方式每访问一章都需要进行上下章查询与计算,而通过链表这种方式,只需要第一次生成一次链表,后面每次在链表中读取即可,相比每次计算和一次计算,当然要选择后者啦,而且随着章节越多耗费的性能差距也就越大。

按着这个思路,接下来就是要设计具体数据了。我们看一下下边的数据结构,key 代表当前章节ID,value代表上下章关系数据,都有一个 pre 和 next 指向前驱章节和后继章节,这样当请求任意章节时,通过传入的章节ID就直接获得了前后章节信息了。

[
    {
        "key":"519",
        "value":{
            "id":529,
            "name":"第一章 装B的乞丐",
            "pre":null,
            "next":[
                {
                    "id":530,
                    "name":"第二章 资格"
                }
            ]
        }
    },
    {
        "key":"530",
        "value":{
            "id":530,
            "name":"第二章 资格",
            "pre":[
                {
                    "id":529,
                    "name":"第一章 装B的乞丐"
                }
            ],
            "next":[
                {
                    "id":531,
                    "name":"第三章 开始修炼清心诀"
                }
            ]
        }
    },
    {
        "key":"530",
        "value":{
            "id":531,
            "name":"第三章 开始修炼清心诀",
            "pre":[
                {
                    "id":530,
                    "name":"第二章 资格"
                }
            ],
            "next":[
                {
                    "id":532,
                    "name":"第四章 暴打恶霸"
                }
            ]
        }
    }
]

设计好了数据结构,想想Redis哪些类型能存储我们的章节数据呢,String自然是不行的了,Hash 貌似可行哎,可以通过 K / V 的形式存储,key即我们的章节ID,value即我们的链表内容,获取的时候只需要提供 key 即可,时间复杂度 O(1),不错的赶脚吧。好了,那就实现它吧~

数据结构类是 BookPreviousAndNextChapterNode ,实现函数是 BookChapterServiceImpl.getChapterNodeData,那么有了章节基础数据了,剩下的就是完成整个接口的设计了。接口响应的结果数据除了前后章信息之外,就是当前章节内容了,而前后章的内容万万不能返回的,浪费资源啊。

大致流程图(蓝色环节只在没有缓存时执行一次):

其中,没有缓存时,会查询一次数据库,计算整个链表存到缓存,后面再请求时,直接redis的hash返回,不需要再计算前后章节了。


上一节:一个阅读APP微服务项目(一)

更多参考github介绍:一个阅读APP微服务项目