前言

生产力就是王道,谁能够更短时间做出更多工作,谁就能胜利。

保障生产力持续在线的最佳方案就是:将琐事交给工作流

需求

搭建一个在线小说阅读编写网站

功能如下:

  1. 实现登录、注册
  2. 实现小说编写、阅读、查询、收藏
  3. 实现评论查看、发布

依赖如下:

  1. 使用 SpringBoot、Vue 框架,实现前后端分离
  2. 使用 Spring Cloud 构建微服务

技术要点

业务逻辑明确、简单,不做赘述,我们讲讲微服务。

什么是微服务

我们在没有接触技术之前,或仅仅道听途说,很容易产生畏难心理。

实际上,微服务就是把我们具有多种功能的单体服务,拆分成内聚性强的个体,方便解耦。

就拿我们的项目举例,用户登录注册逻辑连贯,共享用户表格,可以拆分。

又如评论模块,尽管使用了用户模块的 ID、小说模块的 ID,但其功能独立,可以拆分。

这样我们通过拆分,就得到了多个服务,每个服务放到一个服务器上,这样就能将原本的单体服务高压力,分散到多个服务器上,从而提升服务可用性。

服务注册与发现

如果是单体服务,我们在前端只需注明它的 IP 地址就好了,但是微服务有多个 IP 地址,我们怎么管理?

如果我们没法预知服务器 IP,那么我们可以让服务器自己来我们这注册。

Nacos 是一个服务注册与发现系统,只需要在每个服务的配置文件中添加 Nacos 服务器的 IP,Nacos 就能找到我们的服务,并将其写入它的数据库。

服务分发与登录校验

服务分发

我们可以在 Nacos 上看到服务列表,但是我们怎么从前端发送请求呢,到底要发给谁?

我们自然而然会想到 Nginx,根据路径分发服务。

好想法!不过 Nacos 有自己的小团体,Gateway 就能帮助Nacos 做服务分发。

所以为什么不用 Nginx? 我们考虑有多个相同服务的场景,如果服务上线后需要添加和删除服务,那么 Nginx 配置文件就要手动修改,非常麻烦。

登录校验

Gateway 还能做什么? 把登录校验放进去吧,加个拦截器,神人才去写配置文件。

用户发送请求后,某些服务需要获取当前用户信息,比如 userId

如果前后端发送的时候都携带 userId,首先是要改接口,其次不安全。

不安全好说,加密 userId 就 ok,这就是 token,可以使用 JWT (JSON Web Tokens) 来生成。

改接口也不需要,直接在请求头部添加 Authorization = token 即可。

然后在服务侧解密 token,获取 userId,存入 threadLocal 中,方便后续使用。

那我们抽取这一过程 解密->获取userId,到 Gateway 中完成。

此时,我们在 vue 中就可以访问 GateWay 来发送请求了,它会为我们自动转发到配置好的分发地址指向的服务。

一些小问题

如果你和我一样使用了云服务器,那么大概率在 Nacos 上注册的服务列表项中的 IP 是内网地址。

此时 Gateway 会将请求转发到内网地址,而内网地址不直接可达。

我们还得在配置文件中添加 spring.cloud.nacos.discovery.ip 配置项,指定外网地址。

工作流

微服务多了之后,部署起来就很麻烦。

尤其是我们这种小作坊,还是敏捷开发。

万幸 githubactions,可以通过编写一个 workflow 脚本,我们可以很轻松的将触发更新的代码编译成 jar 包,并部署到服务器上。

下面是 novel-serviceworkflow 脚本,我一共做了 4 个脚本,是因为 secrets 有不同的命名,所以需要分开,大概率有更好的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
name: Build and Deploy Novel Service

on:
push:
branches:
- main # 或你希望触发的分支名
pull_request:
branches:
- main

jobs:
build-deploy:
runs-on: ubuntu-latest
strategy:
matrix:
service: ["novel-service"]

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: "17"
distribution: "temurin"

- name: Cache Maven packages
uses: actions/cache@v4
with:
path: ~/.m2
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-

- name: Build Service - ${{ matrix.service }}
run: |
cd final-chapters-backend/${{ matrix.service }}
mvn clean package

- name: Upload Artifact (Optional)
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.service }}-jar
path: final-chapters-backend/${{ matrix.service }}/target/*.jar

- name: Deploy to server
uses: appleboy/scp-action@master
with:
host: ${{ secrets.HOST_NOVEL_SERVICE }}
username: ${{ secrets.USERNAME_NOVEL_SERVICE }}
port: ${{ secrets.PORT_NOVEL_SERVICE }}
key: ${{ secrets.KEY_NOVEL_SERVICE }}
source: "final-chapters-backend/${{ matrix.service }}/target/*.jar"
target: "/root/workspace/FinalChaptersServer"

- name: Restart Service
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST_NOVEL_SERVICE }}
username: ${{ secrets.USERNAME_NOVEL_SERVICE }}
port: ${{ secrets.PORT_NOVEL_SERVICE }}
key: ${{ secrets.KEY_NOVEL_SERVICE }}
command_timeout: 20s
script: |
set -ex

PID=$(ps -ef | grep ".jar" | grep -v "grep" | awk '{print $2}')
if [ -n "$PID" ]; then
kill -9 $PID
fi

cd /root/workspace/FinalChaptersServer/final-chapters-backend/${{ matrix.service }}/target/
nohup java -jar *.jar > ${{ matrix.service }}.txt 2>&1 &
exit

几个要点

  1. 使用 scpjar 包推送到远程服务器上
  2. 通过 ssh 连接远程服务器,执行命令,并把命令的输出重定向到文件中。
  3. secrets 用来保存敏感信息,比如密码,用户名,ssh 密钥等。
  4. 如果你不写 2>&1,最后一步脚本会超时。