追梦App系列博客——后端架构篇

简介: 追梦App系列博客——后端架构篇

前言

聊完追梦App的设计思路后我们来聊聊后端架构。


一、架构选择

此时我们面临几种架构选择。


单体架构


单体架构是最简单的软件架构,常用于传统的应用软件开发以及传统Web应用。传统Web应用,一般是将所有功能模块都打包(jar,war)在一个Web容器(JBoss、Tomcat)中部署、运行。随着业务复杂度增加、技术团队规模扩大。在一个单体应用中维护代码,会降低开发效率。即使是处理一个小需求,也需要将所有机器上的应用全部部署一遍,增加了运维的复杂度。


服务化架构(SOA架构)


当某一天使用单体架构发现很难推进需求的开发、以及日积月累的技术债,很多企业会开始做单体服务的拆分。拆分的方式一般有水平拆分以及垂直拆分。垂直拆分把一个应用拆成松耦合的多个独立的应用,让应用可以独立部署,有独立的团队进行维护。水平拆分把一些通用的,会被很多上层服务调用的模块独立拆分出去,形成一个共享的基础服务,这样拆分可以对一些性能瓶颈的应用进行单独的优化和运维管理,也一定程度防止了垂直拆分的重复造轮子。

SOA也叫面向服务的架构,从单体服务到SOA的演进,需要结合水平拆分以及垂直拆分。SOA强调用统一的协议进行服务间的通信,服务间运行在彼此独立的硬件平台但是需通过统一的协议接口相互协作,也即将应用系统服务化。举个易懂的例子,单体服务如果相当于一个快餐店,所有的服务员都是一样的,又要负责收银结算,又要负责做汉堡,又要负责端盘子,又要负责打扫,服务员之间不需要有交流,一个用户来了一个服务员从前到后负责到底。SOA相当于让服务员有职责分工,收银员负责收银,厨师负责做汉堡,保洁阿姨负责打扫等等。所有服务员需要同一种语言交流,方便工作协调。


微服务架构


微服务可以在“自己的程序”中运行,并通过“轻量级设备与HTTP型API进行沟通”。关键在于该服务可以在自己的程序中运行。通过这一点我们就可以将服务公开与微服务架构(在现有系统中分布一个API)区分开来。在服务公开中,许多服务都可以被内部独立进程所限制。如果其中任何一个服务需要增加某种功能,那么就必须缩小进程范围。在微服务架构中,只需要在特定的某种服务中增加所需功能,而不影响整体进程的架构。


首先,我们要明确的一点是——脱离实际业务谈架构的都是耍流氓!


我们先回顾下设计思路,我们要完成哪些功能。


追梦体系、个人账户成长系统、任务系统、虚拟货币系统、社交系统、学习系统、学习工具…


可以看到我们如果要把这款软件做好的话,实际上体量还是有点大的,而且业务对服务器要求比较高,这样一看,分布式微服务确实是个不错的选择。


诚然,分布式微服务架构确实是个不错的选择,但我们也该考虑自己的实际情况:

1.这款App首先是去参加比赛的,所以必须快速出产品,最起码要把基本功能做完。

2.分布式微服务的成本太高,我们大学生暂时无力承担服务器费用。

3.初期用户量小,分布式微服务并不合适。

4.小组成员技术参差不齐,学过分布式微服务的就只有我一个。


综上,分布式微服务对于初期的开发来说并不合适,单体架构对于初期的我们来说会更合适些。但我们就不用分布式微服务去架构吗?


不,初期确实不适合用分布式微服务去开发,但是等到我们做出点成绩后,等到我们App用户量增大的时候,我们再去拆分服务,用分布式微服务重构项目。


这确实会花费些时间,但只要我们前期有拆分服务的思想,有意的去区分服务,这样后期拆分起来也会容易很多。


二、技术选型

前端:Flutter

后端框架:SpringBoot、SSM

数据库:mysql

数据库连接池:Druid

服务器:Tomcat

JDK:8

开发工具:IDEA


三、项目结构

之前架构选择中说到我们选择单体架构,但是又要有服务化的趋势,这样才能让服务拆分变得简单容易。


那么怎么设计呢?


