谷粒商城笔记合集
分布式基础篇 | 分布式高级篇 | 高可用集群篇 |
---|---|---|
===简介&环境搭建=== | ===Elasticsearch=== | |
项目简介与分布式概念(第一、二章) | Elasticsearch:全文检索(第一章) | |
基础环境搭建(第三章) | ===商品服务开发=== | |
===整合SpringCloud=== | 商品服务 & 商品上架(第二章) | |
整合SpringCloud、SpringCloud alibaba(第四、五章) | ===商城首页开发=== | |
===前端知识=== | 商城业务:首页整合、Nginx 域名访问、性能优化与压力测试 (第三、四、五章) | |
前端开发基础知识(第六章) | 缓存与分布式锁(第六章) | |
===商品服务开发=== | ===商城检索开发=== | |
商品服务开发:基础概念、三级分类(第七、八章) | 商城业务:商品检索(第七章) | |
商品服务开发:品牌管理(第九章) | ||
商品服务开发:属性分组、平台属性(第十、十一章) | ||
商品服务:商品维护(第十二、十三章) | ||
===仓储服务开发=== | ||
仓储服务:仓库维护(第十四章) | ||
基础篇总结(第十五章) |
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties
templates 下的视图页面只能通过controller返回。访问项目根地址的欢迎页默认返回 classpath:/templates/index.html。
如果controller返回的不是json数据,而是一个字符串。那么 springmvc 的视图解析器就会根据前后缀进行拼串找到并返回视图页面
静态资源存放路径:classpath:/META-INF/resources/、classpath:/resources/、classpath:/static/、classpath:/public/。可直接访问
项目架构
${...}
*{...}
#{...}
@{...}
~{...}
'one text'
,'Another one!'
,…0
,34
,3.0
,12.3
,…true
,false
null
one
,sometext
,main
,…+
|The name is ${name}|
+
,-
,*
,/
,%
-
and
,or
!
,not
>
,<
,>=
,<=
(gt
,lt
,ge
,le
)==
,!=
(eq
,ne
)(if) ? (then)
(if) ? (then) : (else)
(value) ?: (defaultvalue)
_
开发传统Java WEB工程时,我们可以使用JSP页面模板语言,但是在SpringBoot中已经不推荐使用了。SpringBoot支持如下页面模板语言
thymeleaf 官网:https://www.thymeleaf.org/
官网文档给出了 语法、相关标签 如何使用的步骤,由于官网文档都是英文,英文文档阅读能力好的同学可以选择阅读,英文不好的同学可以选择中文文档进行学习,为此我在网上找到了相关的中文文档:http://note.youdao.com/noteshare?id=7771a96e9031b30b91ed55c50528e918
在商品微服务的 pom.xml 中引入thymeleaf依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
修改配置文件,关闭 thymeleaf缓存:application.yml
spring:
thymeleaf:
cache: false
拷贝前端资源到指定目录
修改项目结构
在商品微服务的 pom.xml 中引入dev-tools依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<optional>trueoptional>
dependency>
让页面修改实时生效:CTRL+F9、CTRL+SHIFT+F9
创建首页跳转controller,首页渲染一级分类数据:cn/lzwei/bilimall/product/web/IndexController.java
@Controller
public class IndexController {
@Resource
CategoryService categoryService;
@GetMapping(value = {"/","/index.html"})
public String index(Model model){
//首页渲染:获取一级分类数据
List<CategoryEntity> categoryEntities=categoryService.getCategoryLevel1();
model.addAttribute("category",categoryEntities);
return "index";
}
}
CategoryService:首页渲染,获取一级分类数据
public interface CategoryService extends IService<CategoryEntity> {
/**
* 首页渲染:获取一级分类数据
*/
List<CategoryEntity> getCategoryLevel1();
}
CategoryServiceImpl:首页渲染,获取一级分类数据
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
/**
* 首页渲染:获取一级分类数据
*/
@Override
public List<CategoryEntity> getCategoryLevel1() {
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return categoryEntities;
}
}
在 /template/index.htnl 中添加 thymeleaf 属性命名空间
<html lang="en" xmlns:th="http://www.thymeleaf.org">
修改 /template/index.html,渲染页面一级分类数据
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
<header>
<div class="header_main">
<div class="header_banner">
<div class="header_main_left">
<ul>
<li>
<a href="#" class="header_main_left_a" th:attr="ctg-data=${category.catId}" th:each="category : ${categorys}" th:text="${category.name}"><b>家用电器b>a>
li>
ul>
div>
...
div>
div>
header>
body>
html>
首页渲染时发送请求到当前服务获取分类数据:src/main/resources/static/index/js/catalogLoader.js
$(function(){
/* 修改分类数据请求路径 */
$.getJSON("index/catalog.json",function (data) {
...
});
});
分析数据结构
{
"1": [
{
"catalog1Id": "1",
"catalog3List": [
{
"catalog2Id": "1",
"id": "1",
"name": "电子书"
},
...
],
"id": "1",
"name": "电子书刊"
},
...
],
...
}
web/IndexController.java:获取分类数据:用于渲染二级、三级分类
@Controller
public class IndexController {
@Resource
CategoryService categoryService;
/**
* 获取分类数据:用于渲染二级、三级分类
*/
@ResponseBody
@GetMapping(value = "/index/catalog.json")
public Map<String, List<CategoryLevel2Vo>> getCategoryLevel2(){
Map<String, List<CategoryLevel2Vo>> categorys=categoryService.getCategoryLevel2();
return categorys;
}
}
CategoryService:获取分类数据:用于渲染二级、三级分类
public interface CategoryService extends IService<CategoryEntity> {
/**
* 获取分类数据:用于渲染二级、三级分类
*/
Map<String, List<CategoryLevel2Vo>> getCategoryLevel2();
}
CategoryServiceImpl:获取分类数据:用于渲染二级、三级分类
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
/**
* 获取分类数据:用于渲染二级、三级分类
*/
@Override
public Map<String, List<CategoryLevel2Vo>> getCategoryLevel2() {
//1.获取一级分类:将一级分类转换为map进行遍历,自定义key、value
List<CategoryEntity> categoryLevel1s = this.getCategoryLevel1();
Map<String, List<CategoryLevel2Vo>> collect=null;
if(categoryLevel1s!=null){
collect = categoryLevel1s.stream().collect(Collectors.toMap(level1 -> level1.getCatId().toString(), level1 -> {
//2.通过一级分类id获取二级分类列表进行遍历:封装成CategoryLevel2Vo集合
List<CategoryEntity> level2s = this.list(new QueryWrapper<CategoryEntity>().eq("parent_cid", level1.getCatId()));
List<CategoryLevel2Vo> Level2Vos = null;
if (level2s != null) {
//遍历二级分类:封装成CategoryLevel2Vo
Level2Vos = level2s.stream().map(level2 -> {
CategoryLevel2Vo categoryLevel2Vo = new CategoryLevel2Vo(level2.getCatId().toString(), level2.getName(), level1.getCatId().toString(), null);
//3.通过二级分类id获取三级分类列表:封装成CategoryLevel3Vo集合
List<CategoryEntity> level3s = this.list(new QueryWrapper<CategoryEntity>().eq("parent_cid", level2.getCatId()));
if (level3s != null) {
//遍历三级分类:封装成CategoryLevel3Vo
List<CategoryLevel2Vo.CategoryLevel3Vo> level3Vos = level3s.stream().map(level3 -> {
CategoryLevel2Vo.CategoryLevel3Vo categoryLevel3Vo = new CategoryLevel2Vo.CategoryLevel3Vo(level2.getCatId().toString(), level3.getCatId().toString(), level3.getName());
return categoryLevel3Vo;
}).collect(Collectors.toList());
categoryLevel2Vo.setCatalog3List(level3Vos);
}
return categoryLevel2Vo;
}).collect(Collectors.toList());
}
return Level2Vos;
}));
}
return collect;
}
}
http://nginx.org/en/docs/
由于Nginx跑在云服务器上,所以打算实现内网穿透:⚠️⚠️⚠️行不通,域名访问 公网:80 时要求域名需要备案⚠️⚠️⚠️
浏览器bilimall.com—>云服务器:80(Nginx)—>云服务器:88(http内网穿透端口)—>本地:88(网关服务)
修改云服务器安全组开放对应端口、关闭云服务器内部防火墙(systemctl stop firewalld)
在云服务下载 frp 服务端,或者通过科学的方法在github上拉比较快:https://github.com/fatedier/frp
[root@tencent opt]# wget https://github.com/fatedier/frp/releases/download/v0.37.0/frp_0.37.0_linux_amd64.tar.gz
将压缩包解压到当前目录,进入文件夹查看文件列表
[root@tencent opt]# tar -zxvf frp_0.37.0_linux_amd64.tar.gz
[root@tencent opt]# cd frp_0.37.0_linux_amd64/
[root@tencent frp_0.37.0_linux_amd64]# ll
修改服务端配置。注意:配置项中用到的端口,需要在服务器上开启
[common]
bind_addr=0.0.0.0
bind_port = 7777
#作为http映射的端口:即访问 云服务器:88 ,实现访问内网服务。内网服务
vhost_http_port = 88
token=******
dashboard_port=8888
dashboard_user=admin
dashboard_pwd=******
给frp所在目录下的frp文件开通可执行权限
[root@tencent frp_0.37.0_linux_amd64]# chmod 755 ./frps
配置frp后台服务
[root@tencent frp_0.37.0_linux_amd64]# sudo vim /lib/systemd/system/frps.service
[Unit]
Description=fraps service
After=network.target syslog.target
Wants=network.target
[Service]
Type=simple
#启动服务的命令(此处写你的frps的实际安装目录)
ExecStart=/opt/frp_0.37.0_linux_amd64/frps -c /opt/frp_0.37.0_linux_amd64/frps.ini
[Install]
WantedBy=multi-user.target
启动 frps,打开开机启动
[root@tencent frp_0.37.0_linux_amd64]# systemctl start frps
[root@tencent frp_0.37.0_linux_amd64]# systemctl enable frps
Created symlink from /etc/systemd/system/multi-user.target.wants/frps.service to /usr/lib/systemd/system/frps.service.
访问控制面板:http://云服务器ip:控制面板服务端口
下载frp客户端:https://github.com/fatedier/frp/releases?page=2
解压下载后的压缩包,并修改配置文件:frpc.ini
#注意:相关参数需要与服务端相互对应
[common]
server_addr = 云服务器IP
server_port = 7777
token=******
#这里表示将云服务器的88端口的所有请求映射到本地88端口
[web]
type=http
local_ip=127.0.0.1
local_port=88
custom_domains=114.132.162.129
#localtions=/
客户端运行
命令行方式
nohup .\frpc.exe -c .\frpc.ini >/dev/null 2>&1 &
脚本方式,在安装目录下创建文件:xxx.bat
@echo off
if "%1" == "h" goto begin
mshta vbscript:createobject("wscript.shell").run("""%~nx0"" h",0)(window.close)&&exit
:begin
REM
.\frpc.exe -c .\frpc.ini
配置成功
配置HTTPS服务器文档:http://nginx.org/en/docs/http/load_balancing.html
HTTP负载均衡文档:http://nginx.org/en/docs/http/configuring_https_servers.html
负载均衡其他细节文档:http://www.nginx.com/blog/load-balancing-with-nginx-plus-part-2/
这里使用域名 bilimall.com 访问到云服务器
修改本地域名映射文件:C:\Windows\System32\drivers\etc\hosts
注意:可以将该文件拖到桌面修改,然后再拖回原文件夹。可能需要转成txt文件才可被修改,之后再重命名回来
#注意这里找到云服务器80端口,即nginx服务
114.132.162.129 bilimall.com
为 云服务器的nginx 配置上游服务器,上游服务为云服务器的88端口,88端口已内网穿透到本地网关服务
[root@tencent ~]# vim /mydata/nginx/conf/nginx.conf
...
http {
...
upstream bilimall{
server 114.132.162.129:88;
}
}
为 云服务器的nginx 配置反向代理,将访问nginx的所有服务转发到上游服务器
server {
listen 80;
server_name bilimall.com;
location / {
proxy_set_header Host $host;
proxy_pass http://bilimall;
}
...
}
重启 nginx容器实例
浏览器访问:bilimall.com/api/product/attrattrgrouprelation/list
修改本地域名映射文件:C:\Windows\System32\drivers\etc\hosts
注意:可以将该文件拖到桌面修改,然后再拖回原文件夹。可能需要转成txt文件才可被修改,之后再重命名回来
#注意这里找到本地80端口,即nginx服务
192.168.100.1 bilimall.com
下载Windows版本的nginx安装包:http://nginx.org/en/download.html
将安装包解压到本地:F:\software\Nginx\
为 本地nginx 配置上游服务器和反向代理,上游服务为本地的网关服务,将访问nginx的所有请求转发到上游服务器:F:\software\Nginx\conf\nginx.conf
达到访问本机80端口(nginx服务),转发到本地网关服务根据规则进行路由的功能
...
http {
#添加下列配置:注意此配置为windows本地的nginx配置
upstream bilimall{
server 192.168.100.1:88;
}
#建议拷贝一份原来的进行修改:注意此配置为windows本地的nginx配置
server {
listen 80;
server_name bilimall.com;
location / {
proxy_set_header Host $host;
proxy_pass http://bilimall;
}
...
}
...
}
启动命令行窗口,进入nginx安装目录启动nginx
start nginx #开启nginx服务
nginx.exe -s stop #关闭nginx服务,快速停止nginx,可能并不保存相关信息
nginx.exe -s quit #关闭nginx服务,完整有序的停止nginx,并保存相关信息
nginx.exe -s reload #重载nginx服务,当你改变了nginx配置信息并需要重新载入这些配置时可以使用此命令重载nginx
taskkill /F /IM nginx.exe > nul #强关nginx服务器
浏览器访问:bilimall.com/api/product/attrattrgrouprelation/list
在 网关服务 的配置文件中添加域名路由规则,实现域名访问到商城系统:application.yaml
spring:
cloud:
gateway:
routes:
- id: bilimall_route
uri: lb://bilimall-product
predicates:
- Host=**.bilimall.com,bilimall.com
压力测试考察当前软硬件环境下系统所能承受住的最大负荷并帮助找出系统的瓶颈所在,压测都是为了系统在线上的处理能力和稳定性维持在一个标准范围内,做到心中有数。
使用压力测试,我们有希望找到很多种用其他测试方法更难发现的错误,有两种错误类型是:内存泄漏、并发与同步
有效的压力测试系统将应用以下这些关键条件:重复、并发、量级、随机变化
吞吐量大:系统支持高并发
响应时间:越短说明接口性能越好
SQL 耗时:越小越好、一般情况下微妙级别
命中率:越高越好、一般情况下不能低于95%
锁等待次数:越低越好、等待时间越短越好
响应时间(Response Time:RT)
响应时间指用户从客户端发起一个请求开始,到客户端接收到服务器端返回的响应结束,整个过程所耗费的时间
HPS(Hits Per Second) :每秒点击次数,单位是次/秒
TPS(Transaction per Second):系统每秒处理交易数,单位是笔/秒
QPS (Query perSecond) :系统每秒处理查询次数,单位是次/秒。对于互联网业务中,如果某些业务有且仅有一个请求连接,那么TPS=QPS=HPS,一般情况下用TPS来衡量整个业务流程,用QPS来衡量接口查询次数,用HPS来表示对服务器单击请求。
无论TPS、QPS、HPS,此指标是衡量系统处理能力非常重要的指标,越大越好,根据经验,一般情况下:
最大响应时间(Max Response Time) :指用户发出请求或者指令到系统做出反应(响应)的最大时间。
最少响应时间 (Mininum ResponseTime):指用户发出请求或者指令到系统做出反应(响应)的最少时间
90%响应时间(90% Response Time): 是指所有用户的响应时间进行排序、第90%的响应时间
从外部看、性能测试主要关注如下三个指标:
jmeter官网:https://jmeter.apache.org/
影响性能考虑点
可能的异常:JMeter Address Already in use
到 90% 以上,则可以说明服务器有问题,压力机没有问题。
windows本身提供的端口访问机制的问题
Windows提供给TCP/IP 链接的端口为1024-5000,并且要四分钟来循环回收他们。就导致我们在短时间内跑大量的请求时将端口占满了。
监控内存泄漏,跟踪垃圾回收,执行时内存、cpu分析,线程分析…
安装jvisualvm
https://visualvm.github.io/download.html
进入官网下载地址,下载相应版本的安装包
解压安装包,修改配置文件:F:\software\Java\visualvm_215\etc\visualvm.conf
#添加下列配置:指定自己jdk的安装目录
visualvm_jdkhome="F:\software\Java\jdk-17.0.5"
双击启动脚本启动 jvisualvm:F:\software\Java\visualvm_215\bin\visualvm.exe
如果没有反应可能是因为jdk安装目录下存在jre文件夹,可以选择删除获取移动到别的地方
安装插件
插件更新地址,根据版本选择地址:https://visualvm.github.io/pluginscenters.html
测试nginx,gateway,简单服务等中单个服务以及服务结合的性能:吞吐量/ms、90%响应时间/ms、99%响应时间/ms
压测内容 | 压测线程数 | 吞吐量/ms | 90%响应时间/ms | 99%响应时间/ms |
---|---|---|---|---|
Nginx | 50 | 2335 | 11 | 944 |
GateWay | 50 | 10367 | 8 | 31 |
简单服务 | 50 | 11341 | 8 | 17 |
Gateway+简单服务 | 50 | 3126 | 30 | 125 |
全链路 | 50 | 800 | 88 | 310 |
首页:一级菜单渲染 | 50 | 270(db,themleaf渲染,日志) | 267 | 365 |
三级数据获取 | 50 | 2(db,业务代码) | …(24000) | …(25000) |
首页:全量数据 | 50 | 7(静态资源) |
中间件越多,性能损失越大,大多都损失在了中间之间的网络交互:
业务:
压测内容 | 压测线程数 | 吞吐量/ms | 90%响应时间/ms | 99%响应时间/ms |
---|---|---|---|---|
首页:一级菜单渲染(开themleaf缓存) | 50 | 270(290) | 267(251) | 365(365) |
首页:一级菜单渲染(开themleaf缓存、优化数据库,降低日志级别) | 50 | 270(700) | 267(105) | 365(183) |
三级数据获取(优化数据库,降低日志级别) | 50 | 2(8) |
业务:
压测内容 | 压测线程数 | 吞吐量/ms | 90%响应时间/ms | 99%响应时间/ms |
---|---|---|---|---|
首页:全量数据(开themleaf缓存、优化数据库,关日志,动静分离) | 50 | 7(11) | ||
首页:全量数据(开themleaf缓存、优化数据库,关日志,动静分离,jvm内存) | 200 | 7(14) |
将 商品服务 resources/static/ 下的静态资源拷贝到 nginx中,并删除商品服务下的静态资源:F:\software\Nginx\html\static\
首页返回后下载商品服务中静态资源 resources/static/index/css/swiper-3.4.2.min.css 的路径为:http://bilimall.com/index/css/swiper-3.4.2.min.css
<link rel="stylesheet" href="index/css/swiper-3.4.2.min.css">
修改首页中 所有 静态资源请求的路径,请求 nginx中的静态资源。重启商品服务
<link rel="stylesheet" href="index/css/swiper-3.4.2.min.css">
<link rel="stylesheet" href="/static/index/css/swiper-3.4.2.min.css">
修改nginx配置文件:F:\software\Nginx\conf\nginx.conf
...
http {
upstream bilimall{
server 192.168.100.1:88;
}
server {
listen 80;
server_name bilimall.com;
#增加下列配置:注意此配置为windows本地的nginx配置
location /static/ {
root html;
location / {
proxy_set_header Host $host;
proxy_pass http://bilimall;
}
...
}
...
}
重启nginx,分离成功
业务:
压测内容 | 压测线程数 | 吞吐量/ms | 90%响应时间/ms | 99%响应时间/ms |
---|---|---|---|---|
三级数据获取(业务代码减少数据库IO) | 50 | 2(111) | 24000(571) | 25000(896) |
修改 商品服务 中首页获取三级分类数据的业务代码,减少数据库IO:cn.lzwei.bilimall.product.service.impl.CategoryServiceImpl
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
/**
* 获取分类数据:用于渲染二级、三级分类
*/
@Override
public Map<String, List<CategoryLevel2Vo>> getCategoryLevel2() {
//**************************缓存所有三级分类数据**************************************
List<CategoryEntity> categoryEntities = baseMapper.selectList(null);
//1.获取一级分类:将一级分类转换为map进行遍历,自定义key、value
List<CategoryEntity> categoryLevel1s = getParent_cid(categoryEntities,0l);
Map<String, List<CategoryLevel2Vo>> collect=null;
if(categoryLevel1s!=null){
collect = categoryLevel1s.stream().collect(Collectors.toMap(level1 -> level1.getCatId().toString(), level1 -> {
//2.通过一级分类id获取二级分类列表进行遍历:封装成CategoryLevel2Vo集合
List<CategoryEntity> level2s = getParent_cid(categoryEntities,level1.getCatId());
List<CategoryLevel2Vo> Level2Vos = null;
if (level2s != null) {
//遍历二级分类:封装成CategoryLevel2Vo
Level2Vos = level2s.stream().map(level2 -> {
CategoryLevel2Vo categoryLevel2Vo = new CategoryLevel2Vo(level2.getCatId().toString(), level2.getName(), level1.getCatId().toString(), null);
//3.通过二级分类id获取三级分类列表:封装成CategoryLevel3Vo集合
List<CategoryEntity> level3s = getParent_cid(categoryEntities,level2.getCatId());
if (level3s != null) {
//遍历三级分类:封装成CategoryLevel3Vo
List<CategoryLevel2Vo.CategoryLevel3Vo> level3Vos = level3s.stream().map(level3 -> {
CategoryLevel2Vo.CategoryLevel3Vo categoryLevel3Vo = new CategoryLevel2Vo.CategoryLevel3Vo(level2.getCatId().toString(), level3.getCatId().toString(), level3.getName());
return categoryLevel3Vo;
}).collect(Collectors.toList());
categoryLevel2Vo.setCatalog3List(level3Vos);
}
return categoryLevel2Vo;
}).collect(Collectors.toList());
}
return Level2Vos;
}));
}
return collect;
}
//通过 parent_id 获取分类数据
private List<CategoryEntity> getParent_cid(List<CategoryEntity> categoryEntities,Long parentId) {
List<CategoryEntity> collect = categoryEntities.stream().filter(item -> item.getParentCid().equals(parentId)
).collect(Collectors.toList());
return collect;
}
}
重启服务