《瑞吉项目》项目优化(Redis+MySQL主从复制+Nginx)
一、基于Redis进行缓存优化
1.问题说明
用户数量多,系统访问量大,频繁访问数据库,系统性能下降,用户体验差。
2.环境搭建
(1)导入坐标
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
(2)配置文件
spring redis: host: localhost port: 6379 database: 0
(3)提供配置类
使用RedisTemplate需要该配置类,也可以直接使用SpringRedisTemplate(已封装该配置类)。
3.缓存短信/验证码
前面随机生成的验证码是保存在HttpSession中的,现在优化为将验证码缓存在Redis中。
优化思路及代码实现:
-
在服务端UserController中注入RedisTemplate对象,用于操作Redis
@Autowired private RedisTemplate redisTemplate;
-
在服务端UserController的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟
//将生成的验证码缓存到Redis中,并设置有效期为5min redisTemplate.opsForValue().set(userPhone,code,5, TimeUnit.MINUTES);
-
在服务端UserController的login方法中,从Redis中获取缓存的验证码,如果登录成功则删除Redis中的验证码
//从Redis中获取验证码 Object codeInRedis = redisTemplate.opsForValue().get(userPhone); //登录成功,删除Redis中的验证码 redisTemplate.delete(userPhone);
4.缓存菜品数据
前面实现的移动端菜品查看功能,对应的服务端方法为DishController的list方法,此方法会根据前端提交的查询条件进行数据库查询操作。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。因此需要对此方法进行缓存优化,根据菜品分类缓存菜品数据,提高系统的性能。
优化思路及代码实现:
-
改造DishController的list方法,先从Redis中获取菜品数据,如果有则直接返回,无需查询数据库;如果没有则查询数据库,并将查询到的菜品数据放入Redis
@GetMapping("/list") public R<List<DishDto>> getDishWithFlavor(Dish dish) { List<DishDto> dishDtoList = new ArrayList<>(); //动态构造key String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus(); //从Redis中获取菜品数据 String json_dishDtoList = (String) redisTemplate.opsForValue().get(key); dishDtoList = JSON.parseObject(json_dishDtoList,List.class); if (dishDtoList != null) { //若存在,直接返回菜品数据,无需查询数据库 return R.success(dishDtoList); } //若不存在,查询数据库 LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper<>(); lqw.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId()); lqw.eq(Dish::getStatus, 1); lqw.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime); List<Dish> dishList = dishService.list(lqw); List<DishDto> dishDtoList2 = new ArrayList<>(); for (Dish dish1 : dishList) { DishDto dishDto = new DishDto(); BeanUtils.copyProperties(dish1, dishDto); LambdaQueryWrapper<DishFlavor> dishFlavor_lqw = new LambdaQueryWrapper<>(); dishFlavor_lqw.eq(DishFlavor::getDishId, dish1.getId()); List<DishFlavor> flavorList = dishFlavorService.list(dishFlavor_lqw); dishDto.setFlavors(flavorList); dishDtoList2.add(dishDto); } //将查询到的菜品数据缓存到Redis中 String json_dishDtoList2 = JSON.toJSONString(dishDtoList2); redisTemplate.opsForValue().set(key, json_dishDtoList2, 60, TimeUnit.MINUTES); return R.success(dishDtoList2); }
-
改造DishController的save和update方法,加入清理缓存的逻辑
//法一:清理所有菜品缓存数据 Set keys = redisTemplate.keys("dish_*"); redisTemplate.delete(keys); //法二:精确清理缓存:清理某个分类下的菜品缓存数据 String key="dish_"+dishDto.getCategoryId()+"_1"; redisTemplate.delete(key);
5.Spring Cache
(1)介绍
Spring Cache是一个框架,实现了基于注解的缓存功能,通过注解实现缓存功能。Spring Cache提供了一层抽象,底层可以切换不同的cache实现,具体是通过★CacheManager接口★来统一不同的缓存技术。CacheManager是Spring提供的各种缓存技术抽象接口。
针对不同的缓存技术需要实现不同的CacheManager:
CacheManager |
说明 |
EhCacheManager | 使用EhCache作为缓存技术 |
GuavaCacheManager | 使用Google的GuavaCache作为缓存技术 |
RedisCacheManager | 使用Redis作为缓冲技术 |
【tips】在spring boot项目中,使用某种缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。例如使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。
(2)常用注解
【tips】①@Cacheable可以通过设置condition属性/unless属性来实现满足条件时才缓存/不缓存数据的功能。
②@Cacheable、@CachePut、@CacheEvict使用时,需要指定value(缓存的名称,其下可有多个key)和key(方法返回值对应的key)。
③★SpEL动态设置key:key="#result.xx"。其中,#result代表的就是方法的返回值:
#result | 方法返回值 |
#形参名 | 方法的形参 |
#p0、#p1 | 方法的第一个、第二个参数 |
#root.method | 方法名 |
(3)使用方式
1)导入坐标
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2)配置文件
spring redis: host: localhost port: 6379 database: 0
3)在启动类上加入@EnableCaching注解,开启缓存注解功能
4)在Controller的方法上加入@Cacheable、@CacheEvict等注解,进行缓存操作
6.缓存套餐数据
前面实现的移动端套餐查看功能,对应的服务端方法为SetmealController的list方法,此方法会根据前端提交的查询条件进行数据库查询操作。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。因此需要对此方法进行缓存优化,提高系统的性能。
优化思路及代码实现
-
导入Spring Redis相关maven坐标
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
在application.yml中配置缓存数据的过期时间
cache: redis: time-to-live: 1800000 #设置缓存数据过期时间
- 在启动类上加入@EnableCaching注解,开启缓存注解功能
-
在SetmealController的list方法上加入@Cacheable注解
@GetMapping("/list") @Cacheable(value = "setmealCache",key = "#setmeal.categoryId+'_'+#setmeal.status") public R<List<Setmeal>> getSetmeal(Setmeal setmeal) {
-
在SetmealController的save和delete方法上加入@CacheEvict注解
//allEntries = true 表示删除setmealCache下的所有缓存 @CacheEvict(value = "setmealCache",allEntries = true)
二、读写分离
1.问题说明
(1)读(查)和写(增删改)都由一台数据库承担,压力大;
(2)若数据库服务器磁盘损坏,则数据丢失,造成单点故障。
因此需要将读写分离:
2.MySQL主从复制
对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。
MySQL主从复制是一个异步的复制过程,底层是基于MySQL数据库自带的二进制日志功能。就是一台或多台MySQL数据库(slave,即从库)从另一台MySQL数据库(master,即主库)进行日志的复制然后再解析日志并应用到自身,最终实现从库的数据和主库的数据保持一致。MySQL主从复制是MySQL数据库自带功能,无需借助第三方工具。
MySQL复制过程分成三步:
- master将改变记录到二进制日志(binary log)
- slave将master的binary log拷贝到自己的中继日志(relay log)
- slave重做中继日志中的事件,将改变应用到自己的数据库中
3.配置MySQL主从复制
(1)前置准备
准备两台服务器,分别安装MySQL并启动服务。
(2)配置主库master
1)修改MySQL数据库的配置文件/etc/my.cnf,开启二进制日志
[mysqld] log-bin=mysql-bin #[必须]启用二进制日志 server-id=100 #[必须]服务器唯一ID2)重启MySQL服务
systemctl restart mysqld3)登录MySQL数据库,执行下面的SQL
GRANT REPLICATION SLAVE ON *.* to 'xiaoming'@'%' identified by 'Root@123456';
【tips】这个SQL的作用是创建一个用户xiaoming,密码为Root@123456,并且给xiaoming用户授予REPLICATION SLAVE权限。常用于建立复制时所需要用到的用户权限,也就是slave必须被master授权具有该权限的用户,才能通过该用户复制。
4)在l数据库中执行下面的SQL,记录下结果中File和Position的值
4)在l数据库中执行下面的SQL,记录下结果中File和Position的值
show master status;【tips】这个SQL的作用是查看master的状态,执行完此SQL后不要再执行任何操作,否则上面记录的File和Position的值会发生变化。
(3)配置从库slave
1)修改MySQL数据库的配置文件/etc/my.cnf
[mysqld] server-id=101 #[必须]服务器唯一ID
2)重启MySQL服务
![](https://uploadfiles.nowcoder.com/images/20220921/130619287_1663756085582/A52CDC5B31A9C419A584FCC00F8354C7)
systemctl restart mysqld3)登录MySQL数据库,执行下面的SQL
change master to master_host='主库ip',master_user='xiaoming',master_password='Root@123456',master_log_file='mysql-bin.000001',master_log_pos=441; start slave;4)执行下面SQL,查看从数据库的状态
show slave status;
4.Sharding-JDBC介绍及使用入门
(1)介绍
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标准的数据库
(2)使用
1)导入坐标
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>4.0.0-RC1</version> </dependency>2)在配置文件中配置读写分离规则
spring: 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.152.100:3306/数据库名?characterEncoding=utf-8 username: root password: root # 从库数据源 slave: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.152.129:3306/数据库名?characterEncoding=utf-8 username: root password: root masterslave: # 读写分离配置 load-balance-algorithm-type: round_robin #轮询从库 # 最终的数据源名称 name: dataSource # 主库数据源名称 master-data-source-name: master # 从库数据源名称列表,多个用逗号分隔 slave-data-source-names: slave props: sql: show: true #开启SQL显示,可以在控制台输出SQL,默认false3)在配置文件中配置允许bean定义覆盖(allow-bean-definition-overriding)配置项
spring: main: allow-bean-definition-overriding: true
5.项目读写分离优化
(1)主从数据库准备
(2)导入坐标
(3)配置文件
三、Nginx
1.概述
Nginx是一款轻量级的web服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中表现较好。
(1)下载
到Nginx官方网站下载Nginx的安装包。
(2)安装
将nginx安装到Linux下/usr/local/nginx的中:
- 安装依赖包yum -y install gcc pcre-devel zlib-devel openssl openssl-devel
- 下载Nginx安装包wget https://nginx.org/download/nginx-1.16.1.tar.gz(需要先yum install wget) 或上传本地压缩包
- 解压tar -zxvf nginx-1.16.1.tar.gz
- cd nginx-1.16.1
- ./configure --prefix=/usr/local/nginx
- make && make install
重点目录/文件:
- conf/nginx.conf nginx的配置文件
- html 存放静态文件(html、css、Js等)
- logs 日志目录,存放日志文件
- sbin/nginx 二进制文件,用于启动、停止Nginx服务
2.常用命令
(1)查看版本
在sbin目录下输入:
./nginx -v(2)检查配置文件正确性
在启动Nginx服务之前,可以先检查一下conf/nginx.conf文件配置是否有错误,在sbin目录下输入:
![](https://uploadfiles.nowcoder.com/images/20220921/130619287_1663762611902/8877DB55BB8C26078D9BF30E6199590F)
(3)启动和停止nginx服务
./nginx -t
(3)启动和停止nginx服务
在sbin目录下:
//启动Nginx服务 ./nginx //停止Nginx服务 ./nginx -s stop(4)重新加载配置文件★
修改完nginx.conf配置文件后需要重新加载才能生效!!!在sbin目录下:
./nginx -s reload(5)配置在任意目录下直接使用nginx命令
通过在/etc/profile文件配置PATH变量(配置完记得source重新加载一下profile文件),实现在任意目录下直接使用nginx命令:
PATH=/usr/local/nginx/sbin:$JAVA_HOME/bin:$PATH
3.Nginx配置文件结构
Nginx配置文件(conf/nginx.conf)整体分为三部分:全局块、events块、http块。
【tips】http块中可以配置多个Server块,每个Server块中可以配置多个location块。
4.具体应用★
(1)部署静态资源
Nginx可以作为静态web服务器来部署静态资源(指在服务端真实存在并且能够在浏览器直接展示的一些文件,比如常见的html页面、css文件、js文件、图片、视频等资源)。
相对于Tomcat,Nginx处理静态资源的能力更加高效,所以在生产环境下,一般会将静态资源部署到Nginx中。
只需要将文件复制到Nginx安装目录下的html目录中即可完成静态资源部署到Nginx。
server { listen 80; #监听端口 server_name localhost; #服务器名称 location/{ #匹配客户端请求url root html; #指定静态资源根目录 index index.html; #指定默认首页 } }
(2)反向代理
1)正向代理
正向代理是一个位于客户端和原始服务器(origin server)之间的服务器。为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。
正向代理的典型用途是为在防火墙内的局域网客户端提供访问Internet的途径。
正向代理一般是在客户端设置代理服务器,通过代理服务器转发请求,最终访问到目标服务器。
2)反向代理
反向代理服务器位于用户与目标服务器之间,但是对于用户而言,反向代理服务器就相当于目标服务器,即用户直接访问反向代理服务器就可以获得目标服务器的资源,反向代理服务器负责将请求转发给目标服务器。
用户不需要知道目标服务器的地址,也无须在用户端作任何设定,反向代理服务器一般是在服务端设置。
3)配置反向代理
将192.168.138.100配置为反向代理服务器,在conf/nginx.conf中配置,将请求转发到web服务器(192.168.138.101):
server { listen 82; server_name localhost; location/{ proxy_pass http://192.168.188.101:8080; #反向代理配置,将请求转发到指定服务器 } }
(3)负载均衡
早期的网站流量和业务功能都比较简单,单台服务器就可以满足基本需求,但是随着互联网的发展,业务流量越来越大并且业务逻辑也越来越复杂,单台服务器的性能及单点故障问题就凸显出来了,因此需要多台服务器组成应用集群,进行性能的水平扩展以及避免单点故障出现。
- 应用集群:将同一应用部署到多台服务器上,组成应用集群,接收负载均衡器分发的请求,进行业务处理并返回响应数据。
- 负载均衡器:将用户请求根据对应的负载均衡算法分发到应用集群中的一台服务器进行处理。
1)配置负载均衡
在conf/nginx.conf中配置:
upstream targetserver{ #upstream指令可以定义一组服务器 #应该使用两个不同的ip地址的服务器,这里由于服务器有限,用同一服务器的不同端口来模拟两台服务器 server 192.168.188.101:8080; server 192.168.188.101:8081; } server { listen 8080; server_name localhost; location / { proxy_pass http://targetserver; } }2)负载均衡策略
四、前后端分离开发
1.问题说明
(1)开发人员同时负责前端和后端代码开发,分工不明确
(2)开发效率低
(3)前后端代码混合在一个工程中,不便于管理
(4)对开发人员要求高,人员招聘困难
2.前后端分离开发
(1)介绍
前后端分离开发是在项目开发过程中,对于前端代码的开发由专门的前端开发人员负责,后端代码则由后端开发人员负责,这样可以做到分工明确、各司其职,提高开发效率,前后端代码并行开发,可以加快项目开发进度。目前,前后端分离开发方式已经被越来越多的公司所采用,成为当前项目开发的主流开发方式。 前后端分离开发后,从工程结构上也会发生变化,即前后端代码不再混合在同一个maven工程中,而是分为前端工程和后端工程。
(2)开发流程
前后端分离开发后,面临一个问题,就是前端开发人员和后端开发人员如何进行配合来共同开发一个项目?按照如下流程进行:
接口(API接口):是一个http的请求地址,主要就是去定义请求路径、请求方式、请求参数、响应数据等内容。
(3)前端技术栈
1)开发工具
- Visual Studio Code
- hbuilder
- nodejs
- VUE
- ElementUI
- mock
- webpack
3.YApi
(1)介绍
YApi是高效、易用、功能强大的api管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 API,YApi还为用户提供了优秀的交互体验,开发人员只需利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。
(2)使用方式
使用YApi可以执行下面操作:
- 添加项目
- 添加分类
- 添加接口
- 编辑接口
- 查看接口
4.Swagger
(1)介绍
使用Swagger只需要按照它的规范去定义接口及接口相关的信息,再通过Swagger衍生出来的一系列项目和工具,就可以做到生成各种格式的接口文档,以及在线接口调试页面等。
knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案。
(2)使用方式
1)导入knife4j坐标
<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>3.0.2</version> </dependency>
2)创建knife4j相关配置类
两个注解+两个方法
@Slf4j @Configuration @EnableSwagger2 @EnableKnife4j public class WebMvcConfig extends WebMvcConfigurationSupport { @Bean public Docket createRestApi() { //文档类型 return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.ka.reggie.controller")) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("瑞吉外卖") .version("1.0") .description("瑞吉外卖接口文档") .build(); } }
3)设置静态资源映射,否则接口文档页面(doc.html)无法访问
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" };
(3)常用注解
注解 | 说明 |
---|---|
@Api | 用在请求的类上,例如Controller,表示对类的说明 |
@ApiModel | 用在类上,通常是实体类,表示一个返回响应数据的信息 |
@ApiModelProperty | 用在属性上,描述响应类的属性 |
@ApiOperation | 用在请求的方法上,说明方法的用途、作用 |
@ApilmplicitParams | 用在请求的方法上,表示一组参数说明 |
ApilmplicitParam | 用在@ApilmplicitParams注解中,指定一个请求参数的各个方面 |
五、项目部署
1.部署架构
2.部署环境说明
服务器:
- 192.168.152.100(服务器A)
- Nginx:部署前端项目、配置反向代理
- Mysql:主从复制结构中的主库
- 192.168.152.129(服务器B)
- jdk:运行Java项目
- git:版本控制工具
- maven:项目构建工具
- jar:Spring Boot项目打成jar包基于内置Tomcat运行
- Mysql:主从复制结构中的从库
- localhost
-
Redis:缓存中间件
3.部署前端项目
(1)在服务器A中安装Nginx,将前段项目打的包dist目录上传到Nginx的html目录下
(2)在配置文件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.152.129:8080; } error_page 500 502 503 504 /50x.html; location = /50x.html{ root html; } }
4.部署后端项目
(1)使用git clone命令将git远程仓库的代码克隆下来
git clone remote_url(2)将reggieStart.sh脚本上传到服务器B,通过chmod命令设置执行权限
chmod 777 reggieStart.sh(3)执行reggieStart.sh脚本,自动部署项目
【tips】也可手动部署项目。