初步想法是分业务大致拆分出来,有人可能马上想到用多模块,因为多模块结构清晰,可以很好的区分业务。


之前我也曾想过单体架构多模块项目的结构,但我感觉其实没有必要,既然不是分布式微服务架构,就没必要分模块,因为这要会增加模块间交互的成本,得不偿失。


多包完全可以达到之前想要的效果。


所以我计划如下图这样来构建项目:



q1.png

通过业务来区分包名:

common:存放实体类以及一些公共的接口文件

blog:发布博客、动态的业务

dream:追梦体系

social:社交业务

study:学习系统业务

user:个人管理中心,用来管理账户信息

(开发可能不完全按照这个,因为我总感觉这样弄怪怪的)


四、数据库表设计

此部分日后再补,需要和组员讨论一下。


五、前后端交互规范

以下摘自前后端接口规范


具体需要定义哪些接口, 可以按照下面的思路来整理


资源接口: 系统涉及到哪些资源, 按照 RESTful 方式定义的细粒度接口

操作接口: 页面涉及到哪些操作, 例如修改购物车中商品的数量, 更换优惠券等等, 也可以使用 RESTful 方式来定义

页面接口: 页面涉及到太多接口, 如果是一个个地调用, 会需要很多次请求, 有可以影响到前端的性能和用户感知(特别是首屏的体验), 因此可能需要将这些接口的数据合并到一起, 作成一个聚合型接口提供给前端来使用

接口协商要点

接口必须返回统一的数据结构, 参考后端接口通用规范中接口返回的数据结构

接口查询不到数据时, 即空数据的情况下返回给前端怎样的数据 * 建议返回非 null 的对应数据类型初始值, 例如对象类型的返回空对象({}), 数组类型的返回空数组([]),

其他原始数据类型(string/number/boolean…)也使用对应的默认值 *

这样可以减少前端很多琐碎的非空判断, 直接使用接口中的数据 * 例如: result.fieldName * 如果

result 为 null, 可想而知会报错 Uncaught TypeError: Cannot read property 'fieldName' of null

调用接口业务失败的常用错误码, 例如未授权时调用需要授权的接口返回 "status": 1

接口需要登录时如何处理, 特别是同时涉及到 Web 端/微信端/App 端, 需要前端针对运行环境判断如何跳转到登录页面

返回数据中图片 URL 是完整的还是部分的 * http://a.res.com/path/to/img.png 这就是完整的, 前端直接使用这个 URL * /path/to/img.png 这就是部分的, 一般省略域名部分, 前端需要自己拼接后才能使用

'http://a.res.com' + '/path/to/img.png'

返回数据中页面跳转的 URL 是给完整的还是部分的 * 内部页面返回部分的, 或者只给ID, 由前端自己拼接, 例如只给出商品ID, 让前端自己拼接商品详情页的 URL * 外部页面返回完整的, 例如广告位要跳转去谷歌

返回数据中日期的格式, 是使用时间戳还是格式化好的文字 * 对于需要前端再次处理的日期值(例如根据日期计算倒计时), 可以使用时间戳(简单暴力), 例如: 1458885313711, 或者参考

Date.prototype.toJSON

提供 ISO 标准格式(例如需要考虑时区时) * 对于纯展示用的日期值, 推荐返回为格式化好的文字, 例如: 2017年1月1日

对于大数字(例如 Java 的 long 类型), 返回给前端时需要设置为字符串类型, 否则 JavaScript 会发生溢出, 造成得到的数值错误 * 例如: 返回 JSON 数据 {"id": 362909601374617692} 前端拿到的值却是:

362909601374617660

分页参数和分页信息 * 如何限制只返回 N 条数据(limit 参数) * 如何控制每页的数据条数(pageSize 参数) * 如何加载某一页的数据(page 参数)

第一页是从 0 开始还是从 1 开始 * 如何避免无限滚动加载可能出现的重复数据(采用 lastId 分页方式, 来避免传统分页方式的弊端)

假设数据是按照新增时间倒序排列的

首先加载 2 页的数据

等了很久

期间新增了很多数据

再获取第 3 页数据

此时就可能出现重复数据的情况, 因为新增的数据都排在最前面, 后面会接着已经加载过数据 * 分页信息包含什么(total, page, pageSize) * 分页信息何时表明已经是最后一页了

