【SpringBoot项目实战】瑞吉外卖优化篇
文章目录
- 优化背景
- 搭建环境
- 缓存短信验证码
- 缓存菜品数据
- Spring Cache
- Spring Cache介绍
- 原理
- Spring Cache常用注解
- Spring Cache使用方式
- 缓存套餐数据
- 读写分离
- 问题分析
- Mysql主从复制
- Mysql主从复制配置
- 前置准备
- 主库Master配置
- 从库Slave配置
- 读写分离
- Sharding-JDBC介绍
- 项目实现读写分离
- Nginx
- Nginx概述
- Nginx目录结构
- Nginx命令
- Nginx配置文件结构
- Nginx具体应用
- 部署静态资源
- 反向代理
- 负载均衡
- 前后端分离开发
- 问题分析
- 前后端分离开发
- Yapi
- Swagger
- 项目部署
- 部署架构
- 部署环境说明
- 部署前端项目
- 部署后端项目
优化背景
用户数量多,系统访问量大频繁访问数据库,系统性能下降,用户体验差
故使用缓存进行优化
搭建环境
Maven坐标:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置文件:
spring
redis:
host:172.17.2.94
port: 6379
password: root@123456
database: 0
缓存短信验证码
前面我们已经实现了移动端手机验证码登录,随机生成的验证码我们是保存在HttpSession中的。现在需要改造为将验证码缓存在Redis中,具体的实现思路如下:
-
在服务端UserController中注入RedisTemplate对象,用于操作Redis
-
在服务端UserController的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟
-
在服务端UserController的login方法中,从Redis中获取缓存的验证码,如果登录成功则删除Redis中的验证码
缓存菜品数据
前面我们已经实现了移动端菜品查看功能,对应的服务端方法为DishController的list方法,此方法会根据前端提交的查询条件进行数据库查询操作。
我们每次点击右边的分类查询菜品的时候都会进行一次数据库的查询:
在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。现在需要对此方法进行缓存优化,提高系统的性能。
具体的实现思路如下:
-
改造DishController的list方法,先从Redis中获取菜品数据,如果有则直接返回,无需查询数据库;
-
如果没有则查询数据库,并将查询到的菜品数据放入Redis。
-
改造DishController的save和update方法,加入清理缓存的逻辑
注意:在使用缓存过程中,要注意保证数据库中的数据和缓存中的数据一致,如果数据库中的数据发生变化,需要及时清理缓存数据。否则会造成数据库与缓存数据不一致的情况
Spring Cache
Spring Cache介绍
Spring cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
Spring Cache提供了一层抽象,底层可以切换不同的cache实现。具体就是通过CacheManager接口来统一不同的缓存技术。
CacheManager是Spring提供的各种缓存技术抽象接口。
针对不同的缓存技术需要实现不同的CacheManager:
原理
在SpringCache官网中,有一个缓存抽象的概念,其核心就是将缓存应用于Java方法中,从而减少基于缓存中可用信息的执行次数。换句话来说。就是每次调用目标方法前,SpringCache都会先检查该方法是否正对给定参数执行,如果已经执行过,就直接返回缓存的结果。(通俗的讲,就是查看缓存里面是否有对应的数据,如果有就返回缓存的数据),而无需执行实际方法、如果该方法尚未执行。则执行该方法(缓存中没有对应的数据就执行方法获取对应数据,并进行缓存),并缓存结果并返回给用户。这样就不用多次去执行数据库操作,减少cpu和io的消耗。
Spring Cache常用注解
SpringCache注解中公共的属性:
cacheNames
:每一个需要缓存的数据都需要指定要放到哪个名字的缓存,缓存的分区,按照业务类型分key
:缓存的key,如果是redis,则相当于redis的key。可以为空,如果需要可以使用spel表达式进行表写。如果为空,则缺省默认使用key表达式生成器进行生成。默认的 key 生成器要求参数具有有效的 hashCode() 和 equals() 方法实现。key的生成器。key/keyGenerator二选一使用condition
:缓存的条件,对入参进行判断。可以为空,如果需要指定,需要使用SPEL表达式,返回true/false,只有返回true的时候才会对数据源进行缓存/清除缓存。在方法调用之前或之后都能进行判断。condition=false
时,不读取缓存,直接执行方法体,并返回结果,同时返回结果也不放入缓存。condition=true
时,读取缓存,有缓存则直接返回。无则执行方法体,同时返回结果放入缓存(如果配置了result,且要求不为空,则不会缓存结果)- 注意:condition 属性使用的SpEL语言只有#root和获取参数类的SpEL表达式,不能使用返回结果的#result 。 所以
condition = "#result != null"
会导致所有对象都不进入缓存,每次操作都要经过数据库。
SPEL中可以使用的对象:
在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。
例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。
Spring Cache使用方式
在Spring Boot项目中使用Spring Cache的操作步骤(使用redis缓存技术);
-
导入maven坐标
- spring-boot-starter-data-redis
- spring-boot-starter-cache
-
配置application.yml
spring: cache: redis: time-to-live: 1800000#设置缓存有效期
-
在启动类上加入@EnableCaching注解,开启缓存注解功能
-
在Controller的方法上加入@Cacheable、@CacheEvict等注解,进行缓存操作
缓存套餐数据
前面我们已经实现了移动端套餐查看功能,对应的服务端方法为SetmealController的list方法,此方法会根据前端提交的查询条件进行数据库查询操作。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。现在需要对此方法进行缓存优化,提高系统的性能。
具体的实现思路如下:
-
导入Spring Cache和Redis相关maven坐标
-
在application.yml中配置缓存数据的过期时间
-
在启动类上加入@EnableCaching注解,开启缓存注解功能
-
在SetmealController的list方法上加入@Cacheable注解
-
在SetmealController的save和delete方法上加入CacheEvict注解
-
将R实现Serializable接口
读写分离
问题分析
Mysql主从复制
MySQL主从复制是一个异步的复制过程,底层是基于Mysql数据库自带的二进制日志功能。就是一台或多台MySQL数据库(slave,即从库)从另一台MySQL数据库(master,即主库)进行日志的复制然后再解析日志并应用到自身,最终实现从库的数据和主库的数据保持一致。MySQL主从复制是MySQL数据库自带功能,无需借助第三方工具。
MysQL复制过程分成三步:
- master将改变记录到二进制日志( binary log)
- slave将master的binary log拷贝到它的中继日志(relay log)
- slave重做中继日志中的事件,将改变应用到自己的数据库中
Mysql主从复制配置
前置准备
提前准备好两台服务器,分别安装Mysql并启动服务成功
主库Master 192.168.188.100
从库slave 192.168.188.101
注意:克隆的虚拟机需要修改数据库的uuid
主库Master配置
第一步:修改cnf配置文件:
[mysqld]
log-bin=mysql-bin #[必须]启用二进制日志
server-id=100 #[必须]服务器唯一ID
第二步:重启Mysql服务
systemctl restart mysqld
第三步:登录Mysql数据库,执行下面SQL
GRANT REPLICATION SLAVE ON *.* to 'xiaoming'@'%' identified by 'Root@123456';
注:上面SQL的作用是创建一个用户xiaoming,密码为Root@123456,并且给xiaoming用户授予REPLICATION SLAVE权限。常用于建立复制时所需要用到的用户权限,也就是slave必须被master授权具有该权限的用户,才能通过该用户复制。
在mysql8中如果报错就拆成下面的两句:
create user xiaoming identified by 'Root@123456'
grant replication slave on *.* to xiaoming
第四步:登录Mysql数据库,执行下面SQL,记录下结果中File和Position的值
show master status;
注:上面SQL的作用是查看Master的状态,执行完此SQL后不要再执行任何操作!!!
如果执行了操作,或者重启了mysql那么执行flush logs
,然后再此执行上述的命令!记录新的master状态
从库Slave配置
第一步:修改Mysq1数据库的配置文件cnf
[mysqld]
server-id=101 #[必须]服务器唯一ID
第二步:重启Mysql服务
systemctl restart mysqld
第三步:登录Mysq1数据库,执行下面SQL
change master to
master_host='192.168.188.100',master_user='xiaoming',master_password='Root@123456',master_log_file='mysql-bin.000003',master_log_pos=441;
start slave;
这个地方与我们前面的master status的信息是对应的
第四步:登录Mysql数据库,执行下面SQL,查看从数据库的状态show slave status;
这两项显示yes就代表ok了
读写分离
面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。
Sharding-JDBC介绍
Sharding-JDBC定位为轻量级Java框架,在Java的JDBC层提供的额外服务。它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。
使用Sharding-JDBC可以在程序中轻松的实现数据库读写分离。
- 适用于任何基于JDBC的ORM框架,如: JPA, Hibernate,Mybatis, Spring JDBC Template或直接使用JDBC。
- 支持任何第三方的数据库连接池,如:DBCP,C3PO,BoneCP, Druid, HikariCP等。
- 支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92标准的数据库。
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>
使用Sharding-JDBC实现读写分离步骤
- 导入maven坐标
- 在配置文件中配置读写分离规则
spring:
#Sharding-JDBC配置
shardingsphere:
#数据原配置
datasource:
#数据源名称
names:
master,slave
# 主数据源
master:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.188.100:3306/rw?characterEncoding=utf-8
username: root
password: 123456
# 从数据源
slave:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.188.101:3306/rw?characterEncoding=utf-8
username: root
password: 123456
#主从数据库读写分离配置
masterslave:
# 负载均衡算法类型(因为有的时候从库可能有多个,存在负载均衡的情况)
load-balance-algorithm-type: round_robin #轮询
# 最终的数据源名称
name: dataSource
# 主库数据源名称
master-data-source-name: master
# 从库数据源名称列表,多个逗号分隔
slave-data-source-names: slave
props:
sql:
show: true #开启SQL显示,默认false
- 在配置文件中配置允许bean定义覆盖配置项
spring:
main:
allow-bean-definition-overriding: true
这一项配置的原因是因为:我们的dateSource的bean在JDBC-Sharding以及Druid中都有:
项目实现读写分离
和前面讲的步骤一样:
-
导入maven坐标
-
在配置文件中配置读写分离规则
-
在配置文件中配置允许bean定义覆盖配置项
Nginx
Nginx概述
Nginx是一款轻量级的web服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中表现较好,中国大陆使用nginx的网站有:百度、京东、新浪、网易、腾讯、淘宝等。
Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的Rambler .ru站点(俄文: Paw6nep)开发的,第一个公开版本0.1.e发布于2004年10月4日。
官网: https://nginx.org/
Nginx目录结构
我们先来熟悉一下Nginx的目录结构,如下图:
重点目录/文件:
- conf/nginx.conf:nginx配置文件
- html:存放静态文件(html、css、Js等)
- logs:日志目录,存放日志文件
- sbin/nginx:二进制文件,用于启动、停止Nginx服务
Nginx命令
-
查看版本:在sbin目录下输入
./nginx -v
-
检查配置文件正确性:在启动Nginx服务之前,可以先检查一下conf/nginx.conf文件配置的是否有错误,命令如下:
./nginx -t
-
启动和停止:在sbin目录下
- 启动Nginx服务使用如下命令:
./nginx
- 停止Nginx服务使用如下命令:
./nginx -s stop
- 启动完成后可以查看Nginx进程:
ps -ef | grep nginx
- 修改运行的进程数目:
vim usr/local/nginx/conf/nginx.conf
worker_processes 2;
- 启动Nginx服务使用如下命令:
可以通过修改profile文件配置环境变量,在/
目录下可以直接使用nginx命令:
vim etc/profile
PATH=/usr/local/nginx/sbin:$JAVA_HOME/bin:$PATH
然后执行以下命令使配置文件生效:
source /etc/profile
接下来我们可以直接使用下面的命令:
- 重启Nginx:
nginx -s reload
- 停止Nginx:
nginx -s stop
- 启动Nginx:
nginx
Nginx配置文件结构
Nginx配置文件(conf/nginx.conf)整体分为三部分:
- 全局块:和Nginx运行相关的全局配置
- events块:和网络连接相关的配置
- http块:代理、缓存、日志记录、虚拟主机配置
- http全局块
- Server块
- Server全局块
- location块
注意:http块中可以配置多个Server块,每个Server块中可以配置多个location块。
Nginx具体应用
部署静态资源
Nginx可以作为静态web服务器来部署静态资源。静态资源指在服务端真实存在并且能够直接展示的一些文件,比如常见的html页面、css文件、js文件、图片、视频等资源。
相对于Tomcat,Nginx处理静态资源的能力更加高效,所以在生产环境下,一般都会将静态资源部署到Nginx中。
将静态资源部署到Nginx非常简单,只需要将文件复制到Nginx安装目录下的html目录中即可。
server {
listen 80; #监听端口
server_name localhost; #服务器名称
location/{ #匹配客户端请求url
root html; #指定静态资源根目录
index index.html; #指定默认首页
}
反向代理
-
正向代理:
-
是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。
-
正向代理的典型用途是为在防火墙内的局域网客户端提供访问Internet的途径。
-
正向代理一般是在客户端设置代理服务器,通过代理服务器转发请求,最终访问到目标服务器。
-
-
反向代理
- 反向代理服务器位于用户与目标服务器之间,但是对于用户而言,反向代理服务器就相当于目标服务器,即用户直接访问反向代理服务器就可以获得目标服务器的资源,反向代理服务器负责将请求转发给目标服务器。
- 用户不需要知道目标服务器的地址,也无须在用户端作任何设定。
-
配置反向代理
server {
listen 82;
server_name localhost;
location / {
proxy_pass http://192.168.188.101:8080; #反向代理配置
}
}
负载均衡
早期的网站流量和业务功能都比较简单,单台服务器就可以满足基本需求,但是随着互联网的发展,业务流量越来越大并且业务逻辑也越来越复杂,单台服务器的性能及单点故障问题就凸显出来了,因此需要多台服务器组成应用集群,进行性能的水平扩展以及避免单点故障出现。
- 应用集群:将同一应用部署到多台机器上,组成应用集群,接收负载均衡器分发的请求,进行业务处理并返回响应数据
- 负载均衡器:将用户请求根据对应的负载均衡算法分发到应用集群中的一台服务器进行处理
配置负载均衡:
修改ngnix.conf
upstream targetserver{ #upstream指令可以定义一组服务器
server 192.168.188.101:8080;
server 192.168.188.101:8081;
}
server {
listen 8080;
server_name localhost;
location / {
proxy_pass http://targetserver;
}
}
targetserver上下要对应。
负载均衡策略:
前后端分离开发
问题分析
- 开发人员同时负责前端和后端代码开发,分工不明确
- 开发效率低
- 前后端代码混合在一个工程中,不便于管理
- 对开发人员要求高,人员招聘困难
前后端分离开发
前后端分离开发,就是在项目开发过程中,对于前端代码的开发由专门的前端开发人员负责,后端代码则由后端开发人员负责,这样可以做到分工明确、各司其职,提高开发效率,前后端代码并行开发,可以加快项目开发进度。目前,前后端分离开发方式已经被越来越多的公司所采用,成为当前项目开发的主流开发方式。
前后端分离开发后,从工程结构上也会发生变化,即前后端代码不再混合在同一个maven工程中,而是分为前端工程和后端工程。
前后端分离开发后,面临一个问题,就是前端开发人员和后端开发人员如何进行配合来共同开发一个项目?可以按照如下流程进行:
接口(API接口) 就是一个http的请求地址,主要就是去定义:请求路径、请求方式、请求参数、响应数据等内容
Yapi
YApi是高效、易用、功能强大的api管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 API,YApi还为用户提供了优秀的交互体验,开发人员只需利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。
YApi让接口开发更简单高效,让接口的管理更具可读性、可维护性,让团队协作更合理。
源码地址: https://github.com/YMFE/yapi
要使用YApi,需要自己进行部署。
使用YApi可以执行下面操作
- 添加项目
- 添加分类
- 添加接口
- 编辑接口
- 查看接口
Swagger
使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,再通过Swagger衍生出来的一系列项目和工具,就可以做到生成各种格式的接口文档,以及在线接口调试页面等等。
官网:https://swagger.io/
knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案。
使用方式
1、导入knife4j的maven坐标
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
2、导入knife4j相关配置类
WebMvcConfig
@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Bean
public Docket createRestApi() {
//文档类型
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//配置要扫描的controller包
.apis(RequestHandlerSelectors.basePackage("com.ka.reggie.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("瑞吉外卖")
.version("1.0")
.description("瑞吉外卖接口文档")
.build();
}
}
3、设置静态资源,否则接口文档页面无法访问(addResourceHandlers方法)
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
4、在LoginCheckFilter中设置不需要处理的请求路径
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**",
"/common/**",
"/user/sendMsg",
"/user/login",
"/doc.html",
"/webjars/**",
"/swagger-resources",
"/v2/api-docs"
};
常用注解
项目部署
部署架构
部署环境说明
-
192.168.138.100(服务器A)
-
Nginx:部署前端项目、配置反向代理
-
Mysql:主从复制结构中的主库
-
-
192.168.138.101(服务器B)
-
jdk:运行Java项目
-
git:版本控制工具
-
maven:项目构建工具
-
jar: Spring Boot项目打成jar包基于内置Tomcat运行
-
Mysql:主从复制结构中的从库
-
Redis:缓存中间件
-
部署前端项目
第一步:在服务器A中安装Nginx,将课程资料中的dist目录上传到Nginx的html目录下
第二步:修改Nginx配置文件nginx.conf
server{
listen 80;
server_name localhost;
location /{
root html/dist;
index index.html;
}
location ^~ /api/{
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://192.168.188.101:8080;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html{
root html;
}
}
部署后端项目
第一步∶在服务器B中安装jdk、git、maven、MySQL,使用git clone命令将git远程仓库的代码克隆下来
第二步:将资料中提供的reggieStart.sh文件上传到服务器B,通过chmod命令设置执行权限
第三步:执行reggieStart.sh脚本文件,自动部署项目