GuliMallKnowledge
软件开发流程
需求分析——写出需求规格说明书(仅仅说明需求)
设计——产品文档,UI界面设计,概要设计,详细设计,数据库设计
开发!——写项目代码+单元测试
测试——测开人员编写测试用例,写测试报告
上线运维——运维人员配置软件运行环境
项目组角色分工
软件环境
本地环境:自己电脑上的运行环境
开发环境:开发人员在开发阶段使用的环境,一般外部用户无法访问
测试环境:专门给测试人员使用的环境,用于项目测试(另一个服务器上运行其JDK,tomcat,MySQL…)
生产环境:即线上环境,提供对外服务的环境
瑞吉外卖
系统管理后台
- 功能:分离管理,菜品管理,套餐管理,菜品口味管理,员工登录,员工退出,员工管理,订单管理
移动端
- 功能:手机号登录,微信登录,地址管理,历史订单,菜品规格,购物车,下单,菜品浏览
角色分类
后台系统管理员:用于后台全部操作权限
后台系统普通员工:登录后对菜品,套餐,订单进行管理
C端用户。
技术选型:
说明:
Nginx:
Nginx是一个用于提供静态资源(HTML,CSS,JS,图像…)的HTTP服务器。
功能之一是代理前端请求,发送到后端。目的是提供负载均衡,缓存,SSL等功能。
- 负载均衡的实现:Nginx在多个后台服务器之间分发请求。
功能之一是高并发处理(特色)
Spring Session:
Spring Session是Spring Framework的一个子项目。用于简化在分布式系统中管理用户会话的功能。
Spring Session的提供了一种抽象的方式来管理用户会话——将会话数据存储在外部存储介质中(如数据库,Redis,Hazelcast等),为了使不同服务器上的应用实例能共享和访问同一用户的会话信息。
Swagger:
Swagger是一个用于描述,设计,构建,管理,测试和文档化RESTful Web服务的工具集。是用来对前后端分离时中间API进行管理的。
Redis:
Remote Dictionary Server 高性能键值存储数据库。
Redis以键值对的形式存储数据,并支持多种数据结构,如字符串,哈希,列表,集合,有序集合…
Redis主要用途:缓存、会话存储、消息队列、排行榜,实时数据分析,分布式锁,地理位置信息
谷粒商城
技术选型
基础篇
后端基础环境
Spring Boot + Spring Cloud + Docker
前端
三件套+V3+Element-Plus
通过逆向工程方式构建项目
架构篇
Elasticsearch 弹性搜索
网关
远程调用
链路追踪
缓存
Session同步方案
全文检索
异步编排
线程池
压力测试与性能优化
调优
Redis
CI/CD
集群篇
k8s集群
Devops
……
细说各个技术栈
配置linux虚拟环境
virtualBox是虚拟机
允许在一台计算机上创建和运行虚拟机
虚拟机:模拟完整的计算环境,包括操作系统,应用程序等
vagrant是自动配置操作系统的代理软件
是一个用于构建和维护虚拟化开发环境的工具
用它可以加载某个操作系统的镜像,相当于直接配置好操作系统
镜像:Image:指一个预先配置好的操作系统或软件环境的副本。镜像包含了操作系统,应用程序,设置,配置,数据等,是一个虚拟机的完整快照。
CentOS是基于Linux的操作系统
Docker written by golang
Docker用于将应用程序和其依赖打包成一个独立的、轻量级的容器,以便在任何环境中运行。
如果打包有等级:Maven<Tomcat<Docker
Maven只打包和管理依赖jar包
Tomcat打包和管理到各种应用程序Servlet和JSP,但却是专门为Java Web程序设计的——即只能运行javaWeb应用。而且Tomcat的运行依赖本地JVM
Docker容器目的就是实现应用程序的可移植性,环境隔离性。
解决的问题——代码水土不服——由于开发/测试/生产环境不同导致代码运行结果不同。
水土不服——软件跨环境迁移的问题
解决方法——打包水土——将程序和环境绑定,用一个容器进行管理——Docker
另外:Docker容器化也是对内部程序的二次封装,统一原本每个应用程序不同的命令:例如mysql.serve start统一成docker run;mysqld统一成 docker exec…
容器化与云原生:
伏笔
虚拟化与容器化:
为什么需要虚拟化 / 容器化?
当企业的三个应用程序功耗不大,可以考虑运行在单台物理机上时;如果当三个应用程序的底层依赖的版本有冲突(依赖没有隔离)例如:一个是java 8,一个是Java 17,就会导致物理机底层依赖比较难以管理 ; 更严重的情况是:比Java、MySQL等更加底层的依赖配置起冲突,例如
如果是更底层的运行库/库版本冲突,例如某个应用需要glibc,另一个应用需要uclibc,这就无解了
所以虚拟化技术并不是单纯的开一个新的操作系统,而是说开一个原本操作系统无法提供的环境
Spring Cloud
Spring Cloud是一个用于构建分布式系统和微服务架构的框架。
提供了一组工具和组件,用于简化分布式系统开发的复杂性和解决微服务架构中一些常见问题。
Spring Cloud下有许多子项目,每个子项目都专注于不同的分布式系统和微服务开发问题。LIsted below
ElasticSearch:
分布式搜索和分析引擎,能够快速地搜索大量数据,并支持实时查询和分析。用于实现全文搜索,日志分析,数据可视化等功能。
Sentinel:
是一个分布式系统的流量控制和熔断器库。旨在提供一种简单,文档,可到的流量控制手段,以保护微服务架构中的应用程序免受流量过载、雪崩效应和故障的影响。
Nacos:
是一个开源系统,支持动态服务发现,配置管理和命名空间管理,服务员数据等功能。总体而言是用于构建和管理 微服务架构 和 分布式系统 的平台。
zipkin:
是一个分布式系统的追踪系统。用于收集、存储、查看分布式系统中各个服务之间的调用链路和性能数据。
在微服务架构中,一个业务请求往往会经过多个不同的服务进行处理。每个服务可能又依赖其他的服务,形成一个复杂的调用链路。当系统中出现问题或性能瓶颈时,追踪调用链路和性能数据对于定位问题和优化性能非常重要。
概念:
微服务:
是一种架构风格:拒绝从前的大型单体应用,因为在大型应用中一个小项目不可用会影响全局,一个解耦的思想就是在一堆提供相同服务的项目服务中选择能用的即可——这就要求大型项目分离成微服务。微服务架构风格将大型单体应用根据其业务边界进行服务微化拆分,各个服务独立部署运行。
分层所面临的问题就是需要传递——推荐http。
集群 & 分布式 & 节点:
集群:物理形态——只要是一堆机器,就可以叫集群,无论他们是不是干同一件事。
集群往往是解决一台机器容易宕机、数据备份等问题。
分布式:工作方式——将不同的业务分布在不同的地方,其中每个业务根据需要配集群。
节点:分布式中每个业务的服务器
远程调用 RPC
在分布式系统中,各个服务可能处于不同主机,但服务之间需要相互调用/传递信息。称之为远程调用。
Spring Cloud 使用HTTP + JSON的方式完成远程调用。
项目中 不同微服务之间通过OpenFeign,主微服务向目的微服务发送http请求,完成远程的方法调用
负载均衡
根据不同的负载均衡算法,将对一个服务的调用平衡到集群的各个服务器上。为了使每一个服务器都不要太忙或者太闲。
常见算法:
服务注册/发现 & 注册中心
一个服务内的集群是多台服务器,管理这些服务器哪台上线了,哪台下线了的一个应用就是注册/发现中心。对B服务的访问需要经过对B的注册/发现中心,找到被发现可用的一个服务器。
配置中心
一个服务内的集群是多台服务器,统一管理这些服务器的配置——配置中心。
服务熔断 & 服务降级
由于微服务之间是用http协议相互调度的,这就不可避免在传输中出现错误。
如果服务A对B的调度没有及时响应,而对A的调度一直在进行,导致A的服务器很容易压力过大导致宕机
服务熔断:
如果被调度的服务经常失败(达到某一阈值),开启断路保护机制——后来的请求不再去调度这个服务,而是由本地直接返回默认的数据。
如果只针对超时的请求,该怎么区分是因为传输中不可抗力导致还是被调度端异常导致——最终还是需要识别出多次调度失败的服务
服务降级:
在运维期间,当系统处于高峰期时资源紧张。可以让非核心业务降级运行——即某些不处理/简单处理(抛异常,前端缓存,返回Null,调用Mock数据(模拟数据),调用Fallback处理逻辑(备用方案))
API网关:API Gateway
(过滤器/拦截器/守卫)
目的是抽象出微服务中都需要的某些功能,某些需要在客户端请求到达服务前进行的功能——过滤/认证,重定向,负载均衡…
Docker
- Docker架构
镜像(Image):相当于一个root文件系统(从Docker创建的内部的系统就源于此文件)
每个image包含一个应用程序所需的所有代码、库、依赖项和配置。将其打包成一个可运行的、只读的模板。
容器(Container):容器就是镜像的实例化对象。镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停…
仓库(Respository):代码控制中心,用来保存镜像(Docker hub外国中心仓库,private registry私服)
Docker 命令
关于Docker daemon守护线程的
启动dockers服务
systemctl start docker
停止docker服务
systemctl stop docker
重启docker服务
sysytemctl restart docker
查看docker服务状态
systemctl status docker
设置开机自启docker
systemctl enable docker
关于Docker镜像的(image文件/各个软件)
查看本地所有镜像
docker images
docker images -q #查看所有镜像的id
搜索镜像
docker search 镜像名称
拉取镜像:从仓库下来镜像到本地,镜像名称格式:name:版本
docker pull 镜像名称
删除镜像:删除本地镜像
docker rmi 镜像id #删除指定本地镜像(remove image)
docker rmi `docker images -q` #删除所有本地镜像 (将查看id结果作为参数)
关于Docker容器的
查看容器
docker ps #查看正在运行的容器
docker ps -a #查看所有容器
创建并启动容器
docker run 参数 创建并启动容器
参数说明:
-i 保存容器运行。
-t 为容器重新分配一个伪输入终端。
-d 以守护(后台)模式运行容器。创建一个容器在后台运行
再进入:docker exec -it name /bin/bash
/bin/bash相当于进入容器的初始化操作(必要的)
通常:-it:创建交互式容器 -id:创建守护式容器
用-it就直接进入到容器,用id则还在上一层。
–name:为创建的容器命名
进入容器
docker exec 参数 #退出容器,容器不会关闭
停止容器
docker stop 容器名称(id)(stop后状态是exited)
启动容器
docker start 容器名称(id)
删除容器
docker rm 容器名称(id)
查看容器信息
docker inspect 容器名称(id)
容器自启动(随着虚拟机启动)
docker自启动已经配置过了,就是随着虚拟机自启动
一般过程是:虚拟机启动/docker启动,用户连接vagrant,用户切换到root
数据卷
解决的问题:
Docker 中的Container容器删除后,在容器中的内容也会随之销毁(需要备份)
Docker 中的Container容器和外部机器无法直接交换文件(没配虚拟ip时)(需要Docker容器中介)
Docker 容器中的Container容器怎么进行数据交互(或许需要中介)
数据卷
数据卷是宿主机的一个目录或文件
当容器目录和数据卷目录绑定后,对方的修改会立刻同步
一个数据卷可以被多个容器同时挂载,一个容器也可以挂载多个数据卷
挂载:往往是说容器内的文件挂载到外部文件,从而通过外部读写内部。
挂载的常见对象:配置、日志、数据文件
配置数据卷:(文件映射)
语法:
在创建容器时,使用 -v 参数 设置数据卷
其中,前面是在外层目录中建立的目录
后面是在Container容器中建立的目录
Attention:
目录必须是绝对路径
如果目录不存在,会自动创建
一个容器可以挂载多个数据卷
基本Linux语法(文件):
touch itheima.txt ==> 创建文件
echo i love mazda > itheima.txt ==> 向此文件中写入数据
cat itheima.txt ==> 读取此文件
基本Linux文件层次:
[root@localhost /] 斜杠/表示根目录:根目录是整个文件系统的起点,所有目录和文件的根节点。根目录下一级是许多重要的大目录:
而在home下就拥有每个用户的一个独立的子目录,用于存储用户个人文件和配置信息。
root似乎没有在home下的文件夹
[root@localhost ~]波纹~表示用户主目录
数据卷容器
主要思想:创建一个容器,这个容器和一个数据卷进行了挂载。然后又创建了许多容器,和这个数据卷容器进行了绑定,实际效果就是后面的每个容器和数据卷进行了绑定。
有点像定义了一个接口。
步骤:
docker run -it –name=c3 -v /volumn centos:7 /bin/bash
创建启动c3数据卷容器,使用-v参数,/volumn表示在c3中所挂载到数据卷的文件
在数据卷中自动创建了一个文件用于挂载,似乎并不关心在数据卷中的文件,所以名字很随意。
创建启动c1,c2容器,使用–volumes-from 参数,设置数据卷
docker run -it –name=c1 –volumnes-from c3 centos:7 /bin/bash
docker run -it –name=c2 –volumnes-from c3 centos:7 /bin/bash
之后c1,c2中都有volumn文件,看起来是c3主导了。
注意的Bug
用vi进入txt写入文件后,先进行ctrl+c结束写入,再:wq保存退出
Docker 应用部署
宿主机和虚拟机
二者概念相辅相成
二者都是在虚拟化技术环境下的概念
宿主机是承载虚拟机的计算机,虚拟机是在宿主机上模拟的独立操作系统环境。
宿主机要运行虚拟机,就要为其管理和分配物理资源(处理器,内存,存储…)
虚拟机在宿主机上的虚拟环境运行自己的操作系统和应用程序——感觉像是独立的计算机一样。
部署面临第一个问题——外界无法直接访问到虚拟机内的某一容器
但外界可以通过访问宿主机,宿主机再访问容器
不过由宿主机到容器的过程没有特殊逻辑,需要实现配置——端口映射
端口映射:
docker run -d -p 3306:3306 -v $PWD/conf:/etc/mysql/conf.d -v $PWD/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 –name mysql mysql:5.7
bug简述:
Docker logs -f Container_id 可以查看具体某个容器的日志,某些不显示报错的报错会在日志中报出。
关于没有空间
du -sh *
查看当前目录下各个子目录/文件夹大小
disk usage -summarize human-readable
df -h
显示文件系统的词频空间使用情况。是整个分区的概念,磁盘分区无法进入,它是存储在文件系统的一个设备节点,用于挂载到文件系统树的特定路径上。
据说:某个包(vagrant)的存储过大,是因为vagrant挂载了vagrantFile所在文件夹(win下)的所有文件,只要把vagrantFile换到新建文件夹即可
但我还是直接找到不能再往下走的大文件,将其删除
关于/var/lib/下没有mysql文件
本应没有
重装镜像,装5.7
谷粒商城架构图
大致流程说明:
左侧外网:用户访问到Nginx服务器,服务器访问内网的网关,网关进行路由/认证/限流等操作后调用业务集群中的各种微服务功能(正中间)。
核心微服务的功能实现需要Redis做缓存,MySQL做持久化数据库,RabbitMQ做消息队列,ElasticSearch做全文检索。将需要存储的文件云存储到阿里云OSS(中下方)。
对于核心微服务的管理,通过Nacos注册中心/配置中心管理。微服务的调用链如果有某处出问题,调用Spring Cloud的业务追踪模块,外加许多其他功能完成错误提示(上方)。
开发人员将代码托管到Github,OP运维人员用自动化工具Jenkins从Github拉取代码后,打包成Docker镜像,再用K8s集成服务,以Docker容器来进行(最下方)。
开发环境搭建
要求:
注意Java版本
注意maven版
git
git
git是一个分布式版本控制系统,内容管理系统,工作管理系统
用于跟踪代码和文件的变化,协助多人协作开发,并管理项目的不同版本
具体作用:
跨区域多人协同开发
追踪和记载文件的历史记录
组织和保护源代码和文档
统计工作量
并行开发,提高效率
追踪记录整个软件开发过程
降低人为错误
常见的版本控制软件/系统
Git
SVN(subversion)
CVS(Concurrent Version System)
Visual Studio Online
git将项目代码存储在仓库 repository ,并记录每个文件的历史修改记录
Git具有分布式特性——开发者可以在不同分支上同时工作,然后将分支合并到主要代码库。
分支:从主分支(master/main)分离出来的代码路径,可以并行开发不同功能
提交:Commit 记录仓库的一个特定状态
合并:Merge 将一个分支的更改合并到另一分支中
拉取:Pull 从远程仓库获取更新。
推送:Push 将本地的更改推送到远程仓库,使其他人看得到修改
- Git客户端负责代码提交、合并…以上功能,Github可以当做Git的可视化+web通信工具。
版本控制分类
本地版本控制:
个人在本地计算机记录文件/软件的多个版本
集中版本控制: 代表:SVN
企业在中央服务器中存有一版,开发者基于中央服务器的某一历史版本进行开发,开发结果合并回中央服务器中。
缺点:没有实现热部署——开发虽是并行的,但并不协同。
分布式版本控制: 代表:Git
所有开发人员拥有中央服务器的同一版本所有代码,基于此开发后再提交会中央服务器进行更新
相较于集中版本控制,分布式中将所有代码进行备份。且备份的代码在所有开发人员中同步更新(热部署)。
Git
工具:
Git Bash:Unix和Linux风格的命令行
Git CMD:windows风格的命令行
windows的命令行和Linux的命令行语法上略有不同——like cls / clear
Git GUI:图形界面的Git
常用命令:
Git理论:
Git三个工作区域+远程仓库
工作区:working 平时存放代码
暂存区:index 用于临时存放改动
资源区/仓库区/本地仓库:head 安全存放数据,里面有所有版本的数据
又有说法表示head指向最后一次提交的结果
远程仓库:托管代码的服务器
分支:
用来将特性开发绝缘开的。
创建仓库时,master是默认分支。在其他分支上进行开发,完成后需要合并到主分支上
创建一个叫feature_x的分支并切换过去
git checkout -b feature_x
切换回主分支
git checkout master
删除新建的分支
git branch -d feature_x
建立的分支需要被推送到远端仓库,不如该分支就不为他人所见
推:git push origin
Git常用操作:
初始化:
创建项目:
Git bash中:git init
空项目结构:
克隆/复制项目:
Git bash中:git clone url
如果是在远端服务器上的仓库:
git clone username@host:/path/to/repository
项目结构
提交操作
git add <filename>
git add *
将代码添加到暂存区
git commit -m “提交代码的描述信息”
将代码提交到HEAD
git push origin master
将在head的改动提交到远端仓库
master可以替换成要推送的任何分支
更新与合并
更新本地仓库,同步最新改动
git pull
在当下的工作目录中获取并合并远端的改动;或要合并其他分支到当前分支
git merge
在这两种情况下,git 都会尝试去自动合并改动。遗憾的是,这可能并非每次都成功,并可能出现冲突(conflicts)。 这时候就需要你修改这些文件来手动合并这些冲突(conflicts)。
执行命令标记合并成功
git add
合并改动之前,可以使用如下命令预览差异
git diff
为版本添加标签
IDEA中集成
Git文件操作
文件的四种状态:
- Untracked:未跟踪,表示文件还处于个人编写状态,虽在文件夹中,但没有加入Git库,不参与版本控制。
通过git add 将状态变为Staged
Unmodify:文件已经入库,未修改。表示版本库中的文件内容和文件夹里完全一致。这种类型的文件有两种去处——被修改则变为Modified,如果使用git rm移出版本库,则成为untracked文件
Modified:文件已修改,但还没有其他操作。这种文件也有两个去处——通过git add 进入暂存区staged,或通过git checkout 丢弃(git checkout表示从库中取出文件,覆盖此文件)
Staged:暂存状态,执行git commit后将修改同步到库中,这是库中的文件和本地文件变为一致,文件变为unmodify;执行git reset head filename 取消暂存,文件状态为Modified
gitignore
免密登录:
从本地仓库提交到gitee远程仓库时,避免每次输入密码。
根据本地的git的信息生成公钥私钥——唯一识别当前git(git内保存有global的账户和密码)——就可以免密登录到gitee本人远程仓库。
使用SSH协议:用于在不安全的网络上安全地进行远程访问,文件传输和命令执行。
SSH中使用公钥私钥模式:
公钥和私钥文件: SSH使用公钥加密来确保通信的安全性。在用户使用SSH连接到远程服务器时,他们需要一对密钥:一个私钥保留在用户计算机上,另一个公钥嵌入在远程服务器的授权文件中。公钥和私钥通常存储在用户的主目录下的
.ssh
文件夹中,分别是id_rsa
(私钥)和id_rsa.pub
(公钥)。大致:我的本地保存私钥,公钥发到gitee。身份认证时我发送加密后的信息,到达gitee用私钥解密。
开源许可证:各种协议限制此项目对外的开源程度
是否可以随意转载/是否能够商业使用…
IDEA集成Git
idea新建项目绑定Git
创建时确定URL
新项目中复制来.git文件,即可将新项目和git账户绑定
因为.git是在git init时产生,并没有保留项目的任何信息,只是指向个人身份的gitee
谷粒项目构建
后台管理系统
前端用开源项目——人人开源
后端
powerDesign 设计数据库
基础crud代码由开源项目生成(从数据库逆向)
系统项目结构:
基础crud代码结构
项目亮点:
代码生成面临缺失依赖问题:
由于五个子模块都缺失类似的依赖,所以能抽离出依赖部分最好——于是用一个子模块包含需要的依赖,再导入此子模块作为依赖。
分布式-微服务
老版本选择Spring Cloud
注册中心:管理各个微服务
Spring Cloud Netflix 中的 Eureka
配置中心:管理各个微服务的配置信息
Spring Cloud Config
网关:拦截!过滤!
Spring Cloud Netflix 中的 Zuul
短路保护:
Spring Cloud Netflix 中的 Hystrix
Spring Cloud Alibaba
提供微服务开发的一站式解决方案,包含更强的分布式应用的必要组件
更强:封装到只需要添加一些注解和少量配置。
新版本选择Spring Cloud Alibaba
注册中心/配置中心
Nacos
服务容器(限流、降级、熔断)
Sentinal
分布式事务解决方案
Seata
任用Spring Cloud
负载均衡
Ribbon
声明式HTTP客户端(调用远程服务)
OpenFeign
API网关(webflux编程模式)
Gateway(比Zuul更强)
调用链监控
Sleuth
by the way:记录bug
git拉取代码的时候报错:
解决方法:
解开代理
1
2git config --global --unset http.proxy
git config --global --unset https.proxy清理DNS缓存
cmd下:
1
ipconfig/flushdns
技术栈
Nacos
用于管理注册中心和配置中心
步骤:
启动Nacos服务器——可以运行在本地/Linux
Java代码中:
将Nacos依赖导入微服务的pom文件——将此微服务放入注册中心,并能检测/发现其他微服务。(统一放入common)
给此微服务写配置信息(目标nacos服务器的位置)
必要的注解(前两步都是配置资源可用,这一步是将微服务绑进注册中心)
bug记录:卡最长时间的bug
原委:nacos服务器启动正常,java的微服务配置没有报错,但jdk,spring-boot,spring-cloud-alibaba都是用的高版本,此时nacos无法发现微服务
答案:果然是版本匹配的问题
原先老哥怀疑是JDK版本过高,却没有怀疑Spring-boot,Spring-cloud,Spring-cloud-alibaba虽都是高版本但能否匹配。(jdk过高可能影响微服务,也可能影响nacos/底层用java执行,依赖jdk)
最后的答案是:从github官网上找到了版本匹配说明(真难找——在wiki中)
关于版本的第二个问题
原委:想把开源项目renren-fast也加入nacos,但开源项目的版本较落后且不易更改
答案:按照低版本重新引入相关依赖,而非按照common统一引入
配置中心
目的:
当下的问题是:每次更改某些配置,需要改Java微服务的配置文件,并重启微服务。重启就会导致项目阻塞甚至报错,于是想要有类似热部署的功能。
nacos配置中心就可以将nacos中的数据实时更新到微服务的运行内存中。并没有绑定某个文件(热部署),而是覆盖
步骤:
引入依赖 nacos-config
原本配置文件中,添加nacos-config配置中心的ip+端口地址
原本配置文件中,给项目绑定配置中心的具体某一配置文件
1
2
3
4
5
6
7
8
9cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
config:
import:
- nacos:example.properties?refresh=true项目中可以动态获取配置
@value(“${配置项名}”)
@ConfigurationProperation()
在Controller中添加注解 @RefreshScope 表示每次访问时自动更新配置(将配置中心的配置刷新到项目中)
据说:如果配置中心和当前应用的配置文件都配置了相同的项,优先用配置文件的。
- 命名空间
应用场景:
环境隔离:对于开发、测试、上线需要不同的配置。而不可能为这些配置中同类的信息取不同的名,于是就将其大分类
环境隔离:对于不同微服务,分开不同配置也合理
没有命名空间时,默认都在public保留空间中
步骤:
配置文件明确命名空间(类似uuid,由nacos创建的空间确定)
配置集:
配置集合
服务器预加载时是根据确定配置集ID加载配置的。
配置集ID:DataID
- nacos服务器可以乱起,后端配置文件可以乱起,但后端需要找得到nacos的文件
- 配置分组
配置分组>命名空间>配置集 ===> 配置分组就是将一系列配置组合成一组,可以同时存在多组
默认:DEFAULT_GROUP
应用场景:
淘宝618,双11,平时,可能需要不同的配置,且是更改全局的大配置。
此项目的配置的配置:每个微服务创建自己的命名空间,使用配置分组区分环境:dev开发环境,test测试环境,prod生产环境
写法:
由于Spring Boot版本不同,还没查到高版本3.0的写法,于是下面是自己测出来的
一个微服务必须在外面写清楚某些东西(不能导入)
name
nacos服务器的ip地址
配置的group
配置的命名空间
在此范围内,其实限制了许多,只能导入同一group同一命名空间的一些配置集。
参数也可以跟到导入的后面,似乎group也可以,但没搞好
Feign
用于微服务之间远程调用的声明式API(封装HTTP)
步骤:
项目引入open-feign,starter.balancer依赖
当下微服务下编写接口(声明将用Feign访问目标微服务的Mapper)
具体API:需要辨明要访问的是哪个微服务,哪个Controller的哪个方法
具体而言:写明微服务的name,http的requestMapping的全路径
开启远程调用功能——将以上配置的资源伴随启动加入到内存中。
在此,Feign对象/接口对象,和原本Spring bean对象一致,但不同在于
Feign对象
参数更多,底层实现方法略复杂,但对上层而言还仅仅是个bean
-
和初始化bean对象的模式不同,需要额外指定一个注解+包扫描范围
Gateway
作用:网关——作为流量的入口,常用功能包括:路由转发、权限校验、限流控制等。
之前用zuul,但被Gateway淘汰。
三大组成部分:
route 路由:
决定前往哪里
参数id和uri
predicate 断言:
决定哪些流量需要走此路由(作用类似urlPattern)
参数繁多,且是多方位的
局部举例:
后置断言路由
接收一个参数,datetime类型/ZonedDateTime类型,此断言匹配指定日期之后发生的请求。
前置断言路由
夹逼——用Between表示两个时间之间
Cookie断言路由
接收两个参数,cookie的名称和regexp(Java的正则)
header断言路由
判断请求头是否含有某类信息
由主机host,method,path,Query查参数,IP地址
Filter 过滤器:
决定流量进入退出时的钩子函数
和vue路由守卫、httpServlet的Filter略有不同,那里对路由到哪里并不关心,因为在进入过滤器前就明确了,于是用next或者doFilter传递,但这里由于环境是分布式集群,于是更关心具体路由到哪个服务器
Java8新特性
lambda、stream、
lambda
缘由:
简化定义接口的实现类的代码,但只能简化只有一个抽象方法的接口
写法:
class classname = () -> {重写方法的具体代码}
例如:
代码中,lambda只关心最重要的方法重写的逻辑,而忽略方法定义、接口多态等已知信息。
由lambda函数引入概念
函数式编程
在JS中广泛应用
将数据的变化过程进行抽象
函数可以存储在变量中
函数可作为参数
函数可作为返回值
函数是第一等公民(之一)
传统OOP中,变量,参数,返回值都是对象(基本数据类型也可以视为对象),所以对象是第一等公民,但函数式编程中,函数不属于对象也可以传递。
传递示例:
方法接收一个Factory类型的参数
Factory可用lambda表达式表示为函数
调用方法如下:
第二种更是一次性、简上又简
返回值示例:
语法简写
- 函数内只有一行时:省略{}和return(不论唯一的一行是否是return)
思路:能省就省,能推断就推断
由于有接口做模板限制,实现类的可变位置其实只有参数名和方法逻辑。
函数式接口
是使用lambda的前提
指有且只有一个抽象方法的接口,其他静态方法可以有
通常用@FunctionalInterface注解声明
常见函数式接口
Predicate 断言
唯一方法:test(T t)返回bool值,表示验证 t 是否符合test定义的逻辑
by the way:
接口中的default方法:作用就是让实现类可以调用已经写好的逻辑
但问题就是:类可以实现多个接口,就容易导致default方法重名,此时需要重写重名方法。
Function 表示功能
泛型内两个参数:<返回值类型,参数类型>
完成符合泛型规范的功能
方法引用
当lambda表达式所要完成的业务逻辑已经存在,则直接引用对应的方法
语法:
- 某对象::方法名
例:
1 |
|
具体语法
静态方法(类方法)、构造方法,直接通过类名调用,普通方法(成员方法)通过对象调用
可以被引用的方法是有限制的:(要求被引用的方法格式符合lambda中重写方法的格式)
参数数量相同,对应位置类型兼容
返回值类型兼容
在函数式接口中如果是void,具体实现类返回类型任意
在函数式接口中如果有限制(返回int、User…),具体实现类只能返回相应的
以上兼容是指函数式接口的抽象方法,和被引用的某个方法之间
例:
- 引用静态方法
- 引用构造方法
编译器会根据当前位置传入什么参数去自动寻找合适的构造方法
- 引用普通方法/成员方法
由于方法引用只明确方法名,所以对重载的方法进行选择是编译器自动完成的,就也是根据参数和返回值找到合适的方法
Stream流
是Collection顶级父类接口中的defualt方法,目的就是简化容器操作,提高多处理器的性能
关注”做什么“,而非”怎么做“
感觉就是用流替代for遍历
使用步骤:
获取流 —— 中间操作 —— 终结操作
对Stream流的操作分为中间操作和终结操作
中间操作:返回的任是流对象
终结操作:没有返回值
短路操作:也是终结操作,只是一旦条件命中就停止,应用于对流处理时间有限制的场景。
真实执行
lazy延迟执行
据说在对流的多次聚合时,中间操作并非按顺序执行,而是先缓存到某个队列中,直到碰到终结操作,才开始执行——给了并行的可能——or say 中间操作先后其实无影响。
底层并行
用 Fork/Join 分治处理
本质上是回不去的
加并行操作并非是在流跑完一般后再跑一遍,而是加入并行队列分治跑。
而一旦对流对象操作到最后(也就是终结操作完后),流对象就不能再进行任何操作了
API之中间操作
API之终结操作
API之收集操作
语法:stream.collect(收集器)
收集器明确将流的每个实体归纳成什么形式
by the way
参数Function类型就是典型用lambda写函数式接口的。
商品服务
三级分类
业务目标:
数据库中要存储每个分类及其所属关系
后端需要做出符合层次关系的数据类型
数据库:
存储的每行表示某层的一个分类
构建理论:
用cat_id和层级确定此行在分类树的具体位置
用parent_id确定上下层关系——树型结构
如此一来,在确定了最上层的分类后,表中父id是最上层的就根据父安排在第二层,第三层同理,那感觉层级字段非必要,可能是为了查找性能考虑吧
查找代码逻辑:
如此一来,如果想要找某个顶级类的所有子类的分组,就是遍历底层用父id匹配出来。
代码实现思路:
流处理+过滤器+lambda滤出最上层每个实体,对最上层每个实例设置其下层子实体。
具体怎么设置:
流处理+过滤器+lambda滤出第二层、父id是对应实体的下层实体
同时由于整个分类有三层,还要考虑用递归再来一遍
得到的流中是设置了子类的顶级大类们,再用sorted+lambda按照某顺序排序
最后将流转化成列表
流处理:完成过滤(Filter)+集合内实体分别操作(map)+排序(sorted)
解决前端发8080需要分配到后台各个端口的微服务的问题
由于前后端分离开发,且暂时不考虑完美的API文档。前端的所有xhr请求都默认发送给后台8080端口,但后台的设计是:8080只负责基础框架和crud,其他业务相关的代码按照微服务的方式分配到各个子模块——且各个微服务占有不同端口,统一注册到Nacos注册中心进行管理。
解决方法是:后台需要一个分流器将不同需求分流到不同微服务
Gateway可以拦下所有请求
Gateway配合Nacos可以将请求送到Nacos注册中心的某个微服务上
前端需要将默认发送方式从8080改为到网关
网关(Gateway)
uri:Uniform Resource Identifier 统一资源标识符
用来表示互联网上资源的字符串序列
uri包括两种形式:URL URN
URL:统一资源定位符:
表示一个资源的完整地址,协议包含:http,https,ftp,
例如,Example Domain 是一个URL。
URN : 统一资源名称:
重点在于为资源分配一个唯一的名称,而不关心资源的具体位置。
例如,urn:isbn:0451450523 是一个URN,它标识了一个书籍的国际标准书号(ISBN)。
网关的负载均衡
网关通过将请求转发到注册中心的同种微服务的不同个体上,实现负载均衡
大致流程:
网关在后台配置了路由到的微服务
lb://serviceName
当网关受到请求,首先得到注册中心关于此服务的所有实例信息。
根据指定的负载均衡算法(如轮询、随机、权重等),从这些实例中选择一个作为目标。
请求转发:
Gateway 将接收到的请求转发到所选的目标实例,这个实例可能是多个同名服务实例中的一个。这个转发过程是透明的,客户端无需关心具体的目标实例.
透明:暂不明是通过Feign还是原生http还是其他内部通信协议…
记录关于浏览器缓存Ajax的get方法结果
当多次用get+相同的url访问资源
服务器返回的响应信息中包含
Cache-Control
、Expires
、Last-Modified
等信息,表示能否进行缓存
破解浏览器缓存:
在URL中添加唯一参数
默认的一个参数 t 获取当前时间戳
将 t 和传入参数拼接,一并传入
设置缓存头部信息
服务器可以在响应头部设置
Cache-Control: no-cache
和Expires: 0
等头部信息,告诉浏览器不要缓存响应。用POST据说不会被缓存。
因为它们被认为是非幂等的,即可能对服务器状态产生影响,因此浏览器通常不会缓存 POST 请求的响应。
关于拖拽
简记业务逻辑
业务逻辑都是基于Element封装好的API:
拖动的视觉动画
钩子们/由MVVM监听的事件
allow-drop是在每次按住鼠标拖动时进行判断,这种封装很上层,但问题也就在于太上层了——如果我要在松开鼠标的时候判断,就做不到
向上提供的API
需求分析
拖拽时需要判断能否拖拽
项目的思路是总层数不能超过三层
获取正在拖动的标签的最大层数,以及目的地的父节点的层数,相加后是否超过三层
而如何获取最大层数?
Element提供的钩子给的参数是当前的节点,于是想要挨个遍历每个子节点,只好dfs深度遍历了。
拖拽后对后端的影响(对表的影响)
影响 parentId 父节点
影响 catLevel 当前节点层级
影响 sort 节点们的排序
拖拽获取信息的具体实现(全靠API提供)
核心在于真正理解Element提供的参数
三个参数分别是:
正在拖动的节点
拖动后较近的节点(目标节点)
拖动节点和目标节点的关系
钩子方法:指前端执行到某个阶段自动调用的方法,所以所有参数都是系统传入
————>
然后就取决于如何解构Element传递的参数
额外业务:
关于拖动排序
对于拖入inner,直接将节点合并到子节点,重排。前端没有必要的顺序,也不必调整库
对于拖入after,before,后端需要保存拖动后的顺序 —— 在表中只好将拖动后同级节点全部发送给后端,后端将新排好的数据存入表中。
关于拖动多次后一并提交到后端
场景分析:每次拖动都会请求后台,后台再请求数据库,对性能消耗略高
业务分析:
由于拖拽对后台/表的影响无非是更新一张产品表内多行数据,前端缓存中如果有重复修改也可按照最新数据更改。
问题在于:缓存的部分没有及时更新到库,导致对接下来的判断没有产生影响。
是缓存的通病————被缓存的数据无法生效
答案:前端存储更多的数据 —— 对于每个参与变化的节点,实时更新其层级。
简记后端
前端传递的数据是:由于拖动会改变多了Category行,所以前端决定返回所有移动的行以及目的地。
后端用Category[]接收,SpringMVC即可自动投影成一系列Category对象。
之后分别update or 调用封装好的 updateBatch 全部更改即可
關於文件上傳OSS雲服務器
傳統單服務器項目,文件跟隨請求到達服務器,即保存在服務器。
但在分佈式項目中,由於瀏覽器每次的請求會被隨機路由到不同的服務器上,導致可能找不到上次傳輸的文件。
版本答案是:提另準備一臺服務器用來存儲文件。將文件上傳到統一的服務器
AOP?– 抽象出相同的部分統一操作。?高内聚?
專用文件存儲服務器的模式
自建!:
FastDFS(Fast Distributed File System)是一个开源的分布式文件系统,旨在提供高性能、高可用性的文件存储解决方案。它是为了解决大规模文件存储和访问的问题而设计的。FastDFS 主要用于存储大量的媒体文件(如图片、音频、视频等)以及其他类型的文件。
思想似乎是有一個管理系統調度/記錄分佈式的服務器和文件的對應關係
vsftpd(Very Secure FTP Daemon)是一个开源的FTP 服务器软件,旨在提供一个安全、稳定、高性能的文件传输协议(FTP)服务器。它是在类 Unix 操作系统上运行的,如 Linux、FreeBSD 等,并且被广泛用于搭建 FTP 服务器,以方便文件的上传、下载和管理。
雲存儲:
分布式文件系统
命运的齿轮从此转动
FastDFS是一个开源的分布式文件系统,实现的功能有:文件存储,文件同步,文件访问(上传、下载)
核心性能:大容量存储和负载均衡问题
机制:冗余备份,负载均衡,线性扩容…
阿里雲OSS搭建雲存儲
資源術語:
存儲空間 Bucket
劃分出存儲文件的一個基本單元,對此基本單元可以設置相關存儲配置
對象/文件 Object
對象是OSS存儲數據的基本單元,對象由存儲空間内的唯一的key來標識
地域 Region
表示OSS的數據中心所在物理位置(各個的費用不同)
訪問域名 Endpoint
表示OSS對外服務的訪問域名,OSS以HTTP RESTful的形式對外提供服務。
據説訪問不同地域所需的域名不同
訪問密鑰(yao) AccessKey
訪問身份驗證中用到的AccessKeyId和AccessKeySecret
類似Gitee的密鑰
上傳模式
簡單思路
前端提交multiPart的文件,後端java程序接受,獲取文件流后寫入OSS服務器
好處:後端是必經之路,可以審核、修改、鑒權…而且後端是唯一有權利上傳的,密鑰只保存在後端即可
壞處:後端壓力太大
简述实现:
Java后台引入依赖OSS,配置对目标的识别+确认信息:
OSS 服务器的对外访问端口
bucket的name
密钥id
密钥密码
依赖为容器自动注入OssClient对象,调用其流式传输方法即可完成上传。
地道思路
服務器簽名后直傳
用戶向服務器獲取加密後的密鑰,添加在文件某處,直接傳輸給OSS
感覺和Gitee的區別在於,Gitee是自己上傳自己的,無需證明自己的身份,而OSS系統則需要驗證用戶能否上傳,於是還得後端插一脚
关于文件上传OSS
前端收集到文件/表单信息后,对文件/表单进行初步校验。
校验很不简单——构造rules,写validator…
前端在之前需要先获取到后台的认证标签,用于在向OSS传输文件时标识身份
关于前端校验
前端数据校验的目的有:减少无效的网络请求、提高用户体验(更快返回校验错误)…
但由于前端校验容易被绕过,所以后端校验才是最终的屏障
可能的校验技术:
关于后台校验
确保数据在进入数据库或进行后续处理之前的合法和安全。校验的对象不止需求手册上来自前端的潜在异常输入,也有绕过前端的其他输入。
Hibernate Validator 是实现了 Java Bean Validation(JSR 303)规范的一个库。因此,它实际上是 JSR 303 的一个具体实现。
JSR303:Java规范请求(Java Specification Request)定义了java中对Bean的验证规范。
此包下含标识各种规范的注解
后台的校验逻辑:
当通过RequestBody接收到实体对象,会按照实体类中,属性上声明的校验注解来验证。
需要对需要校验的接收对象标注 @Valid 标签
后台验证不过的请求:
400:服务器不理解请求
bad request 错误的请求——参数错误没法接受–>校验不过
invalid hastname 域名不存在
400还未执行到controller就停了
统一异常处理:
本质是封装原生异常,封装成更符合业务的异常
简单异常处理:
用@Valid声明的RequestBody,通过就近声明BindingResult可以获取到JSON投影对象的结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16@RequestMapping("/Demo")
public R Demo(@Valid @RequestBody CategoryEntity category, BindingResult result) {
if (result.hasErrors()) {
HashMap<String, String> objectObjectHashMap = new HashMap<>();
List<FieldError> fieldErrors = result.getFieldErrors();
fieldErrors.stream().forEach((item) -> {
String defaultMessage = item.getDefaultMessage();
objectObjectHashMap.put(item.getField(), defaultMessage);
});
System.out.println("YES");
return R.error(400, "测试用例1").put("data",objectObjectHashMap);
} else {
return R.ok("我们意念合一!");
}
}声明了BindingResult后——@Valid接收错误报异常的情况就被BindingResult屏蔽了
声明了一个形参——重写了全部异常处理
validate 自定义规则
通过写正则
统一异常处理:
由于假如对每个Controller的方法都用BindingResult异常处理,有点重复有点麻烦
答案是:让程序默认报自定义错——不用BR重写,而是全局地统一重写报错操作
要点:
在外面定义的全局异常类需要几个必备参数(配置)
整个异常类——明确包扫描范围
@ControllerAdvice就是表示异常处理类
通过给一个java类加注解,就将所有目的范围内的异常全部引流过来——钩子?AOP?过滤器?…在Spring的思想中,这个注解一定将类声明成bean对象加载到容器,在底层报异常时调用此bean的方法。
避免面向对象,而是面向切面——仅仅一个注解,便只需要关心业务逻辑
每个方法——明确针对的异常
用好错误码:
思路:用枚举类型将不同错误类型对应不同错误码,前端接收各种各样错误码。
一种枚举类型相当于一个对象,内部都是静态属性(还是面向对象啦)
分组校验:
由于一张表的某一字段在不同业务中的校验模式不同(id在新增行时可为null,在删改时不能为null),于是需要将每张Entity表的每个属性的每个校验注解增加分组。最后在Servlet方法接收参数时声明具体分组
Attention:
每个分组的定义方法是:用一个接口声明,用接口的字节码对象做具体识别符。
自定义校验:
步骤:
- 编写自定义校验注解
- 编写自定义校验器
- 关联自定义校验器和自定义注解
思路:
编写注解:和其他校验注解声明同样的元注解,声明同样的插槽/参数
编写校验器:
校验器是实现校验器接口的一个类,它的核心作用是:拿到校验注解得到的参数,在校验器中完成逻辑判断
怎么拿到?—— 泛!
之前对泛型传参的理解,只有泛型能够传递类型(baseMapper由Entity类似确定上层)
此时泛型传入一个注解类型,由此类型即可得到其被传入的参数
可见:注解不同于类和类对象,其在内存中似乎是一个唯一的类型。
关联二者:
谁是在主程序中能够被扫描到的?:注解——于是需要在声明注解时绑定校验器
感觉双绑有点多余…
SPU and SKU
SPU ( Single Product Unit ) :
单独产品单元:具有相同基本属性和特征的商品集合
SKU(Stock Keeping Unit):
库存单位:对具体产品进行编码和表示
一个SPU可能包含多个SKU:例如:S20是SPU,黑色128g和绿色256g分别是一个SKU。
基本属性(规格参数)and 销售属性
SPU对应的属性就是基本属性
区分不同SKU的属性就是销售属性
特征:
基本属性和销售属性都能被各自的三级分类组织起来
数据库的逻辑删除
当对某表进行删除时,没有实质上删除数据库中数据,而是通过将某个字段设置为反,或者其他信息,来表示对上不可见。
分析:如此一来:关于这个字段的查询需要凌驾于许多SQL之上,比如select,delete,update,insert都需要判断此字段,需要一个对所有SQL都管理的工具
myBatis-Plus
对于自动注入的SQL
实际上,mp的模式是:基于接口的映射
通过继承通用的Mapper接口来实现数据库操作,避免手写大量的CRUD代码
so my question is —— 表和表不一样,继承下来的方法不可能都不同吧?
答案:通过泛型传递数据
Service层的实现类继承了一个大接口,用泛型传递了这个Service要操作的表。
于是针对这张表的CRUD代码已经确定
一个问题是:主键是谁在Java的entity对象中体现不来,这在mp的默认规则是:叫id的是主键,或者在entity的主键上加注解@TableId声明主键
mp逻辑删除:
分布式高级篇
ElasticSearch
是一个分布式搜索引擎
Elasticsearch可以为所有类型的数据提供实时的搜索和分析,包括结构化文本、非结构化文本、数字数据或地理空间数据。Elasticsearch都能够以快速搜索的方式有效地对其进行存储和建立索引(search其实就是对索引的快速查找)。 ElasticSearch不仅可以进行简单的数据检索,还可以聚合信息来发现数据中的趋势和模式。 随着数据和查询量的增长,Elasticsearch的分布式特性可以使部署的集群随之无缝地扩容。
应用场景:
为应用(APP)或网站添加内容搜索框 | 全文检索
存储和分析日志、监测值、和安全事件数据 | ELK系统
使用机器学习自动实时为数据行为建模
几个ES相关技术关键词:
全文检索,倒排索引,封装Lucene jar包,ELK:ElasticSearch+Logstach+Kibana
项目搭建Nginx服务器
搭建背景 —— 动静分离 :
如果将动态资源和静态资源全都部署在Tomcat服务器上,则对业务的请求和对静态资源的请求全都到达Tomcat服务器,但考虑到服务器可贵的并发性能,应该主要交给业务逻辑。于是需要将对静态资源的请求分配到其他服务器。
动静分离:
静:图片,JS,CSS等静态资源(以实际文件存在)
动:服务器需要处理的请求(以代码存在)
项目的后台管理系统是vue,但将首页等页面放到Tomcat中(各个微服务的resource)
即:每个微服务不仅有后端逻辑,也内聚了前端的页面
核心:用 thymeleaf 模板引擎
thymeleaf是一款用于渲染XML/XHTML/HTML5内容的模板引擎。类似JSP
Thymeleaf
作用类似 JSP,是一种写html时插入java对象(属性)的语法
具体而言:
在Tomcat服务器的resource资源中写有html文件
文件中通过${}插槽,将内部的内容替换为Controller中插入的同名内容
return 表示要进入某个视图 xxx.html
某处的配置文件设置有默认前缀后缀 于是可以直接写html文件名
静态资源存放在static文件夹下,即可按照路径直接访问
页面放在templates下,直接访问
springBoot为thymeleaf进行了自动配置 —— 直接访问端口,自动寻找index.html
搭建域名访问环境
正规流程:
购买域名
买一台服务器,可以用虚拟专用服务器/云服务器,服务器的操作系统是Linux
设置域名解析:将域名指向服务器的IP地址
服务器上安装web服务器,如Nginx,Tomcat…
配置服务器路由:确保将访问服务器的请求路由到web服务器/上传的文件
设置安全性:配置防火墙、SSL证书等
正向代理和反向代理:
正向代理反向代理作用在同一位置,区别在于封装(代理)的方向
正向代理:服务器代理客户端,去访问原本客户端访问受限的资源
反向代理:服务器代理后端集群,作为集群的入口/门面让客户端访问
压力测试
对一个系统/微服务,在一定的软硬件环境下(linux,4g内存,4核cpu),能够承受的最大负荷量(系统瓶颈)
在压测得出一个系统的瓶颈后,可以通过负载均衡等操作,避免系统压力过大-宕机
压力测试可以发现些单线程场景下不会暴露的问题
内存泄漏 —— 程序执行中创造了过多对象(占用太多内存)而没有复用
内存泄漏(Memory Leak)是指在计算机程序中,分配的内存空间在不再被程序使用的情况下未能释放,从而导致系统中的可用内存逐渐减少,最终可能导致系统性能下降甚至崩溃。
引发原因:
未释放分配的内存:
当不再需要某对象/某处内存时,没有调用释放内存的函数
循环引用:
对象之间形成了循环引用,即比例引用导致无法被垃圾回收机制正确处理,从而无法释放内存
未关闭资源:
例如文件、网络连接、熟即可连接没有正确关闭,导致资源任然被程序占用,且内存任然被占用
缓存未清理:
由于缓存机制导致大量数据没有被清理而导致占用内存
异常退出:
异常退出导致释放资源和内存的机会被忽略
并发与同步问题
- 并发:由于多个线程同时执行,导致资源竞争、数据不一致等其他意外行为
- 同步:为了解决并发问题,确保多个线程安装顺序执行
常见的并发和同步问题:
压测指标
TPS |Transaction per second 每秒完成交易/事务数
Jmeter
压测工具,使用逻辑是:
底层创建线程池 —— 线程组中每个线程并非对应本机一个端口,而是通过Jmeter创建的线程池完成通信。线程池或许在必要的情况下利用多个线程吧
压测:
影响性能的考虑点:
数据库:
连接池管理
合理的连接数限制,过多的连接数在维持中降低了性能,过低的连接数会导致排队。
连接超时设置
合理的连接超时时间可以防止应用程序陷入无响应状态。
关于预编译
使用预编译语句(Prepared Statements)来执行数据库查询。预编译语句可以减少每次查询的解析时间,提高执行效率。
SQL语句层面的优化
合理使用索引:使用索引缩短查询时间,过多使用导致维护成本过高
对于大批量的数据操作,考虑使用批量操作减少交互次数
使用缓存机制
对于频繁读取但不经常变动的数据,可以考虑使用缓存机制,如内存缓存或分布式缓存。
应用程序
中间件
Nginx
网关
Tomcat
网络,带宽,操作系统
性能分析,就是监控分析某个程序占用内存、CPU、线程、耗时…需要了解Java程序执行过程 ——JVM内存模型
JVisualVM
动态检测内存工具
其中,visual GC插件可以检测Minor GC,Full GC等行为
中间件对性能的影响:
由于在项目环境中,请求的传发流程是:浏览器发送请求先到达Linux虚拟机(192.168.56.10)中的Nginx(80端口),Nginx完成负载均衡,动静分离,将必要的请求转发到Gateway网关,网关通过配置文件各种断言匹配所来的请求,将其路由到不同微服务,最终执行后台逻辑代码。
ATTENTION:
为什么要原路返回 ? 因为要对上层屏蔽细节?
Nginx
检测方法:
用Jmeter直接向Nginx服务器发送请求(不会路由下去的请求,由于Nginx只能识别对域名的访问,于是只要直接对IP地址访问就不会被路由匹配),测试响应的时间、吞吐量…
由于Nginx在docker中,通过命令:docker stats查看容器的性能
Gateway
检测方法:
Jmeter向网关发送请求,检测响应的时间、吞吐量…
由于Fateway是Java进程,可以用JvisualVM进行可视化检测
结果:
访问网关大概时间13s,直接访问Java后台时间大概10s,于是访问网关+简单服务大概是二者之和
由于访问时间大致翻倍了,导致吞吐量大致砍一半了—— 线程数是一定的,每个线程耗时长导致来回次数少。
结论:
中间件越多,性能损失越大
性能大多都损失在性能交互
目标优化方向:
增强中间件的吞吐量
之间的传输效率提高
更好的网线,效率更高的传输协议…
myComputer测试结果
Nginx
测试发现:
正常:
Nginx对CPU的消耗较高,网络IO也较高
因为Nginx的作用只是接收请求完成匹配进行路由
关键在于:需要有更多的线程来接收各种请求,所以CPU需要在线程之间切换跑
不正常:
内存占用过高(视频里只有1.5兆)
盲目分析:
我的吞吐量为13W,视频中只有2k,可能是由于请求量太多导致的
当我关闭了Jmeter对Nginx的请求,发现CPU使用立马为0了,但内存依旧1个G,但重启Nginx后内存就清空了
Gateway
测试发现:
正常:
- CPU利用率很高(维护的线程数很多),内存占用不高
简单服务
测试发现:
正常:
- 由于没用复杂的运算逻辑,只有高高的线程数,导致CPU不高也不低,内存较低
Gateway+简单服务、
全链路:
- 简单服务(直接返回String)
2. 涉及数据库的服务
3. 对首页的直接访问
4. 对首页完全加载的访问(每张静态图片也去访问Tomcat)
吞吐量为10
sumUp
所以:主要的性能损耗——
发生在业务逻辑上
发生在加载静态资源上
@硅谷测试结果
动静分离
场景描述:
老版本业务部署是:浏览器的一个基础页面请求要过Nginx,Gateway才能到达Tomcat,并且一次页面请求中,html中还包含其他静态资源的请求,当将其部署在Tomcat时,第一次的页面请求就会连带众多对Tomcat的请求。
不仅导致后台Tomcat占用了许多线程,也必须跑完Nginx-Gateway全套。
当一个用户进行访问,占用了大量Tomcat线程,导致第二个用户的请求时延增加,往后甚至导致Tomcat宕机。
动静分离:
将不需要过业务逻辑的请求在业务部署的最前端就返回掉,尽量不占用任何不必要的资源 —— 在Nginx中部署静态资源。
做法:
将项目的静态资源放在Nginx中
定义Nginx能够扫描到的规则:规定/static/**的所有请求都有Nginx直接返回
注意Nginx的规则
动静分离后的压测:
缓存!
适合存入缓存的数据
及时性、数据一致性要求不高的
比如物流信息对及时性要求不高
数据一致性:在数据库的真实信息和查询得到的信息是否一致
访问量大且更新频率不高的数据(读多,写少)
配合缓存的读模式:
如何实现缓存:
本地缓存
思路:
对于要调用的方法:在第一次调用后将结果保存为临时变量,下次调用时现场检查变量是否为null即可
将一堆变量集合到一起就是一个map。
定义:
本地缓存:和用户线程存储在同一个栈帧中的数据
问题:
在单服务器模式下,OK,但是在分布式集群模式下存在弊端
每台服务器都需要本地缓存同样的数据。
如果要更改缓存的数据,在单体服务器时,一次同步刷新即可,但在集群模式下,需要全部大改,并且如果是通过请求重新访问数据库来刷新数据,一次刷新只能更改随即一台服务器的缓存,难以实现全部刷新。
分布式缓存—Redis
项目的做法:
就是从判断本地有无缓存转化到判断redis有无缓存
潜在问题:(缓存失效)
缓存穿透:大并发查询缓存中不存在的信息。每个线程都跑了一次数据库,但却因为查不到信息导致后来同样的请求仍需跑数据库
风险:
数据库压力瞬时增大,导致崩溃
解决:
将null值缓存,并加入短暂过期时间
短暂就是为了防止一时的缓存穿透,不能时间过长是因为有可能数据库中真的更新到词条数据,于是需要Redis同步更新。
缓存雪崩:当缓存中的key采用了相同的过期时间,缓存在某一时刻同时失效,导致请求全部转发到DB。
风险:
雪崩——原本程序正常运行,某时刻redis压力突然减少,数据库压力猛增
解决:
失效时间分散
动态设置热点的失效时间
缓存击穿:当一个超级热点的key过期了,但过期后的某一时刻被高并发的访问,由于第一个访问数据库的线程是有时延的,还来不及放进redis中,导致高并发的请求全部落到数据库
风险:
在一个热点数据高并发爆炸访问——击穿
解决:
加锁——高并发导致的问题都可通过加锁来降低性能,提高可用性
缓存击穿之加锁方法
回来吧谷粒商城!
缓存的使用:
一般数据库只承担数据持久化的工作,而数据读取需要给数据库降压
哪些数据适合存入缓存?
即时性、数据一致性要求不高的(面临 缓存一致性 问题)
访问量大且更新频率不高的数据
缓存部署在项目中的位置:
如果所有请求都需要先跑redis,redis的并发程度需要巨高无比
Spring Cache
由于手动操作Redis的代码很多都是重复的 / 固定的,这就是定义接口 + 实现类的意义所在