请求某页数据时返回的数据条数 < pageSize

请求某页数据时返回的数据条数 = 0

如果碰巧最后一页有 pageSize 条数据, 前端无法通过数据条数来判断已经处于最后一页了

接口定义

所有的接口定义在项目前端静态文件目录的 _mockserver.json 文件中, 启动 puer-mock 服务,

即可使用这些接口获得符合规范的假数据, 也可以查看接口文档.


具体 puer-mock 的详细使用手册和 _mockserver.json 如何配置接口请参考 puer-mock

项目, 或者参考项目中已经配置好的其他接口.


接口协作

由于接口规范的定义和接口的实际实现是分开的两个部分, 而且涉及到多人协作, 因此在开发过程中可能出现接口规范与实现不同步,

最终造成实际的接口不符合规范的定义, 接口规范就会慢慢失去存在的意义.


为了尽量避免这种问题, 后端在实现接口的过程中应该确保与接口规范保持一致, 一旦出现分歧, 必须同步修改接口规范, 尽可能保持沟通。


后端接口通用规范

接口地址和请求方式

接口根路径 - Root

Endpoint 推荐为:

http://api.yourdomain.com 或者 http://yourdomain.com/api


接口地址即接口的 URL, 定义时使用相对路径(即不用带上域名信息), 建议分模块来定义, 推荐 REST 风格, 例如


GET /user/:id 表示获取用户信息

POST /user 表示新增用户

接口参数

向接口传递参数时, 如果是少量参数可以作为

URL query string 追加到接口的 URL 中, 或者作为 Content-Type: application/x-www-form-urlencoded 放在请求体(body)中(即表单提交的方式)


对于复杂的接口参数(例如嵌套了多层的数据结构), 推荐在 HTTP 请求体(body)中包含一个 JSON 字符串作为接口的参数,

并设置 Content-Type: application/json; charset=utf-8.


例如


