微服务架构风格就是把单独的应用程序构造为一种松耦合服务,每个小服务运行在自己的进程中,并使用轻量级机制通信,通常是httpApi,这些服务围绕业务能力来构建,并通过完全自动化部署机制来独立部署。这些服务可以使用不同编程语言,以及不同存储技术,并保持最低限度的集中式管理
简而言之:拒绝大型单体应用,基于业务边界进行服务微化拆分,各个服务独立部署运行
集群:同一个业务,部署在多个服务器上。解决的是服务器的压力问题,当一台服务器不足以支撑所有的请求,就需要考虑集群服务器,多台服务器共同提供服务。它是一种物理形态,广义上来讲,只要是一堆机器都可以叫集群,他们是否一起协作干活,这个谁也不知道
分布式:一个业务拆分成多个子业务,部署在不同的服务器。解决的一个大项目的臃肿问题,它把一个大的业务拆分成若干份小业务。是一种工作方式。
例如:京东是一个分布式系统,众多业务运行在不同的机器上,所有业务构成一个大型的业务集群。每个小的业务,比如用户系统,访问压力大的时候一台服务器是不够的,我们就需要将用户系统部署到多个服务器,也就是可以将每个业务系统集群化
在分布式系统中,各个服务可能处于不同主机,但服务之间不可避免的相互调用,我们称之为远程调用。springCloud使用http+json的方式完成远程调用。
好处:跨平台,json在任意平台都可以使用。http请求在php、c++等任意系统都可以接收和发送
在分布式系统中,A服务需要调用B服务,B服务在多台机器中存在,A调用任意一个服务器均可完成,为了使每个服务不要太忙也不要太闲,我们可以负载均衡的调用每一个服务器,提高网站的健壮性。
常见的负载均衡算法:
采用前后端分离开发,分为内网部署和外网部署,外网是面向公众访问的,部署前端项目,可以有手机APP、电脑网页;内网部署的是后端集群,前端在页面上操作发送请求到后端,在这途中会经过nginx集群,nginx把请求转交给API网关(springcloud gateway)(网关可以根据当前请求动态地路由到指定的服务,看当前请求是想调商品服务还是购物车服务还是检索),从路由过来如果请求很多,可以负载均衡地调用商品服务器中一台(商品服务复制了多份),当商品服务器出现问题也可以在网关层对服务进行熔断或降级(使用阿里的sentinel组件),网关还有其他的功能如认证授权、限流(只放行部分到服务器)等。
到达服务器后进行处理(springboot为微服务),服务与服务可能会相互调用(使用feign组件),有些请求可能经过登陆才能进行(基于OAuth2.0的认证中心。安全和权限使用springSecurity控制)
服务可能保存了一些数据或者需要使用缓存,我们使用redis集群(分片+哨兵集群)。持久化使用mysql,读写分离和分库分表
服务和服务之间使用消息队列(RabbitMQ),来完成异步解耦,分布式事务的一致性。有些服务可能需要全文检索,检索商品信息,使用ElasticSearch。
服务可能需要存取数据,使用阿里云的对象存储服务OSS。
项目上线后为了快速定位问题,使用ELK对日志进行处理,使用LogStash收集业务里的各种日志,把日志存储到ES中,用Kibana可视化页面从ES中检索出相关信息,帮助我们快速定位问题所在。
在分布式系统中,由于我们每个服务都可能部署在很多台机器,服务和服务可能相互调用,就得知道彼此都在哪里,所以需要将所有服务都注册到注册中心,服务从注册中心发现其他服务所在位置(使用Nacos作为注册中心)。
每个服务的配置众多,为了实现改一处配置相同配置就同步更改,就需要配置中心,也使用阿里的Nacos,服务从配置中心动态取配置。
服务追踪,追踪服务调用链哪里出现问题,使用springcloud提供的Sleuth、Zipkin、Metrics,把每个服务的信息交给开源的Prometheus进行聚合分析,再由Grafana进行可视化展示,提供Prometheus提供的AlterManager实时得到服务的告警信息,以短信/邮件的方式告知服务开发人员。
还提供了持续集成和持续部署,项目发布起来后,因为微服务众多,每一个都打包部署到服务器太麻烦,有了持续集成后开发人员可以将修改后的代码提交到github,运维人员可以通过自动化工具Jenkins Pipeline将github中获取的代码打包成docker镜像,最终是由k8s集成docker服务,将服务以docker容器的形式运行
反映了需要创建的微服务以及相关技术
前后端分离开发。前端项目分为admin-vue(工作人员使用的后台管理系统)、shop-vue(面向公众访问的web网站)、app(公众)、小程序(公众)
linux虚拟机可以使用VMWare,但推荐安装vitualbox,它是开源的。
vagrant init centos/7
,即可初始化一个centos7(注意这个命令在哪个目录下执行的,他的Vagrantfile就生成在哪里)vagrant up
,启动成功出现default: Rsyncing folder: /Users/wangyang/ => /vagrant。然后ctrl+c退出vagrant ssh
,使用exit
退出下次使用可以直接vagrant up直接启动,但要确保当前文件夹下有一个Vagrantfile,不过我们也可以配置环境变量。
启动成功后使用vagrant ssh连接上即可
虚拟机默认的网络使用的是网络地址转换NAT(端口转发)
使用端口转发的方式,若想要访问linux的mysql,需要在vituralbox里设置本机windows的端口比如3333与linux的3306端口绑定,并且每次在linux装一个软件都需要做一个端口映射。我们希望给虚拟机一个固定的ip地址,windows可以和虚拟机互相ping通,虚拟机里面装好一个软件,我们就拿ip地址访问就可以了。有两种方式:
config.vm.network "private_network",ip:"192.168.56.10"
,这个ip需要在windows的ipconfig中查到vitualbox的虚拟网卡ip,然后更改下最后一个数字就行(不能是1,1是我们的主机)。配置完后vagrant reload
重启虚拟机。在虚拟机中ip addr就可以查看到地址了(eth1的ip)。互相ping也能ping通。docker是虚拟化容器技术,基于镜像,可以秒级启动各种容器,每个容器都是一个完整的运行环境,容器之间互相隔离
解决的痛点:以前在虚拟机上装软件,比如装mysql、redis需要源码编译、执行运行、开启服务等一大堆步骤,而且某个软件在运行期间如果出现了问题,可能会影响linux系统,linux里面安装的其他环境都会出现问题。
安装文档:https://docs.docker.com/engine/install/centos/
镜像仓库:dockerHub
安装步骤:
#卸载系统之前的docker
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
#安装一些必须依赖的包
sudo yum install -y yum-utils
# 配置镜像(告诉linux,docker去哪里装)
sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
#安装docker的引擎、客户端及容器
sudo yum install docker-ce docker-ce-cli containerd.io
#启动docker服务
sudo systemctl start docker
# 设置开机自启动
sudo systemctl enable docker
docker常用命令:
#查看docker版本
docker -v
#查看docker镜像
sudo docker images
为docker配置镜像加速:
因为docker下载镜像默认从dockerHub中下载,而dockerHub是一个国外的网站,下载速度比较慢,我们可以配置一个国内的镜像加速,以后docker想要下载镜像就非常快了,镜像加速推荐大家使用阿里云。根据https://cr.console.aliyun.com/cn-qingdao/instances/mirrors执行完命令
#创建一些目录
sudo mkdir -p /etc/docker
#配置镜像加速器地址
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://chqac97z.mirror.aliyuncs.com"]
}
EOF
#重启docker的伴随线程
sudo systemctl daemon-reload
#重启docker服务
sudo systemctl restart docker
#下载镜像
sudo docker pull mysql:5.7
#检查下载的镜像
sudo docker images
sudo docker run -p 3306:3306 --name mysql \
-v /mydata/mysql/log:/var/log/mysql \
-v /mydata/mysql/data:/var/lib/mysql \
-v /mydata/mysql/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql:5.7
参数说明:
-p:端口映射,将容器的3306端口映射到主机的3306端口
–name:指定容器名字
-v:目录挂载
-e:设置mysql参数(初始化root用户的密码)
-d:后台运行,mysql5.7指的是我们是用哪个镜像启动
su root 密码为vagrant,这样就可以不写sudo了
#查看docker正在运行的进行
[root@localhost vagrant]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6a685a33103f mysql:5.7 "docker-entrypoint.s…" 32 seconds ago Up 30 seconds 0.0.0.0:3306->3306/tcp, 33060/tcp mysql
此时就可以用客户端连接mysql了
补充知识:
1⃣️、每docker run就会启动一个容器,而且容器跟容器之间是互相隔离的,且每一个容器都是一个完整的运行环境(其实就是一个完整的linux)
如何验证?
#以交互模式进入容器中linux的bash控制台
docker exec -it mysql /bin/bash
ls /
#发现目录结构就是一个完整的linux目录结构,mysql就被装在这个linux里面
#查询mysql在哪里
whereis mysql
#推出交互模式
exit
2⃣️、docker容器端口映射和文件挂载
端口映射: mysql装到了容器里面,mysql默认会有一个端口3306,但这个端口相当于是在mysql容器里用的端口,如果我们想要访问mysql需要把3306映射到linux里面,-p 3306:3306意思就是linux的3306端口与mysql容器内部的3306端口对应,相当于访问linux的3306就能访问到容器内部装的mysql
文件挂载: mysql是被装在容器内部,mysql的配置文件是在/etc/mysql下,mysql的相关日志在/var/log/mysql目录下。但是难道我们每次修改mysql的相关配置都需要进到容器内部来改嘛?太麻烦了,我们希望将我们经常要用到或查看的内容映射到linux的目录里,-v的命令就是这个作用。相当于我们之前做的快捷方式
3. 修改mysql的配置
#因为有目录映射,所以我们可以直接在linux里修改
vi /mydata/mysql/conf/my.conf
[client]
default-character-set=utf8
[mysql]
default-character-set=utf8
[mysqld]
init_connect='SET collation_connection = utf8_unicode_ci'
init_connect='SET NAMES utf8'
character-set-server=utf8
collation-server=utf8_unicode_ci
skip-character-set-client-handshake
skip-name-resolve
#重启mysql
docker restart mysql
这些配置也会映射到容器内部
docker pull redis
# 如果直接挂载的话docker会以为挂载的是一个目录,所以我们先创建一个文件然后再挂载,在虚拟机中。
# 在虚拟机中
mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf
#启动redis
docker run -p 6379:6379 --name redis \
-v /mydata/redis/data:/data \
-v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf
docker exec -it redis redis-cli
redis默认是不持久化的。在配置文件中输入appendonly yes,就可以aof持久化了。
vim /mydata/redis/conf/redis.conf
# 插入下面内容
appendonly yes
#重启redis
docker restart redis
设置redis容器在docker启动的时候启动
docker update redis --restart=always
# 配置用户名
git config --global user.name "username" //(名字,随意写)
# 配置邮箱
git config --global user.email "[email protected]" // 注册账号时使用的邮箱
# 配置ssh免密登录
ssh-keygen -t rsa -C "[email protected]"
#三次回车后生成了密钥
#查看密钥
cat ~/.ssh/id_rsa.pub
#需要将密钥复制到码云里面,浏览器登录码云后,个人头像上点设置--ssh公钥
#添加公钥,标题随便写,将查看的id_rsa.pub里的公钥内容复制过来
# 测试
ssh -T [email protected]
#测试成功,手动输入yes,就可以无密给码云推送仓库了
在码云新建仓库,仓库名gulimall,选择语言java,在.gitignore选中maven(就会忽略掉maven一些个人无需上传的配置文件),许可证选Apache-2.0,开发模型选生产/开发模型,开发时在dev分支,发布时在master分支。
在IDEA中New–Project from version control–git–复制刚才项目的地址,如https://github.com/1046762075/mall
IDEA然后New Module–Spring Initializer–group填com.atguigu.gulimall、Artifact填 gulimall-product、describe填谷粒商城商品服务,Next—选择web(web开发)、springcloud routing里选中openFeign(rpc调用)。
依次创建出以下服务:
共同点:
然后右下角显示了springboot的service选项,选择
从某个项目粘贴个pom.xml粘贴到项目目录,修改他
<?xml version="1.0" encoding="UTF-8"?>
://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
>4.0.0 >
>com.atguigu.gulimall >
>gulimall >
>0.0.1-SNAPSHOT >
>gulimall >
>聚合服务 >
>pom >
>
>gulimall-coupon >
>gulimall-member >
>gulimall-order >
>gulimall-product >
>gulimall-ware >
>
>
在maven窗口刷新,并点击+号,找到刚才的pom.xml添加进来,发现多了个root。这样比如运行root的clean命令,其他项目也一起clean了。
修改总项目的.gitignore,把小项目里的垃圾文件在提交的时候忽略掉,比如target
**/mvnw
**/mvnw.cmd
**/.mvn
**/target/
.idea
**/.gitignore
如果你是拉取的我的仓库,你也可以看到我仓库里是没有那些target目录的,非常精简
在git/local Changes,点击刷新看Unversioned Files,可以看到变化。
全选最后剩下21个文件,选择右键、Add to VCS。
在IDEA中安装插件:gitee,重启IDEA。
在Default changelist右键点击commit,去掉右面的勾选Perform code analysis、CHECK TODO,然后点击COMMIT,有个下拉列表,点击commit and push才会提交到云端。此时就可以在浏览器中看到了。
commit只是保存更新到本地
push才是提交到gitee
打开navicat,连接linux里的mysql docker镜像
注意:重启linux和docker,docker里的容器就关了
sudo docker ps
sudo docker ps -a
#这两个命令的区别:后者会显示已创建未启动的容器
#接下来设置我们要用的容器每次都自启
sudo docker update redis --restart=always
sudo docker update mysql --restart=always
#如果不设置上面的自启的话,也可以手动启动
sudo docker start mysql
sudo docker start redis
#如果要进入已启动的容器
sudo docker exec -it mysql /bin/bash
# /bin/bash就是进入一般的命令行,如果改成redis就是进入了redis
使用powerDesigner打开并查看数据库表关系
name是给我们看的,code才是数据库里真正的信息。
选择primary和identity作为主键。然后点preview就可以看到生成这张表的语句。
点击菜单栏database–generate database—点击确定
使用navicat分别创建gulimall-oms、gulimall-pms、gulimall-sms、gulimall-ums、gulimall-wms库,并创建数据库表,sql:https://github.com/FermHan/gulimall。
1、所有的数据库数据再复杂也不建立外键,因为在电商系统中,数据量大,做外间关联很耗性能
2、字符集选utf8mb4,他能兼容utf8且能解决一些乱码的问题。
在码云上搜索人人开源,我们使用renren-fast(后端)、renren-fast-vue(前端)项目。项目地址:https://gitee.com/renrenio
#克隆代码
git clone https://gitee.com/renrenio/renren-fast.git
git clone https://gitee.com/renrenio/renren-fast-vue.git
下载到桌面,我们把renren-fast移到我们项目文件夹里(删掉.git文件),renren-fast-vue用VSCode打开(后面再弄)
在idea的父项目的pom.xml中添加一个renren-fast
<modules>
<module>gulimall-couponmodule>
<module>gulimall-membermodule>
<module>gulimall-ordermodule>
<module>gulimall-productmodule>
<module>gulimall-waremodule>
<module>renren-fastmodule>
modules>
然后打开renren-fast/db/mysql.sql,复制全部,在navicat中创建库guli-admin,粘贴刚才的内容执行。
然后修改项目里renren-fast中的application.yml,修改application-dev.yml中的数库库的url、username、password
url: jdbc:mysql://localhost:3306/guli_admin?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
然后运行该java项目下的RenrenApplication
用VSCode打开renren-fast-vue
安装node.js:http://nodejs.cn/download/ ,下载完安装。
npm是随同nodejs一起安装的包管理工具,javaScript-npm类似于maven
命令行输入node -v
检查node是否安装成功,配置npm的仓库地址
node -v
npm config set registry http://registry.npm.taobao.org/
然后去VScode的项目终端中输入npm install
,是要去拉取依赖(package.json类似于pom.xml的dependency)
在终端输入npm run dev
运行项目
克隆人人项目的逆向工程项目
git clone https://gitee.com/renrenio/renren-generator.git
下载到桌面后,同样把里面的.git文件删除,然后移动到我们IDEA项目目录中,同样配置好pom.xml
<modules>
<module>gulimall-couponmodule>
<module>gulimall-membermodule>
<module>gulimall-ordermodule>
<module>gulimall-productmodule>
<module>gulimall-waremodule>
<module>renren-fastmodule>
<module>renren-generatormodule>
modules>
在maven中刷新一下,让项目名变粗体,稍等下面进度条完成。
修改application.yml
url: jdbc:mysql://localhost:3306/gulimall-pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
然后修改generator.properties
# 主目录
mainPath=com.atguigu
#包名
package=com.atguigu.gulimall
#模块名
moduleName=product
#作者
author=hh
#email
[email protected]
#表前缀(类名不会包含表前缀) # 我们的pms数据库中的表的前缀都pms
# 如果写了表前缀,每一张表对于的javaBean就不会添加前缀了
tablePrefix=pms_
运行RenrenApplication。访问http://localhost,点击renren-fast,显示全部的表,全选,点击生成代码,下载了压缩包,解压压缩包,把main放到gulimall-product的同级目录下
gulimall-common
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.3.2version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.8version>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpcoreartifactId>
<version>4.4.13version>
dependency>
<dependency>
<groupId>commons-langgroupId>
<artifactId>commons-langartifactId>
<version>2.6version>
dependency>
tips: shift+F6修改项目名
此外,说下maven依赖的问题:
:一般用在父模块中,用于管理子模块依赖的版本号,但并没有实际引入
:在子模块中对依赖进行声明。倘若子模块不声明自己的依赖,是不会从父模块的dependencyManagement继承的,只有子模块中也声明了依赖,并且没有写对应的版本号它才会从父类中继承,并且version和scope都是取自父类;此外要是子模块中自己定义了自己的版本号,是不会继承自父类的。
:写在dependencies里,代表本项目依赖,子项目也依赖。如果有个
标签,代表本项目依赖,但是子项目不依赖
在product项目中的pom.xml,引入common
<dependency>
<groupId>com.atguigu.gulimallgroupId>
<artifactId>gulimall-commonartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
将人人项目中用到的类复制过来:
将暂时不用的注释:
总之什么报错就去renren-fast项目里找。重启逆向工程。重新在页面上得到压缩包。重新解压出来,不过只把里面的controller复制粘贴到product项目对应的目录就行。
打开renren-generator,修改generator.properties(主要修改模块名、表前缀)
# 主目录
mainPath=com.atguigu
#包名
package=com.atguigu.gulimall
#模块名
moduleName=coupon
#作者
autho=wangyang
#email
[email protected]
#表前缀(类名不包含表前缀)
tablePrefix=sms_
修改yml的数据库连接信息
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
启动RenrenApplication.java,运行后去浏览器80端口查看,同样让他一页全显示后选择全部后生成。生成后解压将main文件夹复制到coupon项目对应目录下,将resources下src包先删除
让coupon也依赖于common,修改pom.xml
<dependency>
<groupId>com.atguigu.gulimallgroupId>
<artifactId>gulimall-commonartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
添加application.yml
server:
port: 7000
spring:
datasource:
username: root
password: root
url: jdbc:mysql://192.168.56.10:3306/gulimall-sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0
运行gulimallCouponApplication.java
浏览器输入http://localhost:7000/coupon/coupon/list,返回如下信息,表明成功
{"msg":"success","code":0,"page":{"totalCount":0,"pageSize":10,"totalPage":0,"currPage":1,"list":[]}}
同理逆向生成其它模块(membre会员系统、order订单系统、ware仓储系统)
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.3.2version>
dependency>
参考mybaits的官方文档:https://mp.baomidou.com/guide/quick-start.html,主要分为这几个步骤:
1)、导入数据库驱动(由于后面所有的微服务都需要驱动,我们直接将驱动导入到common中,并且注意mysql驱动的版本与数据库版本对应)
<dependency> <groupId>mysqlgroupId> <artifactId>mysql-connector-javaartifactId> <version>8.0.22version> dependency>
2)配置数据源(推荐application.yml)
spring: datasource: username: root password: 123456 url: jdbc:mysql://localhost:3306/gulimall_pms driver-class-name: com.mysql.jdbc.Driver
1、在主启动类上使用@MapperScan告诉mapper接口都在哪
@MapperScan("com.atguigu.gulimall.product.dao")
2、告诉mybatisPlus,sql映射文件在哪里
mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml
可以点进去mapper-locations看看默认的地址(classpath*:/mapper/**/.xml)
classpath后面加个表示不止扫描自己的类路径,包括引用的其它jar里的类路径。要想精确可以不加
3、主键自增
如果每一次都调整需要在每个entry都需要设置,可以直接在application.yml中设置mybatis-plus: global-config: db-config: id-type: auto
1⃣️、要用的servlet相关的东西,导入后重新导包
<dependency>
<groupId>javax.servletgroupId>
<artifactId>servlet-apiartifactId>
<version>2.5version>
<scope>providedscope>
dependency>
2⃣️、删掉common里xssFiler和XssHttpServletRequestWrapper
3⃣️、将renren-fast中RRException复制过来
@SpringBootTest
class GulimallProductApplicationTests {
@Autowired
BrandService brandService;
@Test
void saveBrand() {
BrandEntity brandEntity = new BrandEntity();
brandEntity.setName("华为");
brandService.save(brandEntity);
System.out.println("保存成功");
}
@Test
void getBrand(){
List<BrandEntity> list = brandService.list(new QueryWrapper<BrandEntity>().eq("brand_id", 1l));
list.forEach((item)->{
System.out.println(item);
});
}
}
在分布式开发里面,首先要知道注册中心、配置中心、网关这三个模块。
springCloud Alibaba一站式解决方案:https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
为什么选择springCloud Alibaba?
springCloud的痛点:1⃣️部分组件停止维护和更新,给开发带来不便。2⃣️部分环境搭建复杂,没有完善的可视化界面,需要大量的二次开发和定制,3⃣️配置复杂,难以上手,部分配置差别难以区分和合理应用
springCloudAlibaba的优点:1⃣️阿里使用过的组件,经历过了考验,设计合理、性能强悍,现在开源出来。2⃣️成套的产品搭配完善的可视化界面给开发运维带来了极大的方便。3⃣️搭建简单,学习成本低
结合springCloud Alibaba最终我们的技术搭配方案:
springCloud Alibaba Nacos——注册中心,服务注册/发现
springCloud Alibaba Nacos——配置中心(动态配置管理)
springCloud Ribbon——负载均衡
springCloud Feign——声明式Http客户端(调用远程服务)
springCloud Alibaba Sentinel——服务容错(限流、降级、熔断)
springCloud Gateway——API网关(webFlux编程模式)
springCloud Sleuth——调用链监控
springCloud Alibaba Seata——原Fescar,即分布式事务解决方案
在common的pom.xml中添加springCloud Alibaba
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.2.0.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
官方文档:https://github.com/alibaba/spring-cloud-alibaba/blob/master/spring-cloud-alibaba-examples/nacos-example/nacos-discovery-example/readme-zh.md
下载nacos服务端,并启动:
windows:下载–解压–双击bin/startup.cmd。
linux/Mac:进入解压后的bin目录,执行sh startup.sh -m standalone
访问:http://localhost:8848/nacos/ 账号密码:nacos/nacos
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
不用写版本号,是因为之前之前引入了spring-cloud-alibaba-dependencies作为版本管理
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: gulimall-coupon
依次给member、order、product、ware模块配置nacos server地址并设置服务名,使用@EnableDiscoverClient注解开启服务注册与发现。
启动后访问http://localhost:8848/nacos/ ,可以看到注册进来的实例
feign是一个声明式的http客户端
比如member会员服务想要从coupon优惠券服务获取当前会员的所有优惠券,那么会员服务需要先从注册中心中找一下优惠券服务都在哪几台服务器上,会员服务挑一台机器发送请求,优惠券服务响应数据。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
@RestController
@RequestMapping("coupon/coupon")
public class CouponController {
@Autowired
private CouponService couponService;
@RequestMapping("member/list")
public R memberCoupons(){
CouponEntity couponEntity = new CouponEntity();
couponEntity.setCouponName("满100减20");
return R.ok().put("coupons",Arrays.asList(couponEntity));
}
}
@FeignClient("gulimall-coupon")//告诉springCloud这个接口是一个远程客户端,要调用coupon服务(nacos中找到),其中gulimall-coupon是优惠服务注册到nacos的服务名
public interface CouponFeignService {
//远程服务的url,注意要写全优惠券服务的方法的映射
//注意这个地方不是控制层,所以这个请求映射请求的不是我们服务器上的东西,而是nacos注册中心的
@RequestMapping("/coupon/coupon/member/list")
public R memberCoupons();
}
@FeignClient和@RequestMapping构成远程调用的坐标。@FeignClient指定服务名,@RequestMapping指定方法的映射
@EnableFeignClients(basePackages="com.atguigu.gulimall.member.feign")//使用basePackages指定扫描哪个包下标注了@FeignClient注解的接口
@EnableDiscoveryClient
@MapperScan("com.atguigu.gulimall.member.dao")
@SpringBootApplication
public class GulimallMemberApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallMemberApplication.class);
}
}
@RestController
@RequestMapping("member/member")
public class MemberController {
@Autowired
private MemberService memberService;
@Autowired
CouponFeignService couponFeignService;
@RequestMapping("/coupons")
public R test(){
MemberEntity memberEntity = new MemberEntity();
memberEntity.setNickname("张三");
R coupons = couponFeignService.memberCoupons();
return R.ok().put("member",memberEntity).put("coupons",coupons.get("coupons"));
}
重启服务,发送http://localhost:8000/member/member/coupons,返回
{
"msg":"success",
"code":0,
"coupons":[
{"id":null,"couponType":null,"couponImg":null,"couponName":"满100-10","num":null,"amount":null,"perLimit":null,"minPoint":null,"startTime":null,"endTime":null,"useType":null,"note":null,"publishCount":null,"useCount":null,"receiveCount":null,"enableStartTime":null,"enableEndTime":null,"code":null,"memberLevel":null,"publish":null}
],
"member":{"id":null,"levelId":null,"username":null,"password":null,"nickname":"会员昵称张三","mobile":null,"email":null,"header":null,"gender":null,"birth":null,"city":null,"job":null,"sign":null,"sourceType":null,"integration":null,"growth":null,"status":null,"createTime":null}
}
nacos除了可以作为注册中心,还可以作配置中心。这样就可以不在application.properties中配置,而是放在nacos配置中心里,这样就可以不用每次修改配置都需要打包部署到每一台服务器上
官方文档:https://github.com/alibaba/spring-cloud-alibaba/blob/master/spring-cloud-alibaba-examples/nacos-example/nacos-config-example/readme-zh.md
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
# 配置服务名字和nacos配置中心的地址
spring.application.name=gulimall-coupon
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
服务名.properties
中的配置),配置coupon.user.name="配置中心"
coupon.user.age=12
@RestController
@RequestMapping("coupon/coupon")
public class CouponController {
@Value("${coupon.user.name}")//从application.properties中获取,不要写user.name,他是环境里的变量
private String name;
@Value("${coupon.user.age}")
private Integer age;
@RequestMapping("/test")
public R test(){
return R.ok().put("name",name).put("age",age);
}
}
@RefreshScope
,在nacos中修改配置时,就能动态的获取(不用在修改配置后重启服务){"msg":"success","code":0,"name":"配置中心","age":12}
#指定的是命名空间的id
spring.cloud.nacos.config.namespace=1a0f8db6-c095-44a7-a3ec-a8bb0c7f284a
2)、也可以基于每个微服务互相隔离配置,每一个微服务都创建自己的命名空间,这样只加载
自己命名空间的配置
spring.cloud.nacos.config.group=DEFAULT_GROUP
最终方案:每个微服务创建自己的命名空间,使用配置分组区分环境(dev/test/prod)
业务场景:每个微服务都会有非常多的配置,我们不会将所有的配置都放在一个配置中,比如跟数据源有关的配置、跟框架有关的配置都放在不同的配置文件中。
配置结果如下:
只需要在bootsrap.properties中额外加载这些配置文件
spring.cloud.nacos.config.ext-config[0].data-id=datasource.yml
spring.cloud.nacos.config.ext-config[0].group=dev
spring.cloud.nacos.config.ext-config[0].refresh=true
spring.cloud.nacos.config.ext-config[1].data-id=mybatis.yml
spring.cloud.nacos.config.ext-config[1].group=dev
spring.cloud.nacos.config.ext-config[1].refresh=true
spring.cloud.nacos.config.ext-config[2].data-id=other.yml
spring.cloud.nacos.config.ext-config[2].group=dev
spring.cloud.nacos.config.ext-config[2].refresh=true
就不需要application.yml,只需要bootstrap.properties指定加载多个配置集。默认还是会加载coupon.properties
引入:
1、动态路由:比如前端需要调后台的商品服务,并不知道商品服务器(假如有123台服务器)哪个是正常的哪个是宕机,如果1号服务器掉线,需要手动改成2号服务器。这时就需要先通过网关帮我们动态的路由到正常的服务器,因为网关可以从注册中心实时感知某个服务上线还是下线
2、每一个请求过来,我们要给他加上权限、监控等,去每一个服务都需要做这些事,如果我们把这些功能写在各个服务上,就出现了很多重复开发。我们可以让每个请求先经过网关,进行鉴权、限流、日志输出后代转给其他服务
官方文档:https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/
三大核心概念:
客户端发送请求给服务端,先将请求发给API网关(gateway),gateway通过handler mapping(映射器)看当前请求能否被路由(处理),如果能被处理将请求交给handler(处理器),处理器要处理这些请求,就会经过一系列filter,然后路由到指定服务,指定服务处理完,又经过一系列filter,再返回给客户端
总结:当请求到达网关,网关先利用断言来判断这次请求是否符合某个路由规则,如果符合就按这个路由规则把它路由到指定地方,但要去这指定地方就要经过一些列filter进行过滤
使用:
@EnableDiscoveryClient
开启服务注册发现,排除mybatis的相关配置@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallGatewayApplication.class, args);
}
}
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.application.name=gulimall-gateway
server.port=88
spring.application.name=gulimall-gateway
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
#在nacos中创建gateway名称空间,并指向它
spring.cloud.nacos.config.namespace=1141622a-b05c-4ecd-b423-4c55694ba774
spring:
cloud:
gateway:
routes:
# -代表数组
- id: test_route
uri: https://www.baidu.com
predicates:
- Query=url,baidu
- id: qq_route
uri: https://www.qq.com
predicates:
- Query=url,qq
补充:gateway使用的是Netty,而不是tomcat。Netty有非常高的网络性能
DROP TABLE IF EXISTS `pms_category`;
CREATE TABLE `pms_category` (
`cat_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类id',
`name` char(50) DEFAULT NULL COMMENT '分类名称',
`parent_cid` bigint(20) DEFAULT NULL COMMENT '父分类id',
`cat_level` int(11) DEFAULT NULL COMMENT '层级',
`show_status` tinyint(4) DEFAULT NULL COMMENT '是否显示[0-不显示,1显示]',
`sort` int(11) DEFAULT NULL COMMENT '排序',
`icon` char(255) DEFAULT NULL COMMENT '图标地址',
`product_unit` char(50) DEFAULT NULL COMMENT '计量单位',
`product_count` int(11) DEFAULT NULL COMMENT '商品数量',
PRIMARY KEY (`cat_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1433 DEFAULT CHARSET=utf8mb4 COMMENT='商品三级分类';
controller:
@RestController
@RequestMapping("product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 查出所有分类及子分类,以树形列表展示出来
*/
@RequestMapping("/list/tree")
public R list(@RequestParam Map<String, Object> params){
List<CategoryEntity> entities = categoryService.listTree();
return R.ok().put("data", entities);
}
}
service:
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
@Override
public List<CategoryEntity> listTree() {
//1、查询所有
List<CategoryEntity> categoryEntities = baseMapper.selectList(null);
//2、组成树形
List<CategoryEntity> level1Menus = categoryEntities.stream().filter((categoryEntity) -> {
return categoryEntity.getParentCid() == 0;
}).map((menu)->{
menu.setChildren(getChildrens(menu,categoryEntities));
return menu;
}).sorted((menu1,menu2)->{
return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
}).collect(Collectors.toList());
return level1Menus;
}
//递归查找所有菜单的子菜单
private List<CategoryEntity> getChildrens(CategoryEntity menu,List<CategoryEntity> all){
List<CategoryEntity> collect = all.stream().filter((categoryEntity) -> {
return categoryEntity.getParentCid() == menu.getCatId();
}).map((categoryEntity)->{
categoryEntity.setChildren(getChildrens(categoryEntity,all));
return categoryEntity;
}).sorted((menu1,menu2)->{
return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
}).collect(Collectors.toList());
return collect;
}
通过访问http://localhost:10000/product/category/list/tree测试
继续新增
刷新可以看到,商品系统里多了一个分类维护,并且gulimall_admin.sys_menu多了一条数据,父id是商品系统id
想要做的效果:点击分类维护菜单,展示电商系统的三级分类
现在点击分类维护可以看到url地址为:http://localhost:8001/#/product-category
,观察系统管理里的角色管理(url:http://localhost:8001/#/sys-role
)对应的视图src/view/modules/sys/role.vue,因此我们需要在src/view/modules/下新建文件夹product,product里面新建category.vue。
输入vue快速生成vue模版,在然后去https://element.eleme.cn/#/zh-CN/component/tree
看如何使用树形控件。修改的部分如下
目前的情况:获取验证码和登陆时都是renren-fast-vue直接调用renren-fast后台项目,而后台系统可能部署在多个服务器上
想要做的效果:前端renren-fast-vue先通过网关(一些列的断言、路过滤等操作)再调用renren-fast后台项目
<dependency>
<groupId>com.atguigu.gulimallgroupId>
<artifactId>gulimall-commonartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
<dependency>
<groupId>com.google.code.gsongroupId>
<artifactId>gsonartifactId>
<version>2.8.5version>
dependency>
spring:
application:
name: renren-fast
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
//在renren-fast的启动类上加@EnableDiscoverCliet开启服务注册发现
@EnableDiscoveryClient
@SpringBootApplication
public class RenrenApplication {
public static void main(String[] args) {
SpringApplication.run(RenrenApplication.class, args);
}
}
启动后可以在nacos的服务列表中看到
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';
//前端所有的请求都带上/api,以便网关进行断言
spring:
cloud:
gateway:
routes:
- id: admin_root
uri: lb://renren-fast #lb代表负载均衡
predicates:
- Path=/api/** #路径路由,路径中带api的断言为真
filters:
- RewritePath=/api/(?>.*),/renren-fast/$\{segment}
# 把/api/* 改变成 /renren-fast/*
协议、域名、端口
都要相同,其中一个不同都会产生跨域我们一般使用方法2跨域,在gateway里写一个过滤器,允许所有请求跨域
@Configuration
public class GulimallCorsConfiguration {
@Bean // 添加过滤器
public CorsWebFilter corsWebFilter(){
// 基于url跨域,选择reactive包下的
UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource();
// 跨域配置信息
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 允许跨域的头
corsConfiguration.addAllowedHeader("*");
// 允许跨域的请求方式
corsConfiguration.addAllowedMethod("*");
// 允许跨域的请求来源
corsConfiguration.addAllowedOrigin("*");
// 是否允许携带cookie跨域
corsConfiguration.setAllowCredentials(true);
// 任意url都要进行跨域配置
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
点击分类维护菜单,数据没有加载出来,这是因为网关上所做的路径映射不正确,映射后的路径为http://localhost:8001/renren-fast/product/category/list/tree,但是只有通过http://localhost:10000/product/category/list/tree路径才能够正常访问,所以会报404异常
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?>.*),/$\{segment}
精确断言的路由放在模糊断言的路由前面,优先判断。比如/api/product/** 要放在/api/**的前面
data() {
return {
menus: [],
defaultProps: {
children: "children",
label: "name",
},
};
},
methods: {
handleNodeClick(data) {
console.log(data);
},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据.....", data.data);
this.menus = data.data;
});
},
}
}