第一部分
问题说明
环境搭建
用Git管理项目
1.切换git仓库
因为我之前对这个项目进行版本管理了,连接的仓库里边还有别的代码,这样就比较混乱,所以我就重新创建了一个gitee仓库,然后再idea里边切换 git 远程仓库 具体操作请点击这个链接idea切换git远程仓库
2.提交到远程仓库master分支
commit push
3.创建新的分支,然后再提交到新的分支(目的就是在这个分支上做修改,如果修改有错误的话可以再重新拉取master分支上的代码,如果修改的满意,那么可以合并分支)
4.然后导入一个redis的依赖,出现Unresolved dependency:org.spring-boot-starter-data-redis jar:2.5.2
这个bug, 解决办法
redis依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
5.导入redis配置类在config目录下命名为RedisConfig
package com.itheima.reggie.config;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
//默认的Key序列化器为:JdkSerializationRedisSerializer
redisTemplate.setKeySerializer(new StringRedisSerializer()); // key序列化
//redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // value序列化
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
6.commit and push到v1.0分支去,以后开发都在这个分支,开发完成后再与master分支合并
缓存短信验证码
实现思路
代码改造
1.注入对象
2.发送验证码,并设置有效期5分钟
3.删除验证码
功能测试
1.输入手机号并获取验证码
2.进入客户端查看
3.登录成功后删除验证码,redis客户端里边的电话号码也不见了
刷新后客户端看不见电话号码
缓存菜品
实现思路
前面我们已经实现了移动端菜品查看功能,对应的服务端方法为DishController的list方法,此方法会根据前端提交的查询条件进行数据库查询操作。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。现在需要对此方法进行缓存优化,提高系统的性能。
具体实现实路
代码改造
1.注入redis对象RedisTemplate
2.在list方法里改造代码
对应代码:
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){
//1.创建一个列表对象
List<DishDto> dishDtoList=null;
//2.这是菜品种类id 字符串的形式为 dish_1391233423412_1 动态构造key
String key="dish_"+dish.getCategoryId()+"_"+dish.getStatus();
// 3、先从Redis中获取缓存数据
dishDtoList= (List<DishDto>) redisTemplate.opsForValue().get(key);
// 4.如果存在,直接返回,无需查询数据库
if (dishDtoList!=null){
return R.success(dishDtoList);
}
// 5.1 如果不存在,需要查询数据库,将查询到的菜品数据缓存到Redis
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
//添加条件,查询状态为1(1为起售,0为停售)的菜品
queryWrapper.eq(Dish::getStatus,1);
List<Dish> list = dishService.list(queryWrapper);
dishDtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();
//对象拷贝(每一个list数据)
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId(); //分类id
//通过categoryId查询到category内容
Category category = categoryService.getById(categoryId);
//判空
if(category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
//获取当前菜品id
Long dishId = item.getId();
//构造条件构造器
LambdaQueryWrapper<DishFlavor> dishFlavorLambdaQueryWrapper= new LambdaQueryWrapper<>();
//添加查询条件
dishFlavorLambdaQueryWrapper.eq(dishId != null,DishFlavor::getDishId,dishId);
//select * from dish_flavors where dish_id = ?
List<DishFlavor> dishFlavors = dishFlavorService.list(dishFlavorLambdaQueryWrapper);
dishDto.setFlavors(dishFlavors);
return dishDto;
}).collect(Collectors.toList());
// 5.2 如果不存在,将查询到的菜品数据缓存到Redis
redisTemplate.opsForValue().set(key,dishDtoList,60, TimeUnit.MINUTES);
return R.success(dishDtoList);
}
功能测试
可以用debug模式进行测试
也可以直接在前端页面输入电话号码,验证码,然后再redis客户端查看
当点击一种菜品类别的时候,可以看到Redis客户端会有对应的菜品种类id返回,然后多测试几组。
缓存菜品数据(对新增修改操作)
对修改功能进行改造:
@PutMapping
public R<String> update(@RequestBody DishDto dishDto){
log.info(dishDto.toString());
// 清除所有菜品的缓存数据
// Set keys = redisTemplate.keys("dish_*");
// redisTemplate.delete(keys);
// 清理一部分缓存数据,某个分类下面的菜品缓存数据
String key="dish_"+dishDto.getCategoryId()+"_1";
redisTemplate.delete(key);
// 查询
dishService.updateWithFlavor(dishDto);
return R.success("新增菜品成功!");
}
对新增功能进行修改:
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
log.info("接收的dishDto数据:{}",dishDto.toString());
//保存数据到数据库
dishService.saveWithFlavor(dishDto);
// 清除所有菜品的缓存数据
// Set keys = redisTemplate.keys("dish_*");
// redisTemplate.delete(keys);
// 清理一部分缓存数据,某个分类下面的菜品缓存数据
String key="dish_"+dishDto.getCategoryId()+"_1";
redisTemplate.delete(key);
return R.success("新增菜品成功");
}
Spring Cache
在save方法上加了一个CachePut注解
测试:
在delete方法上加CacheEvict注解
/**
* CacheEvict:清理指定缓存
* value:缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
*/
@CacheEvict(value = "userCache",key = "#p0")
//@CacheEvict(value = "userCache",key = "#root.args[0]")
//@CacheEvict(value = "userCache",key = "#id")
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id){
userService.removeById(id);
}
在update方法上加CacheEvict注解
//@CacheEvict(value = "userCache",key = "#p0.id")
//@CacheEvict(value = "userCache",key = "#user.id")
//@CacheEvict(value = "userCache",key = "#root.args[0].id")
@CacheEvict(value = "userCache",key = "#result.id")
@PutMapping
public User update(User user){
userService.updateById(user);
return user;
}
在getByid方法和list方法上添加Cacheable注解
/**
* Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
* value:缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
* condition:条件,满足条件时才缓存数据
* unless:满足条件则不缓存
*/
@Cacheable(value = "userCache",key = "#id",unless = "#result == null")
@GetMapping("/{id}")
public User getById(@PathVariable Long id){
User user = userService.getById(id);
return user;
}
@Cacheable(value = "userCache",key = "#user.id + '_' + #user.name")
@GetMapping("/list")
public List<User> list(User user){
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(user.getId() != null,User::getId,user.getId());
queryWrapper.eq(user.getName() != null,User::getName,user.getName());
List<User> list = userService.list(queryWrapper);
return list;
}
测试:
启动debug模式,在postman软件上第一次查询id的时候它会跳转到idea,第二次在postman上查询同样的id,直接就返回数据了
SpringCache使用方式
1.pom坐标代码:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.配置application.yml文件(要注意代码加的位置)
前边都已经测试过了,直接在pom文件中加入依赖后,通过在redis客户端观察就行啦
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
cache:
redis:
time-to-live: 1800000 #设置缓存过期时间,可选
缓存套餐数据
实现思路
实现过程
1.导入坐标
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.配置yml
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
cache:
redis:
time-to-live: 1800000 #设置缓存过期时间,可选
mybatis-plus:
configuration:
#在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
3.在SetmealController方法中对list方法进行修改
代码段:
/**
* 根据条件查询套餐数据
* @param setmeal
* @return
*/
@GetMapping("/list")
@Cacheable(value = "setmealChache",key="#setmeal.categoryId + '_' + #setmeal.status")
public R<List<Setmeal>> list(Setmeal setmeal){
LambdaQueryWrapper<Setmeal> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(setmeal.getCategoryId()!=null,Setmeal::getCategoryId,setmeal.getCategoryId());
queryWrapper.eq(setmeal.getStatus()!=null,Setmeal::getStatus,setmeal.getStatus());
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
List<Setmeal> list =setmealService.list(queryWrapper);
return R.success(list);
}
测试发现有错误
在通用返回类加入如下代码
修改后测试成功
然后再删除和新增方法上加入EvitCache注解
删除方法:
/**
* 删除套餐
* @param ids
* @return
*/
@DeleteMapping
// @CacheEvict(value = "setmealCache",allEntries = true)
@CacheEvict(value = "setmealChache",allEntries = true)
public R<String> delete(@RequestParam List<Long> ids){
log.info("ids为:",ids);
setmealService.removeWithDish(ids);
return R.success("套餐数据删除成功");
}
新增方法:
@PostMapping
// @CacheEvict(value = "setmealCache",allEntries = true)
@CacheEvict(value = "setmealChache",allEntries = true)
public R<String> save(@RequestBody SetmealDto setmealDto){
log.info("数据传输对象setmealDto:{}",setmealDto.toString());
setmealService.saveWithDish((setmealDto));
return R.success("新增套餐成功!");
}
测试:
新增一个套餐
刷新后缓存被清理掉
第二部分
2.1内容介绍
2.2Mysql主从复制
从库可以有多个
2.2.1准备条件
Mysql主从复制
2.2.2配置-主库Master
启用日志就能看到增删改的操作信息,server-id是服务器唯一的标识
第一步
第二步
第三步
第四步
以上步骤执行完毕,就相当于主库Master这一端配好了,就不要再在主库上进行操作了
2.2.3配置-从库Slave
第一步
第二步
重启mysql
命令systemctl restart mysqld
第三步
第四步
遇到的问题:配置好上边的步骤但是还不能实现主从同步
可能原因:mysql的uuid相同
上图是配置好的,两个应该都显示yes,而我的两个都显示no
可以 参考链接
一些linux命令:
find /-iname "filename" //表示查找文件
systemctl restart mysqld //表示重启mysql
mysql -uroot -proot //表示登录mysql
show slave status \G; //表示查看slave的状态
测试
根据以上步骤配置过后,在navicat中,主库添加表信息等,在从库中会进行同步
2.3读写分离
2.3.1背景
2.3.2Sharding-JDBC介绍
2.3.3入门案例
1.maven坐标
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>
2.在配置文件中配置读写分离规则(导入后有些配置信息会报红,但是不影响运行。)
server:
port: 8080
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.201.130:3306/rw?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
# 从数据源
slave:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.201.131:3306/rw?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
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显示,默认false
main:
allow-bean-definition-overriding: true
#mybatis-plus:
# configuration:
# #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
# map-underscore-to-camel-case: true
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# global-config:
# db-config:
# id-type: ASSIGN_ID
遇到的bug:java.sql.SQLException: null, message from server: “Host ‘xx‘ is blocked because of many co
解决方案:
2.3.4功能测试
新增操作
新增、修改、删除应该走的都是master库
对应的查询操作,应该走slave库
2.4 项目实现读写分离
数据库环境准备(主从复制)
之前在虚拟机已经搭建好两台服务器,来实现主从复制
代码改造
就是以上这三步
功能测试
配置完成后启动项目
打开网页端
测试成功
2.5Nginx
2.5.1Nginx概述
下面这种树形结构
通过命令 yum install tree
来安装
安装完毕后输入 tree 即可展示
2.5.2Nginx命令
查看版本
./nginx -v
检查配置文件正确性
./nginx -t
启动和停止
./nginx //启动nginx服务
systemctl stop firewalld //关闭防火墙
ps -ef | grep 进程名 //表示查询正在运行的进程
./nginx -s stop //停止nginx进程
重新加载配置文件
./nginx -s reload //重新加载配置文件,不用再先关闭,在启动了
为了让我们启动更加方便nginx,可以配置环境变量
[root@localhost /]# vim /etc/profile
配置好后,让文件立即生效
source /etc/profile
配置好后就可以直接使用 nginx -s reload 命令来启动
nginx -s reload
2.5.3Nginx配置文件结构
全局块
events块
http块:里边有Server全局块,和location块
2.5.4Nginx具体应用
部署静态资源
cp 文件一名 目录一 //把文件一拷贝到目录一下边
set nu //在vim模式下显示行号
反向代理
配置方向代理服务器
用户不需要知道目标服务器的地址,反向代理配置
负载均衡
具体应用
1.在conf文件里边配置如下内容:
upstream targetserver{
server 192.168.201.131:8080;
server 192.168.201.131:8081;
}
server {
listen 8080;
server_name localhost;
location / {
proxy_pass http://targetserver; //反向代理配置,将请求转发到指定服务
}
}
2.保存退出,重新加载
:wq //保存退出
nginx -s reload // 重新加载
负载均衡策略:
第三部分
问题说明
前后端分离开发
介绍
开发流程
前端技术栈
YApi
介绍
使用方式
还没有配置
Swagger
介绍
使用方式
shutdown
1.导入maven坐标
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
2.导入knife4j相关配置(WebMvcConfig)
3.设置静态资源映射
4.在LoginCheckFilter中设置不需要处理的请求路径
配置类代码:
package com.itheima.reggie.config;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import com.itheima.reggie.common.JacksonObjectMapper;
import com.itheima.reggie.entity.Employee;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射...");
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
}
@Bean
public Docket createRestApi() {
// 文档类型
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.itheima.reggie.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("瑞吉外卖")
.version("1.0")
.description("瑞吉外卖接口文档")
.build();
}
}
常用注解
加在属性上,可以更加清晰的查看
项目部署
部署架构
部署环境说明
部署前端项目
部署后端项目
一个脚本文件,开始上传到自己的虚拟机,是没有权限执行它的,需要执行以下命令:
启动项目
启动成功
在网页查看
输入自己的服务器地址:http://192.168.201.130/
查看
图片不能加载的问题
解决步骤:
1.在配置文件里改图片的路径
2.创建文件夹,并上传图片资源
3.把idea里边改过得代码commit and push到远程仓库,然后再执行虚拟机上的脚本文件
4. 网页端查看
加载成功