查询 VIP 用户的接口
charset=utf-8
{
    "name": "hanmeimei",
    "isVip": true } ```
### 接口返回的数据结构
返回的响应体类型推荐为 `Content-Type: application/json; charset=utf-8`, 返回的数据包含在
HTTP 响应体中, 是一个 JSON Object. 该 Object 可能包含 3 个字段 `data`, `status`,
`statusInfo`
```HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8
{
    "data": {},
    "status": 0,
    "statusInfo": {
        "message": "给用户的提示信息",
        "detail": "用于排查错误的详细错误信息"
    } } ```
**字段名** |**字段说明** :----------|:----------- data       | **业务数据**<br>必须是任意 JSON 数据类型(number/string/boolean/object/array).<br>推荐始终返回一个 object
(即再包一层)以便于扩展字段.<br>例如: 用户数据应该返回 `{"user":{"name":"test"}}`, 而不是直接为
`{"name":"test"}` status     | **状态码**<br>必须是 `>= 0` 的 JSON Number
整数.<ul><li>`0` 表示请求处理成功, 此时可以省略 `status` 字段, 省略时和为 `0`
时表示同一含义.</li><li>`非 0`
表示发生错误时的[错误码](http://open.weibo.com/wiki/Error_code "错误码格式可以参考微博API的
Error code"), 此时可以省略 `data` 字段, 并视情况输出 `statusInfo` 字段作为补充信息</li></ul>
statusInfo | **状态信息**<br>必须是任意 JSON 数据类型.<br>推荐始终返回一个 object 包含
`message` 和 `detail` 字段<ul><li>`message` 字段作为接口处理失败时,
**给予用户的友好的提示信息**, 即所有给用户的提示信息都统一由后端来处理.</li><li>`detail` 字段用来放置接口处理失败时的详细错误信息. 只是为了方便排查错误, 前端无需使用.</li></ul>
例如
* 接口处理成功时接口返回的数据
  ```{
      "data": "api result"
      "status": 0   }   ```
* 接口处理失败时接口返回的数据
  ```{
      "status": 1,
      "statusInfo": {
          "message": "服务器正忙",
          "detail": {
              "exception": "java.util.List"
          }
      }   }   ```
这样我们就可以非常容易地通过判断 status 来处理数据了 ```javascript if (!response.status) {
    // status 为 0 或者没有 status 字段时表示接口成功返回了数据
    console.log(response.data); } else {
    // 失败
    console.error(response.status, response.statusInfo);
    // 统一由服务端返回给用户的提示信息
    alert(response.statusInfo.message); } ```
## 错误码规范: `status` 字段该如何取值
采用前后端分离开发模式的项目越来越多, 前端负责调用后端的接口来展现界面, 如果有界面显示异常,
需要有快速方便的手段来排查线上错误和定位出职责范围
综合了经验总结和行业实践, 最简单有效的手段是制定出一套统一的错误码规范, 协助多方人员来排查出接口的错误
例如
* 用户发现错误, 可以截错误码的图, 就能够提供有效的信息帮助开发人员排查错误
* 测试人员发现错误, 可以通过错误码, 快速定位是前端的问题还是后端接口的问题
因此我们确定提示信息规范为: 当后端接口调用出错时, 接口提供一个用户可以理解的错误提示, 前端展示给用户错误提示和错误码, 给予用户反馈
对于错误码的规范, 参考行业实践, 大致有两种方案
* 做显性的类型区分, 快速定位错误的类别, 例如通过字母划分类型: `A101`, `B131`   * [Standard ISO Response
Codes](http://www.nexion.co.za/docs/merchant-access/user-manual/17.%20Standard%20ISO%20Response%20codes.pdf)
* 固定位数, 设定区间(例如手机号码, 身份证号码)来划分不同的错误类型   * [HTTP Status Code Definitions](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
"Most API services follow the HTTP error code system RFC2616 with
ranges of error codes for different types of error")   * [System Error
Codes](https://docs.microsoft.com/en-us/windows/desktop/Debug/system-error-codes)
具体实践如下
* **错误码固定长度**, 以区间来划分错误类型(例如 HTTP 的状态码)
  例如: 10404 表示 HTTP 请求 404 错误, 20000 表示 API 调用失败, 30000 代表业务错误, 31000
表示业务A错误, 32000 表示业务B错误
* **错误码可不固定长度**, 以首字母来划分错误类型, 可扩展性更好, 但实际运作还是需要划分区间
  例如: H404 表示 HTTP 请求 404 错误, A100 表示 API 调用失败, B100 表示业务A错误, B200
表示业务B错误
关于错误分类的原则, 我们可以根据发送请求的最终状态来划分
- 发送失败(即请求根本就没有发送出去)
- 发送成功
  - HTTP 异常状态(例如 404/500...)
  - HTTP 正常状态(例如 200)
    - 接口调用成功
    - 接口调用失败(业务错误, 即接口规范中 status 非 0 的情况)
### 最终规范
错误码可不固定长度, 整体格式为: `字母+数字`, `字母`作为错误类型, 可扩展性更好, `数字`建议划分区间来细分错误
例如:
- `A` for **API**: API 调用失败(请求发送失败)的错误, 例如 `A100` 表示 URL 非法
- `H` for **HTTP**, HTTP 异常状态的错误, 例如 `H404` 表示 HTTP 请求404错误
- `B` for **backend or business**, 接口调用失败的错误, 例如 `B100` 业务A错误, `B200` 业务B错误
- `C` for **Client**: 客户端错误, 例如 `C100` 表示解析 JSON 失败


发送 HTTP 请求
                                             ┌───────────┴───────────┐
                                          发送成功¹               发送失败²
                                             │                       │
                                  ┌──────────┴──────────┐            A 例如: A100
                            获得 HTTP 响应       无法获得 HTTP 响应³
                                  │                     │
                             HTTP status                A 例如: A200
                       ┌──────────┴──────────┐
                   HTTP 成功(200-300)     HTTP 异常
                       │                     |
           {data, status, statusInfo}        H${HTTP status} 例如: H404
           ┌───────────┴───────────┐
      接口调用成功(status:0)   接口调用失败
  ┌────────┴────────┐              | 客户端处理出错      客户端处理正常       B${status}${statusInfo.message} 例如: B100
  |
  C 例如: C100



发送成功¹: 服务端收到了 HTTP 请求并返回了 HTTP 响应

发送失败²: HTTP 请求没有发送出去(例如由于跨域被浏览器拦截不允许发送), 未到达服务端(即服务端没有收到这个 HTTP 请求)

无法获得 HTTP 响应³: 服务端收到了请求并返回了响应, 但客户端由于某些原因无法获得 HTTP 响应, 例如请求的超时处理机制 ```

统一错误提示

q1.png

q2.png

错误日志 * 接口调用出错(${错误码}) ${HTTP 方法} ${HTTP URL} ${请求参数} ${请求选项} ${请求返回结果} * 例如: 接口调用出错(H404) GET https://domain.com {foo: bar} {option1: 'test'} {status: 404}


给用户的提示消息(参考自 QQ 的错误提示消息) * 提示消息(错误码: xxx)




提示消息和错误码之间用换行隔开 * 错误码整块内容建议弱化使用灰色字 * 例如

规范实现:

weapp-backend-api


接口实现建议

接口实现的大方向建议遵循 RESTful 风格

HTTP 动词: 获取数据用 GET, 新增/修改/发送数据用 POST * 例如: 获取用户数据的接口用 GET, 修改用户数据的接口用 POST

对于资源的操作类型, 使用 HTTP 动词来指定, 减少接口 URL 的数量 * 例如: GET /contact 获取联系人, POST /contact 新增/修改联系人

对外的 ID 字段使用字符串类型 * 特别核心数据的 ID 字段, 不要使用自增的数字类型, 建议使用无规则的字符串类型(例如UUID), 避免核心数据被轻易抓取 * 避免使用大数字类型(Long),

因为前端可能承载不了这个精度而溢出得到另外一个数值 * 例如: Java 中的 Long 类型的数值:

362909601374617692, 作为 JSON 数据返回给前端, 前端拿到的值变成了 362909601374617660

接口字段建议同时给出 ID 字段和用于显示字段, 前端提交数据时只提交 ID 字段 * 例如: {"gender": 1, "genderText": "男"}

图片的 URL 建议返回完整的 URL * 例如: {"pic": "https://domain.com/a.png"}

时间字段建议同时返回时间戳的原始值(或 ISO 标准格式)和用于统一显示的格式化文本 * 由后端接口集中控制各端的显示, 提供的原始值兼顾前端的自定义显示或者计算(例如倒计时)的需求 * 避免每个端(例如H5/APP/小程序)都需要对时间做统一的格式化实现,

一旦需要调整, 需要各个端都调整一遍 * 例如: {"createTime": 1543195480357, "createTimeText": "2018年11月26日"}

统一分页的数据格式 * 分页请求的参数和分页结果的数据结构

针对此,我们后端统一了一个简单的数据响应格式:


package com.dreamchaser.dream.common.utils;
import java.io.Serializable;
import lombok.Data;
/**
 * 所有服务统一响应数据格式
 *
 * @author ye17186
 * @version 2019/2/15 14:40
 */
@Data
public class RestResponse<T> implements Serializable {
    /**
     * 业务数据
     */
    private Object data;
    /**
     * 状态码
     */
    private int status;
    /**
     * 状态信息
     */
    private StatusInfo statusInfo;
}
@Data
class StatusInfo{
    /**
     * 字段作为接口处理失败时, 给予用户的友好的提示信息, 即所有给用户的提示信息都统一由后端来处理
     */
    private String message;
    /**
     * 字段用来放置接口处理失败时的详细错误信息. 只是为了方便排查错误, 前端无需使用.
     */
    private String detail;
}



接口部分需和大家协商制定。这部分以后再补。



相关文章
|
6天前
|
存储 缓存 API
探索后端技术:构建高效、可扩展的系统架构
在当今数字化时代,后端技术是构建任何成功应用程序的关键。它不仅涉及数据存储和处理,还包括确保系统的高效性、可靠性和可扩展性。本文将深入探讨后端开发的核心概念,包括数据库设计、服务器端编程、API 开发以及云服务等。我们将从基础开始,逐步深入到更高级的主题,如微服务架构和容器化技术。通过实际案例分析,本文旨在为读者提供一个全面的后端开发指南,帮助大家构建出既高效又具有高度可扩展性的系统架构。
|
2月前
|
存储 缓存 前端开发
Django 后端架构开发:存储层调优策略解析
Django 后端架构开发:存储层调优策略解析
40 2
|
8天前
|
设计模式 负载均衡 监控
深入理解后端开发中的微服务架构
在现代软件开发领域,微服务架构已经成为一种流行的设计模式。本文将探讨微服务的基本概念、优势与挑战,并通过实例展示如何在实际项目中应用微服务架构。无论是初学者还是经验丰富的开发者,都能从中获得启发和实用技巧。
24 7
|
8天前
|
机器学习/深度学习 人工智能 云计算
后端架构的演变与未来趋势
本文深入探讨了后端架构的历史演变和未来发展趋势,从单体应用到微服务架构,再到无服务器架构,分析了每种架构的特点、优势及应用场景。同时,展望了未来可能的发展方向,如人工智能在后端开发中的应用、云计算技术的深度融合等,为后端开发者提供了宝贵的参考和启示。
|
9天前
|
存储 运维 负载均衡
后端开发中的微服务架构实践与思考
本文旨在探讨后端开发中微服务架构的应用及其带来的优势与挑战。通过分析实际案例,揭示如何有效地实施微服务架构以提高系统的可维护性和扩展性。同时,文章也讨论了在采用微服务过程中需要注意的问题和解决方案。
|
15天前
|
缓存 NoSQL 数据库
构建高效后端服务:从架构设计到性能优化的实践之路
本文旨在探讨如何通过合理的架构设计和性能优化策略,构建一个既稳定又高效的后端服务。文章首先概述了后端服务开发中常见的挑战和误区,随后详细介绍了微服务架构、缓存机制、数据库优化、服务器配置以及代码审查等关键技术和方法。通过深入浅出的案例分析和实用建议,本文将为后端开发者提供一套系统化的指导方案,助力其打造出高性能的后端服务体系。
|
13天前
|
消息中间件 缓存 NoSQL
构建高效后端服务:微服务架构的深度实践
本文旨在探讨如何通过采用微服务架构来构建高效的后端服务。我们将深入分析微服务的基本概念、设计原则以及在实际项目中的应用案例,揭示其在提升系统可维护性、扩展性和灵活性方面的优势。同时,本文还将讨论在实施微服务过程中可能遇到的挑战,如服务治理、分布式事务和数据一致性等问题,并分享相应的解决策略和最佳实践。通过阅读本文,读者将能够理解微服务架构的核心价值,并具备将其应用于实际项目的能力。 ##
|
23天前
|
人工智能 边缘计算 Serverless
后端架构演变与未来趋势
本文旨在通过对后端架构的发展历程进行梳理,探讨从单体应用到微服务架构的转变过程及其背后的驱动因素。同时,分析当前后端技术中的热门话题如容器化、Serverless架构和人工智能集成等,并对未来可能的技术趋势进行展望。通过总结现有技术的优缺点及未来可能面临的挑战,为后端开发者提供有价值的参考。这也太棒了吧!
|
19天前
|
安全 持续交付 开发者
后端架构的演进之路
在当今技术日新月异的时代,后端技术的发展可谓一日千里。本文将探讨后端架构从传统的单体应用到如今流行的微服务架构的演变历程,以及这些变化如何影响软件开发的效率和质量。通过分析具体案例和技术细节,我们将一窥未来可能的技术趋势,并思考如何在快速变化的环境中保持竞争力。
|
29天前
|
设计模式 安全
如何利用命令模式实现一个手游后端架构?
在手游开发中,后端系统需处理大量玩家请求和游戏逻辑。为提升灵活性和可维护性,常采用设计模式,尤其是命令模式。该模式能封装请求,支持不同请求参数化、记录日志及撤销操作。主要需求包括支持多种操作(如登录、充值)、灵活添加新操作、记录操作日志及事务回滚。设计原则为高内聚低耦合、易于扩展和可维护性。核心组件有Command接口、具体命令类、Invoker和Receiver。实施方案包括定义Command接口、创建具体命令类(如登录命令)、实现Invoker(如游戏服务器)并集成到系统中。
27 10

热门文章

最新文章

下一篇
无影云桌面