缓存优化

使用Git管理代码

先让IDEA绑定Github,在vcs下选择创建一个git仓库,并选择需要被Git管理的目录,选择确认

再右击整个项目,菜单栏有个Git,依次点击命令即可

image-20230806102330765

记得选中整个文件夹,不然推送可能不一定完整,commit后会出现以下情况,展示页面跟IDEA的版本有关,不过整体内容大差不差

image-20230806101315050

这里我们选择连Commit and Push提交带推送,这样比较快捷,但是我们还没有设置自己的远程仓库,需要去Github上复制一下自己的仓库地址来进行上传

image-20230806101640382

Commit and Push后出现以下页面

image-20230806101805774

在这里定义远程仓库,把刚刚复制的仓库地址粘贴上去

image-20230806101851407 image-20230806101948608

点击push推送过去,可能会让你登录一下Github进行授权啥的,这里登录就行了,然后等待推送完成,去Github上面查看一下,有就成功了image-20230806102219573

这时候我们再创建一个新的分支,把内容跟master分支分隔开

image-20230806103100562

这里我创建了一个v1.0的分支

然后,我们把内容推送到v1.0这个分支上去

image-20230806103339971 image-20230806103356640

现在的内容没有变化,是因为还没有修改代码的内容,后期在v1.0上开发,内容就不一样了

在v1.0上开发完成后,还可以将内容合并回master分支上,非常方便和快捷,即使写错了也不会修改原master上的代码

环境搭建

问题说明

问题说明:

  • 当用户数量足够多的时候,系统访问量大
  • 频繁的访问数据库,系统性能下降,用户体验差
  • 所以一些通用、常用的数据,我们可以使用Redis来缓存,避免用户频繁访问数据库

导入SpringDataRedis的maven坐标

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

redis配置文件

1
2
3
4
5
redis:
host: localhost #这里换成localhost或者你自己的linux上装的redis
password: 123456
port: 6379
database: 0

配置序列化器

  • 配置一下序列化器,防止乱码问题(实际上不是乱码,只是一种展现的形式),方便我们在图形化界面中查看我们存入的数据,在config包下新建RedisConfig类
  • 但是也可以不配置RedisConfig,而是直接用SpringRedisConfig,它的默认序列化器就是StringRedisSerializer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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.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());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 设置值的序列化器统一
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);

return redisTemplate;
}
}

最后将修改完的数据提交并推送到那个新的v1.0的分支上去即可

缓存手机验证码

实现思路

先想一下之前做的邮件验证码是存储在哪的

存储在Session域中,现在学习了Redis,就可以缓存在Redis中

操作步骤如下:

  1. 在服务端UserController中注入RedisTemplate对象,用于操作Redis;
  2. 在服务端UserController中的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟,因为一般的手机短信有效期也为5分钟
  3. 在服务端UserController中的login方法中,从Redis中获取缓存的代码,如果登录成功则删除Redis的验证码

代码改造

在UserController中注入RedisTemplate对象,用于操作Redis

1
2
@Autowired
private RedisTemplate redisTemplate;

在服务端UserController中的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟

修改前后比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@PostMapping("/sendMsg")
public Result<String> sendMsg(@RequestBody User user, HttpSession session) throws MessagingException {
String phone = user.getPhone();
if (!phone.isEmpty()) {
//随机生成一个验证码
String code = MailUtils.achieveCode();
log.info(code);
//这里的phone其实就是邮箱,code是我们生成的验证码
MailUtils.sendTestMail(phone, code);
- //验证码存session,方便后面拿出来比对phone
- session.setAttribute(phone, code);
+ // 验证码缓存到Redis中,设置存活时间5分钟
+ redisTemplate.opsForValue().set("code",code,5, TimeUnit.MINUTES);
return Result.success("验证码发送成功");
}
return Result.error("验证码发送失败");
}

修改后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@PostMapping("/sendMsg")
public Result<String> sendMsg(@RequestBody User user, HttpSession session) throws MessagingException {
String phone = user.getPhone();
if (!phone.isEmpty()) {
//随机生成一个验证码
String code = MailUtils.achieveCode();
log.info(code);
//这里的phone其实就是邮箱,code是我们生成的验证码
MailUtils.sendTestMail(phone, code);
// 存储到Redis中并设置5分钟的存活时间
redisTemplate.opsForValue().set("code",code,5, TimeUnit.MINUTES);
return Result.success("验证码发送成功");
}
return Result.error("验证码发送失败");
}

在服务端UserController中的login方法中,从Redis中获取缓存的代码,如果登录成功则删除Redis的验证码

修改前后比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@PostMapping("/login")
public Result<User> login(@RequestBody Map map, HttpSession session) {
// 获取邮箱
String phone = map.get("phone").toString();
// 获得验证码,需要和系统内部的验证码进行比对
String code = map.get("code").toString();
- // 从session中获得验证码,session中的验证码之前在发送时,已经让服务器获得了
- String codeInSession = session.getAttribute(phone).toString();
+ // 把刚刚存入Redis的code拿出来
+ Object codeInRedis = redisTemplate.opsForValue().get("code");
- if (code != null && code.equals(codeInSession)) {
// 用redis中的code进行比较
+ if (code != null && code.equals(codeInRedis)) {
// 如果输入正确,验证用户是否存在
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getPhone, phone);
User user = userService.getOne(queryWrapper);
if (user == null) {
// 如果user为空,就创建一个新的user对象
user = new User();
user.setPhone(phone);
userService.save(user);
- user.setName("用户" + codeInSession);
+ user.setName("用户" + codeInRedis);
}
// 存个session,表示登录状态
session.setAttribute("user", user.getId());
// 并作为结果返回
+ // 如果登录成功,则删除Redis中的验证码
+ redisTemplate.delete("code");
return Result.success(user);
}
return Result.error("登录失败");
}

修改后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@PostMapping("/login")
public Result<User> login(@RequestBody Map map, HttpSession session) {
// 获取邮箱
String phone = map.get("phone").toString();
// 获得验证码,需要和系统内部的验证码进行比对
String code = map.get("code").toString();
// 把刚刚存入Redis的code拿出来
Object codeInRedis = redisTemplate.opsForValue().get("code");
// 判断从Redis中获取的code是否相同
if (code != null && code.equals(codeInRedis)) {
// 如果输入正确,验证用户是否存在
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getPhone, phone);
User user = userService.getOne(queryWrapper);
if (user == null) {
// 如果user为空,就创建一个新的user对象
user = new User();
user.setPhone(phone);
userService.save(user);
user.setName("用户" + codeInRedis);
}
// 存个session,表示登录状态
session.setAttribute("user", user.getId());
// 并作为结果返回
// 如果登录成功,则删除Redis中的验证码
redisTemplate.delete("code");
return Result.success(user);
}
return Result.error("登录失败");
}

功能测试

在测试时,发现一个报错:ERR Client sent AUTH, but no password is set,说是密码没有设置,原因是我们是通过redis-server.exe启动的,而双击启动默认是去找redis.conf的配置文件,然后没找到,所以也就报了之前的ERR Client sent AUTH, but no password is set错误。

解决办法:

方案1:

在redis安装目录下找到redis.windows.conf文件

找到这一行:image-20230806150448405

然后在redis目录下cmd运行命令:redis-server.exe redis.windows.conf

方案2:

其实就没设置密码,你直接把IDEA里的密码删了就行,无需密码可以直接访问

其实是类似于之前在Linux中的vim文件里的配置

这里我采用的是方案2,方案1我试了几次,感觉比较麻烦,所以我采用了方案2

配好之后我们再次测试

打开redis的服务器和客户端

获取邮箱验证码后,在redis中进行查看

image-20230806153106723

这里发现,redis中已经缓存了我们的数据了

此时我们进行登录

image-20230806153248759

此时就登录成功了,我们再去看看验证码有没有被删除

image-20230806153432733

缓存菜品数据

  • 菜品数据是我们登录移动端之后的展示页面
  • 所以每当我们访问首页的时候,都会调用数据库查询一遍菜品数据
  • 对于这种需要频繁访问的数据,我们可以将其缓存到Redis中以减轻服务器的压力

实现思路

  • 移动端对应的菜品查看功能,是DishController中的list方法,此方法会根据前端提交的查询条件进行数据库查询操作(用户选择不同的菜品分类)。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。所以现在我们需要对此方法进行缓存优化,提高系统性能
  • 但是还有存在一个问题,我们是将所有的菜品缓存一份,还是按照菜品/套餐分类,来进行缓存数据呢?
  • 答案是后者,当我们点击某一个分类时,只需展示当前分类下的菜品,而其他分类的菜品数据并不需要展示,所以我们在缓存的时候,根据菜品的分类,缓存多分类数据,页面在查询时,点击某个分类,则查询对应分类下的菜品的缓存数据
  • 具体实现思路如下
    1. 修改DishController中的list方法,先从Redis中获取分类对应的菜品数据,如果有,则直接返回;如果无,则查询数据库,并将查询到的菜品数据存入Redis缓存中
    2. 修改DishController的save、update和delete方法,加入清理缓存的逻辑,避免产生脏数据,也就是说我们修改/更新/删除了菜品,但是缓存没有被清理,数据还是之前的,所以展示的还是之前的,我们需要清理缓存,让它重新加载数据(其实就是我们实际已经在后台修改/更新/删除了某些菜品,但由于缓存数据未被清理,未重新查询数据库,用户看到的还是我们修改之前的数据)

代码改造

在DishController中需要注入RedisTemplate来使用Redis

1
2
@Autowired
private RedisTemplate redisTemplate;

修改后的代码如下

梳理一下思路,设立一个key用于区分不同的缓存内容,比如1号菜品里有什么菜,二号菜品有什么菜,这些缓存的key都是不同的,不能放在一个里面,通过key来进行区分,然后我们先从redis中获取对应的key,来进行判断,如果key存在,说明该缓存存在,直接返回缓存即可,否则,进行数据的查询,查询完成后,再使用redis进行数据的保存,最后返回数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@GetMapping("/list")
// 先将返回值类型改为List<DishDto>
public Result<List<DishDto>> list(Dish dish){
// 将dishDtoList作为内容缓存到Redis中
List<DishDto> dishDtoList = null;

// key是用于区分不同的缓存内容
String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus();

// 先从redis中获取缓存数据(获取的缓存数据应该是应该为dishDtoList)
dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);

// 如果存在,返回数据,无需查询
if (dishDtoList != null){
// 直接将查询到的缓存数据返回
return Result.success(dishDtoList);
}
// 如果不存在,就需要进行查询,并使用redis加以缓存
// 以下代码都是进行数据查询

LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
// 得到该菜品项对应的菜品
queryWrapper.eq(dish.getCategoryId() != null,Dish::getCategoryId,dish.getCategoryId());

// 添加条件,查询状态为1(起售状态)的菜品
queryWrapper.eq(Dish::getStatus,1);

//添加排序条件(先按照sort来排序,如果sort相同,再按照更新时间来排序)
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

List<Dish> list = dishService.list(queryWrapper);

dishDtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
// 分类id
Long categoryId = item.getCategoryId();
// 根据Id查询分类对象
Category category = categoryService.getById(categoryId);

if (category != null){
// 如果分类对象查询到了,说明该菜品有分类
String categoryName = category.getName();
// 就让菜品设置一下这个分类对象名字
dishDto.setCategoryName(categoryName);
}

// 得到菜品的id
Long itemId = item.getId();

LambdaQueryWrapper<DishFlavor> wrapper = new LambdaQueryWrapper<>();
// 查找与当前菜品id相同的口味信息
wrapper.eq(DishFlavor::getDishId,itemId);
List<DishFlavor> flavors = dishFlavorService.list(wrapper);
// 设置菜品口味
dishDto.setFlavors(flavors);
return dishDto;
}).collect(Collectors.toList());

// 将数据缓存到redis中,避免二次查询(并设置缓存时间为60分钟)
redisTemplate.opsForValue().set(key,dishDtoList,60, TimeUnit.MINUTES);
return Result.success(dishDtoList);
}

为save和update方法加入清理缓存的逻辑

1
2
3
4
5
6
7
8
@PostMapping
public Result<String> save(@RequestBody DishDto dishDto) {
dishService.saveWithFlavor(dishDto);
String key = "dish_" + dishDto.getCategoryId() + "_1";
// 删除之前的key,也就是清除缓存,之前的内容就不存在了,会去数据库中重新查找
redisTemplate.delete(key);
return Result.success("新增菜品成功");
}
1
2
3
4
5
6
7
8
@PutMapping
public Result<String> update(@RequestBody DishDto dishDto) {
dishService.updateWithFlavor(dishDto);
String key = "dish_" + dishDto.getCategoryId() + "_1";
// 删除之前的key,也就是清除缓存,之前的内容就不存在了,会去数据库中重新查找
redisTemplate.delete(key);
return Result.success("更新菜品成功");
}

这里我还没有写这个停售起售的功能,所以就没有进行更改,后期项目做完了再回头来补

注意:这里并不需要我们对删除操作也进行缓存清理,因为删除操作执行之前,必须先将菜品状态修改为停售,而停售状态也会帮我们清理缓存,同时也看不到菜品,随后将菜品删除,仍然看不到菜品,故删除操作不需要进行缓存清理

功能测试

在进行测试前,我们需要做一件事,就是修改RedisConfig类中之前写的值的序列化器统一,之前把他们统一成String了,所以就会只能返回String,我们去注释掉里面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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.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());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 设置值的序列化器统一
// redisTemplate.setValueSerializer(new StringRedisSerializer());
// redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);

return redisTemplate;
}
}

然后启动测试

image-20230806210554043

此时,内容就都展现出来了

image-20230806210641955

并且,我们也能在redis中查询得到菜品的数据,其他内容我就不测试了,有兴趣可以自己测一下

最后,我们将这次写的代码push到github上

Spring Cache

4.1Spring Cache介绍

  • SpringCache是一个框架,实现了基本注解的缓存功能,只需要简单的添加一个注解,就能实现缓存功能
  • SpringCache提供了一层抽象,底层可以切换不同的cache实现,具体就是通过CacheManager接口来统一不同的缓存技术
  • 针对不同的缓存技术,需要实现不同的CacheManager
CacheManger 描述
EhCacheCacheManager 使用EhCache作为缓存技术
GuavaCacheManager 使用Googke的GuavaCache作为缓存技术
RedisCacheManager 使用Rdis作为缓存技术

SpringCache使用方式

在springboot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可

如果只是使用SpringCache的基础功能,只需要导入spring-boot-starter-web这个包就可以了

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>

使用Redis作为缓存技术,需要导入Spring data Redis的maven坐标即可

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

随后配置application.yml

1
2
3
4
5
6
7
8
9
10
11
spring:
redis:
host: localhost #这里换成localhost或者你自己的linux上装的redis
# 这里我没有密码的原因是之前在缓存菜品数据的时候告诉我需要密码,我感觉有点麻烦就把密码去掉了
# 有需要可以自己设置一下
# password: 123456
port: 6379
database: 0
cache:
redis:
time-to-live: 3600000 #设置缓存存活时间为一小时,如果不设置,则一直存活

Spring Cache常用注解

注解 说明
@EnableCaching 开启缓存注解功能
@Cacheable 在方法执行前spring先查看缓存中是否有数据。如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
@CachePut 将方法的返回值放到缓存中
@CacheEvict 将一条或者多条数据从缓存中删除

@Cacheable

@Cacheable的作用主要针对在方法执行前spring先查看缓存中是否有数据。如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中,其主要参数说明如下

注解 说明 举例
value 缓存的名称,每个缓存的名称下面可以有多个Key,必须指定至少一个value 例如:@Cacheable(value=”mycache”)或者@Cacheable(value=(“cache7”, “cache2”]
key 缓存的key,可以为空 例如:@Cacheable(value=”testcache”,key=”#userName”)
condition 缓存的条件,可以为空,返回true或者false,只有为true 才进行缓存 例如:@Cacheable(value=”testcache”,condition=”#userName.length()>2”)
unless 满足条件则不缓存 例如:@Cacheable(value = “userCache”,key = “#id”,unless = “#result == null”)

@CachePut

@CachePut的作用主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,将方法的返回值放入缓存

注解 说明 举例
value 缓存的名称,每个缓存的名称下面可以有多个Key,必须指定至少一个value 例如:@CachePut(value = {“userCache”,”userCacher2”}
key 缓存的key,可以为空 例如:@CachePut(value = {“userCache”,”userCacher2”},key = “#user.id”)
condition 缓存的条件,可以为空,返回true或者false,只有为true 才进行缓存 例如:@CachePut(value=”testcache”,condition=”#userName.length()>2”

这边的key可以通过里面的参数名.属性的形式来获取,使用**#参数名.属性**就可以将数据放入缓存中

当整个方法执行完后,通过返回的值来给key赋上值,缓存的值是被返回的整个对象

例如:

1
2
3
4
5
6
7
8
9
@CachePut(value = {"userCache","userCacher2"},key = "#user.id")
@PostMapping
public User save(User user){
userService.save(user);
// user是参数名,通过user.id就可以得到用户的id
// 当整个方法执行完后,就会将key赋上值
// 缓存的值是返回的整个对象,而key是自己设定的缓存key
return user;
}

@CacheEvict

@CacheEvict的作用是清除缓存

注解 说明 举例
value 缓存的名称,每个缓存的名称下面可以有多个Key,必须指定至少一个value 例如:@CacheEvict(value=”mycache”)或者@CacheEvict(value={“cache1”, “cache2”]
key 缓存的key,可以为空 例如:@CacheEvict(value=”testcache”,key=”#userName”)
condition 缓存的条件,可以为空,返回true或者false,只有为true 才进行缓存 例如:@CacheEvict(value=”testcache”,condition=”#userName.length()>2”)

@CacheEvict可以清除某个缓存名称下的key缓存数据

例如:

1
2
3
4
5
6
7
8
// 如果key为#p1,#p2这种,代表有多参数,根据p后面的数字来决定的
@CacheEvict(value = "userCache",key = "#p0") // #p0代表第一次参数id
//@CacheEvict(value = "userCache",key = "#root.args[0]") 这个与#p1,#p2方法类似,也是参数列表
//@CacheEvict(value = "userCache",key = "#id") 通过名称获取
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id){
userService.removeById(id);
}

缓存套餐数据

在做套餐之前,我们可以先用注解的方式去缓存菜品数据

先在main下开启缓存功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Slf4j
@SpringBootApplication
@ServletComponentScan
@EnableTransactionManagement
@EnableCaching
public class TakeOutApplication {

public static void main(String[] args) {
SpringApplication.run(TakeOutApplication.class, args);
log.info("项目启动成功...");
}

}

然后来到DishController的list方法下进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@GetMapping("/list")
@Cacheable(value="DishCache",key = "#dish.getCategoryId() + '_' + #dish.getStatus()",unless = "#result == null")
// 先将返回值类型改为List<DishDto>
public Result<List<DishDto>> list(Dish dish){
// 将dishDtoList作为内容缓存到Redis中
List<DishDto> dishDtoList = null;

// 如果不存在,就需要进行查询,并使用redis加以缓存
// 以下代码都是进行数据查询

LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
// 得到该菜品项对应的菜品
queryWrapper.eq(dish.getCategoryId() != null,Dish::getCategoryId,dish.getCategoryId());

// 添加条件,查询状态为1(起售状态)的菜品
queryWrapper.eq(Dish::getStatus,1);

//添加排序条件(先按照sort来排序,如果sort相同,再按照更新时间来排序)
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

List<Dish> list = dishService.list(queryWrapper);

dishDtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
// 分类id
Long categoryId = item.getCategoryId();
// 根据Id查询分类对象
Category category = categoryService.getById(categoryId);

if (category != null){
// 如果分类对象查询到了,说明该菜品有分类
String categoryName = category.getName();
// 就让菜品设置一下这个分类对象名字
dishDto.setCategoryName(categoryName);
}

// 得到菜品的id
Long itemId = item.getId();

LambdaQueryWrapper<DishFlavor> wrapper = new LambdaQueryWrapper<>();
// 查找与当前菜品id相同的口味信息
wrapper.eq(DishFlavor::getDishId,itemId);
List<DishFlavor> flavors = dishFlavorService.list(wrapper);
// 设置菜品口味
dishDto.setFlavors(flavors);
return dishDto;
}).collect(Collectors.toList());

return Result.success(dishDtoList);
}

在做缓存的时候报了一个错误DefaultSerializer requires a Serializable payload but received an object of type

解决方法:

为Result对象实现一个Serializable接口即可,因为Spring 会将对象先序列化再存入 Redis,所以需要实现这个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> implements Serializable {
private Integer code; // 编码:1成功。0和其他数字失败
private String msg; // 错误信息
private T data; // 数据
private Map map = new HashMap(); // 动态数据

public static <T> Result<T> success(T data) {
Result<T> r = new Result<>();
r.code = 1; //成功状态码
r.data = data;
return r;
}

public static <T> Result<T> error(String errMsg) {
Result<T> r = new Result<>();
r.msg = errMsg; //设置错误信息
r.code = 0; //默认失败状态码,后期我们可以根据自己的需求来设置其他状态码
return r;
}

public Result<T> add(String msg, String value) {
this.map.put(msg, value);
return this;
}
}

记得删除缓存,在方法中删除缓存使用@CacheEvict

save:

1
2
3
4
5
6
7
8
9
@PostMapping
@CacheEvict(value = "DishCache",key = "#dishDto.getCategoryId() + '_1'")
public Result<String> save(@RequestBody DishDto dishDto) {
dishService.saveWithFlavor(dishDto);
String key = "dish_" + dishDto.getCategoryId() + "_1";
// 删除之前的key,也就是清除缓存,之前的内容就不存在了,会去数据库中重新查找
redisTemplate.delete(key);
return Result.success("新增菜品成功");
}

update:

1
2
3
4
5
6
@PutMapping
@CacheEvict(value = "DishCache",key = "#dishDto.getCategoryId() + '_1'")
public Result<String> update(@RequestBody DishDto dishDto) {
dishService.updateWithFlavor(dishDto);
return Result.success("更新菜品成功");
}

至于为什么不给delete清缓存,在3.3功能测试有讲

这里我们的菜品就全部重新用注解重写了

再来写套餐的,写法是类似的,这里就不做说明了,直接给上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@GetMapping("/list")
@Cacheable(value = "SetmealCache",key = "#setmeal.categoryId + '_' + #setmeal.status",unless = "#result == null ")
public Result<List<SetmealDto>> list(Setmeal setmeal){
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
// 得到该套餐项对应的菜品
queryWrapper.eq(setmeal.getCategoryId() != null,Setmeal::getCategoryId,setmeal.getCategoryId());

// 添加条件,查询状态为1(起售状态)的菜品
queryWrapper.eq(Setmeal::getStatus,1);

//添加排序条件(按照更新时间来排序)
queryWrapper.orderByDesc(Setmeal::getUpdateTime);

List<Setmeal> list = setMealService.list(queryWrapper);

List<SetmealDto> setmealDtoList = list.stream().map((item) -> {
SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(item,setmealDto);
// 分类id
Long categoryId = item.getCategoryId();
// 根据Id查询分类对象
Category category = categoryService.getById(categoryId);

if (category != null){
// 如果分类对象查询到了,说明该套餐有分类
String categoryName = category.getName();
// 就让套餐设置一下这个分类对象名字
setmealDto.setCategoryName(categoryName);
}

// 得到套餐的id
Long itemId = item.getId();

LambdaQueryWrapper<SetmealDish> wrapper = new LambdaQueryWrapper<>();
// 查找与当前套餐id相同的口味信息
wrapper.eq(SetmealDish::getDishId,itemId);
List<SetmealDish> flavors = setmealDishService.list(wrapper);
// 设置菜品口味
setmealDto.setSetmealDishes(flavors);
return setmealDto;
}).collect(Collectors.toList());
return Result.success(setmealDtoList);
}
1
2
3
4
5
6
7
@PostMapping
@CacheEvict(value = "SetmealCache",,key = "#setmealDto.categoryId + '_' + '_1'")
public Result<String> save(@RequestBody SetmealDto setmealDto) {
log.info("套餐信息:{}", setmealDto);
setMealService.saveWithDish(setmealDto);
return Result.success("套餐添加成功");
}

update方法也没写,哎,后期学完补上

将代码push到仓库里

读写分离

问题分析

  • 目前我们所有的读和写的压力都是由一台数据库来承担,
  • 如果数据库服务器磁盘损坏,则数据会丢失(没有备份)
  • 解决这个问题,就可以用MySQL的主从复制,写操作交给主库,读操作交给从库
  • 同时将主库写入的内容,同步到从库中
image-20230807164256580

MySql主从复制

介绍

  • MySQL主从复制是一个异步的复制过程,底层是基于Mysql数据库自带的二进制日志功能。就是一台或多台NysQL数据库(slave,即从库)从另一台MySQL数据库(master,即主库)进行日志的复制然后再解析日志并应用到自身,最终实现从库的数据和主库的数据保持一致。MySQL主从复制是MySQL数据库自带功能,无需借助第三方工具
    • 讲一下自己的理解,由从库(slave)向主库(master)进行日志的复制,再解析日志,解析完之后,就知道在主库里执行的是一个什么样的sql语句,然后在从库(slave)里面再执行一遍刚才得到的sql语句
  • MySQL复制过程分成三步:
    1. master将改变记录到二进制日志(binary log)
    2. slavemasterbinary log拷贝到它的中继日志(relay log)
    3. slave重做中继日志中的事件,将改变应用到自己的数据库中

image-20230807170535714

配置

前置条件
准备好两台服务器,分别安装MySQL并启动服务成功,这里准备两台虚拟机,建议使用克隆

我们先得到主库和从库的IP地址

进入系统,打开命令行,输入ifconfig查看IP地址

  • 主库IP地址:192.168.10.135
  • 从库IP地址:192.168.10.134

然后去mysql中连接这两个的数据库

如果在连接数据库时出现这样一个问题:

image-20230807175956288

vim /etc/my.cnf

在[mysqld]后添加[skip-grant-tables(登录时跳过权限检查)

image-20230807180136928

重启MySQL服务:sudo systemctl restart mysqld

然后输入mysql就可以进去了,在里面就可以改密码干啥了,改了密码再重新连接一下就行,或者不搞密码直接搞连接也是可以的

image-20230807180505474

到数据库中连接,发现这里也可以了,我就没有设置密码,感觉设置密码比较麻烦,后面再来考虑密码的问题

image-20230807180542967

数据库搞好之后,我们用Xshell来连接一下自己的服务器,一个主库,一个从库

image-20230807181110947

OK,这里连接完毕,确认一下mysql服务是否开启

image-20230807181309551

配置主库

修改Mysql数据库的配置文件/etc/my.cnf

1
vim /etc/my.cnf

image-20230807181738719

在mysqld下加入如下内容,skip-grant-tables是我们之前加上来用于跳过权限检查的

1
2
log-bin=mysql-bin  #[必须]启用二进制日志
server-id=100 #[必须]服务器唯一ID,值不是固定的,你只要保证唯一就行

因为刚刚改了配置文件,所以我们一下重启Mysql服务

1
systemctl restart mysqld

这时候我们再登陆Mysql数据库,执行下面的SQL

1
grant replication slave on *.* to 'eastwind'@'%' identified by '1234';

在执行SQL的时候报了个异常:The MySQL server is running with the --[skip-grant-tables] option so it cannot execute this statement

解决方法:

先刷新一下权限表,把在所有数据库的所有表的所有权限赋值给位于所有IP地址的root用户。

1
flush privileges;

image-20230807183641047

再执行就没问题了

注:上面SQL的作用是创建一个用户eastwind,密码为1234,并且给eastwind用户授予REPLICATION SLAVE权限。常用于建立复制时所需要用到的用户权限,也就是slave必须被master授权具有该权限的用户,才能通过该用户复制。

接着登录Mysql数据库,执行下面的SQL,记录下结果File和Position的值

1
show master status
1
2
3
4
5
6
7
mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000001 | 612 | | | |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)

注:上面的SQL的作用是查看Master的状态,执行完该SQL后不要再执行任何操作,因为我们在执行其他任何操作时,里面的日志会变化,所以不要执行其他任何操作

配置从库

修改配置文件

跟配置从表差不多,也是在文件里面加入一点内容

1
vim /etc/my.cnf
1
server-id=101 	#[必须]服务器唯一ID
image-20230807210445457

接着重启mysql服务

1
systemctl restart mysqld

登陆Mysql数据库,执行下面的SQL

1
change master to master_host='192.168.10.135',master_user='eastwind',master_password='1234',master_log_file='mysql-bin.000001',master_log_pos=612;

简单解释一下这里的配置,master_host对应主库的ip地址,master_user对应主库的用户名,master_password对应主库的密码,master_log_file对应之前主库查出来的File,master_log_pos对应对应之前主库查出来的Position,根据自己的情况修改

然后执行这条SQL

1
start slave;

最后执行一条SQL查看一下slave的状态

1
show slave status\G;

本来是show slave status但是显示的不太美观,所以加上\G格式化输出

image-20230807213311786

这两个是必须是yes的,并且Slave IO State 什么也没有,我们发现这里其中一个是no,在翻看了文档后发现是克隆机的问题,我们需要修改克隆机的uuid,现在我们修改一下uuid

1
2
3
4
5
6
mysql> select uuid();
+--------------------------------------+
| uuid() |
+--------------------------------------+
| a499587b-3525-11ee-b696-000c29daa809 |
+--------------------------------------+

查询克隆机的uuid,并记住它

接着查看配置文件目录

1
2
3
4
5
6
mysql> show variables like "datadir";
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| datadir | /var/lib/mysql/ |
+---------------+-----------------+

编辑配置文件目录,修改uuid为刚刚我们生成的uuid

1
vi /var/lib/mysql/auto.cnf

重启服务

1
systemctl restart mysqld

再次进入mysql查看slave的状态

image-20230807213409815

此时发现这两个都是Yes了,并且Slave IO State也有了对应的内容,说明我们的配置完成了

测试

在主库中新建数据库,刷新一下从库

image-20230808072609590

继续在主库中进行测试,新建一张表,对表进行一下增删改操作,看看从库中是否有变化,如果有变化说明配置的没有问题,这里其他的测试我就不展示了,挺简单的

读写分离案例

背景

面对日益增加的系统访问量,数据库的吞吐量面临着巨大的瓶颈。对于同一时刻有大量并发读操作较少的写操作类型的应用系统来说,将数据库拆分为主库从库主库主要负责处理事务性的增删改操作,从库主要负责查询操作,这样就能有效避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善

image-20230808074358625

Sharding-JDBC介绍

  • Sharding-JDBC定位为轻量级的JAVA框架,在JAVA的JDBC层提供额外的服务,它使得客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架

  • 使用Sharding-JDBC可以在程序中轻松的实现数据库读写分离

    • 适用于任何基于JDBC的ORM框架
    • 支持任何第三方的数据库连接池
    • 支持任意实现JDBC规范的数据库
  • 使用Sharding-JDBC框架的步骤

    1. 导入对应的maven坐标

      1
      2
      3
      4
      5
      <dependency>
      <groupId>org.apache.shardingsphere</groupId>
      <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
      <version>4.0.0-RC1</version>
      </dependency>
    2. 在配置文件中配置读写分离规则,并配置允许bean定义覆盖配置项

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      spring:
      shardingsphere:
      datasource:
      names:
      # 这里的master和slave并不是写死的,但是需要跟下面的master和slave对应
      # 定义了两个数据源,名叫master和slave
      master,slave
      # 主数据源
      master:
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
      # 主库ip及连接的数据库名
      url: jdbc:mysql://192.168.10.135:3306/reggie?serverTimezone=UTC&useSSL=false
      username: root
      # 我的数据库没有密码,所以这里注释了,有需要可以自己调整
      # password: root
      # 从数据源
      slave:
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
      # 从库ip及连接的数据库名
      url: jdbc:mysql://192.168.10.134:3306/reggie?serverTimezone=UTC&useSSL=false
      username: root
      # password: root
      masterslave:
      # 读写分离配置
      # 负载均衡:配置的是从库的负载均衡策略(轮询策略)
      # 轮询:从库可以有多个,假设有3个从库,第一次走sql查询就是走1号库,第二次走2号,这样以此类 推,直到走完之后再次重复一遍,说白了就是按顺序来
      load-balance-algorithm-type: round_robin
      # 最终的数据源名称
      name: dataSource
      # 指定主库数据源名称
      master-data-source-name: master
      # 指定从库数据源名称列表,多个从库用逗号分隔
      slave-data-source-names: slave
      props:
      sql:
      show: true #开启SQL显示,默认false,就是在控制台可以输出sql
      # 配置允许bean定义覆盖配置项
      main:
      allow-bean-definition-overriding: true

项目实现读写分离

之前已经搭建好了主从复制的数据库,现在到主库去搭建项目的数据库

image-20230808085658273

相应的,从库里面也会创建,检查一下就行

然后运行一下之前的sql文件代码

image-20230808085953402

记得把这个下面这个读写分离的规则修改成改变之后的

导入对应的maven坐标

1
2
3
4
5
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>

在配置文件中配置读写分离规则,并配置允许bean定义覆盖配置项

配置项可能会爆红,但是不影响影响项目启动,是IDEA的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
spring:
shardingsphere:
datasource:
names:
# 这里的master和slave并不是写死的,但是需要跟下面的master和slave对应
# 定义了两个数据源,名叫master和slave
master,slave
# 主数据源
master:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
# 主库ip及连接的数据库名
url: jdbc:mysql://192.168.10.135:3306/reggie?serverTimezone=UTC&useSSL=false
username: root
# 我的数据库没有密码,所以这里注释了,有需要可以自己调整
# password: root
# 从数据源
slave:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
# 从库ip及连接的数据库名
url: jdbc:mysql://192.168.10.134:3306/reggie?serverTimezone=UTC&useSSL=false
username: root
# password: root
masterslave:
# 读写分离配置
# 负载均衡:配置的是从库的负载均衡策略(轮询策略)
# 轮询:从库可以有多个,假设有3个从库,第一次走sql查询就是走1号库,第二次走2号,这样以此类 推,直到走完之后再次重复一遍,说白了就是按顺序来
load-balance-algorithm-type: round_robin
# 最终的数据源名称
name: dataSource
# 指定主库数据源名称
master-data-source-name: master
# 指定从库数据源名称列表,多个从库用逗号分隔
slave-data-source-names: slave
props:
sql:
show: true #开启SQL显示,默认false,就是在控制台可以输出sql
# 配置允许bean定义覆盖配置项
main:
allow-bean-definition-overriding: true

测试一下,主要检查的查询语句是不是由从库发出,以及增删改操作是不是由主库发出即可

最后我们将代码上传到github,

然后合并到master分支

如何合并?

先回到master分支,点一下master然后checkout就可以切回去了

image-20230808093718695

然后选中v1.0,选中Merge,合并到master

image-20230808093838512

查看master分支下的yml配置文件,发现已经更新了

image-20230808094010705

Nginx

简介

  • Nginx是一款轻量级的Web/反向代理服务器以及电子邮件(IMAP/POP3)代理服务器,其特点是占有内存少,并发能力强。
  • 事实上Nginx的并发能力在同类型的网页服务器中表现较好,中国大陆使用Nginx的网站有:百度、京东、新浪、网易、腾讯、淘宝等。
  • Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的Rambler.ru站点(俄文:Pam6nep)开发的,第一个公开版本0.1.0发布于2004年10月4日。
  • 官网:https://nginx.org/

Nginx的下载和安装

Nginx是C语言开发的,所以需要先安装依赖

1
yum -y install gcc pcre-devel zlib-devel openssl openssl-devel

安装时遇到一个问题:Another app is currently holding the yum lock; waiting for it to exit…

说是另一个应用程序目前持有yum锁;等待它退出

解决方法:

1
2
3
# 强行解除锁定
rm -rf /var/run/yum.pid
# 再次yum安装

下载Nginx安装包

这里我去了nginx官网查看了现在的稳定版本,现在是1.24.0所以我也选择1.24.0

image-20230808100050448
1
wget https://nginx.org/download/nginx-1.24.0.tar.gz

可以使用命令行方式,你也可以通过在Windows下载好之后传上来,我是直接上传了

解压,放在/usr/local目录下

1
tar -zxvf nginx-1.24.0.tar.gz -C /usr/local/

进入到我们解压完毕后的文件夹内

1
cd /usr/local/nginx-nginx-1.24.0/

创建安装路径文件夹

1
mkdir /usr/local/nginx

安装前检查工作

使用nginx-nginx-1.24.0目录下的configure来指定安装目录(不是真正的安装,只是安装前的检查工作)

1
./configure --prefix=/usr/local/nginx

编译并安装

1
make && make install

Nginx目录结构

  • 安装完Nginx后,我们先来熟悉一下Nginx的目录结构
  • 重点目录/文件:
    • conf
      • nginx配置文件
      • 操作较多的是里面的nginx.conf
    • html
      • 存放静态文件(html、css、Js等)
    • logs
      • 日志目录,存放日志文件
    • sbin/nginx
      • 二进制文件,用于启动、停止Nginx服务

可以使用一个命令来展示当前目录下的内容

先安装这个tree

1
yum install tree	

再输入tree

以树形结构来展示当前目录下的所有内容

image-20230808103138800

Nginx配置文件结构

  • Nginx配置文件(conf/nginx.conf)整体分为三部分
    • 全局块 和Nginx运行相关的全局配置
    • events块 和网络连接相关的配置
    • http块 代理、缓存、日志记录、虚拟主机配置
      • http全局块
      • Server块
        • Server全局块
        • location块

注意:http块中可以配置多个Server块,每个Server块中可以配置多个location块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
worker_processes  1;                             <-- 全局块

events { <-- events块
worker_connections 1024;
}

http { <-- http块
include mime.types; <-- http全局块
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;

server { <-- Server块
listen 80; <-- Server全局块
server_name localhost;
location / { <-- location块
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}

Nginx命令

查看版本

  • 进入sbin目录,输入./nginx -v
1
2
[root@localhost sbin]# ./nginx -v
nginx version: nginx/1.24.0

检查配置文件正确性

进入sbin目录,输入./nginx -t,如果有错误会报错,而且也会记录日志

一般在启动Nginx服务之前检查

1
2
3
[root@localhost sbin]# ./nginx -t
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful

启动和停止

进入sbin目录,输入./nginx,启动完成后查看进程

1
2
3
4
5
[root@localhost sbin]# ./nginx 
[root@localhost sbin]# ps -ef | grep nginx
root 13485 1 0 19:36 ? 00:00:00 nginx: master process ./nginx
nobody 13486 13485 0 19:36 ? 00:00:00 nginx: worker process
root 13496 9863 0 19:36 pts/1 00:00:00 grep --color=auto nginx

如果想停止Nginx服务,输入./nginx -s stop,停止服务后再次查看进程

1
2
3
[root@localhost sbin]# ./nginx -s stop
[root@localhost sbin]# ps -ef | grep nginx
root 13499 9863 0 19:37 pts/1 00:00:00 grep --color=auto nginx

重新加载配置文件

重新加载配置文件

  • 当修改Nginx配置文件后,需要重新加载才能生效,可以使用下面命令重新加载配置文件:./nginx -s reload

上面的所有命令,都需要我们在sbin目录下才能运行,比较麻烦,所以我们可以将Nginx的二进制文件配置到环境变量中,这样无论我们在哪个目录下,都能使用上面的命令

使用vim /etc/profile命令打开配置文件,并配置环境变量,保存并退出

这个需要配置jdk的环境,记得改成自己的

1
2
JAVA_HOME=/usr/local/jdk1.8.0_212
PATH=/usr/local/nginx/sbin:$JAVA_HOME/bin:$PATH

之后重新加载配置文件,使用source /etc/profile命令,然后我们在任意位置输入nginx即可启动服务,nginx -s stop即可停止服务

1
2
3
4
5
6
7
8
9
10
[root@localhost jdk1.8.0_212]# vim /etc/profile
[root@localhost jdk1.8.0_212]# source /etc/profile
[root@localhost jdk1.8.0_212]# nginx
[root@localhost jdk1.8.0_212]# ps -ef | grep nginx
root 13942 1 0 19:51 ? 00:00:00 nginx: master process nginx
nobody 13943 13942 0 19:51 ? 00:00:00 nginx: worker process
root 13945 9863 0 19:51 pts/1 00:00:00 grep --color=auto nginx
[root@localhost jdk1.8.0_212]# nginx -s stop
[root@localhost jdk1.8.0_212]# ps -ef | grep nginx
root 13956 9863 0 19:52 pts/1 00:00:00 grep --color=auto nginx

查看自己IP,启动服务后,浏览器输入ip地址就可以访问Nginx的默认页面

  • ifconfig

如果发现自己连不上Nginx的话,可能是防火墙的问题,这里我也遇到了这个问题,所以我们需要开放80端口,并重启防火墙

1
firewall-cmd --zone=public --add-port=80/tcp --permanent
1
systemctl restart firewalld.service

重启完成后,在浏览器中输入ip即可访问

image-20230808111005173

Nginux具体应用

部署静态资源

  • Nginx可以作为静态web服务器来部署静态资源。静态资源指在服务端真实存在并且能够直接展示的一些文件,比如常见的html页面、css文件、js文件、图片、视频等资源。
  • 相对于Tomcat,Nginx处理静态资源的能力更加高效,所以在生产环境下,一般都会将静态资源部署到Nginx中。
  • 将静态资源部署到Nginx非常简单,只需要将文件复制到Nginx安装目录下的html目录中即可。
image-20230808134539268

反向代理

正向代理

  • 正向代理是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。
  • 正向代理的典型用途是为在防火墙内的局域网客户端提供访问Internet的途径。
  • 正向代理一般是在客户端设置代理服务器,通过代理服务器转发请求,最终访问到目标服务器。
image-20230808134722027

总结一下正向代理,其实很好理解,客户端直接访问原始服务器是访问不到的,得依靠代理服务器来访问,所以客户端需要先向代理服务器发送请求来指定原始服务器,然后代理服务器向原始服务器转交请求,原始服务器将内容发给代理服务器,再依靠代理服务器来返回给客户端

反向代理

  • 反向代理服务器位于用户与目标服务器之间,但是对于用户而言,反向代理服务器就相当于目标服务器,即用户直接访问反向代理服务器就可以获得目标服务器的资源,反向代理服务器负责将请求转发给目标服务器。
  • 用户不需要知道目标服务器的地址,也无须在用户端作任何设定。
image-20230808134745099

简单来说,正向代理是:(客户端+代理服务器)访问(web服务器),, 反向代理是:(客户端)访问(代理服务器+web服务器)

反向代理直接访问的是代理服务器,然后让代理服务器去web服务器里转发给你

配置反向代理

1
2
3
4
5
6
7
8
9
server {
listen 82;
server_name localhost;

location / {
# 监听82端口,访问82端口则代理转发到下面的地址
proxy_pass http://IP地址:端口号;
}
}

负载均衡

  • 早期的网站流量和业务功能都比较简单,单台服务器就可以满足基本需求,但是随着互联网的发展,业务流量越来越大并且业务逻辑也越来越复杂,单台服务器的性能及单点故障问题就凸显出来了,因此需要多台服务器组成应用集群,进行性能的水平扩展以及避免单点故障出现。
  • 应用集群:将同一应用部署到多台机器上,组成应用集群,接收负载均衡器分发的请求,进行业务处理并返回响应数据。
  • 负载均衡器:将用户请求根据对应的负载均衡算法分发到应用集群中的一台服务器进行处理。
image-20230808140916732

配置负载均衡
默认是轮询算法,第一次访问是IP地址1,第二次访问是IP地址2
也可以改用权重方式,权重越大,几率越大,现在的访问三分之二是第一台服务器接收,三分之一是第二台服务器接收
server IP地址1 weight=10
server IP地址2 weight=5

proxy_pass要与targetServer一致

1
2
3
4
5
6
7
8
9
10
11
12
upstream targetServer{
server IP地址1:端口号;
server IP地址2:端口号;
}
server {
listen 82;
server_name localhost;

location / {
proxy_pass http://targetServer;
}
}
  • 负载均衡策略
名称 说明
轮询 默认方式
weight 权重方式
ip_hash 依据ip分配方式
least_conn 依据最少连接方式
url_hash 依据url分配方式
fair 依据响应时间方式

Nginx的特点

  1. 跨平台:Nginx可以在大多数操作系统中运行,而且也有Windows的移植版本
  2. 配置异常简单:非常容易上手。配置风格跟程序开发一样,神一般的配置
  3. 非阻塞、高并发:数据复制时,磁盘I/O的第一阶段是非阻塞的。官方测试能够支撑5万并发连接,在实际生产环境中跑到2-3万并发连接数(这得益于Nginx使用了最新的epoll模型)
  4. 事件驱动:通信机制采用epoll模式,支持更大的并发连接数
  5. 内存消耗小:处理大并发的请求内存消耗非常小。在3万并发连接下,开启的10个Nginx进程才消耗150M内存(15M*10=150M)
  6. 成本低廉:Nginx作为开源软件,可以免费试用。而购买F5 BIG-IP、NetScaler等硬件负载均衡交换机则需要十多万至几十万人民币
  7. 内置健康检查功能:如果Nginx Proxy后端的某台Web服务器宕机了,不会影响前端访问。
  8. 节省带宽:支持GZIP压缩,可以添加浏览器本地缓存的Header头。
  9. 稳定性高:用于反向代理,宕机的概率微乎其微。

前后端分离开发

问题说明

  • 开发人员同时负责前端和后端代码开发,分工不明确,开发效率低
  • 前后端代码混合在一个工程中,不便于管理
  • 对开发人员要求高,人员招聘困难
  • 所以衍生出了一种前后端分离开发

介绍

  • 前后端分离开发,就是在项目开发过程中,对前端代码的开发,专门由前端开发人员负责,后端代码由后端开发人员负责,这样可以做到分工明确,各司其职,提高开发效率,前后端代码并行开发,可以加快项目的开发速度。目前,前后端分离开发方式已经被越来越多的公司采用了,成为现在项目开发的主流开发方式。
  • 前后端分离开发后,从工程结构上也会发生变化,即前后端代码不再混合在同一个maven工程中,而是分为前端工程和后端工程
image-20230808150333666

开发流程

  • 前后端开发人员都参照接口API文档进行开发
  • 接口(API接口) 就是一个http的请求地址,主要就是去定义:请求路径、请求方式、请求参数、响应参数等内容。
image-20230808150400991

YApi

介绍

Api是高效、易用、功能强大的api管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护API,YApi还为用户提供了优秀的交互体验,开发人员只需要利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。

YApi让接口开发更简单高效,让接口的管理更具有可读性、可维护性,让团队协作更合理。

Git仓库:https://github.com/YMFE/yapi

使用

使用YApi,可以执行下面操作:

  • 添加项目
  • 添加分类
  • 添加接口
  • 编辑接口
  • 查看接口

这个后期看看文档自学一下即可

Swagger

介绍

  • 使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,再通过Swagger衍生出来的一系列项目和工具,就可以做成各种格式的接口文档,以及在线接口调试页面等。
  • 官网:https://swagger.io/

使用方式

  1. 导入对应的maven坐标
1
2
3
4
5
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>

导入knife4j相关配置,并配置静态资源映射,否则接口文档页面无法访问,注意将controller的包路径修改为你自己的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Configuration
@Slf4j
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射...");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}

@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转化器,底层使用jackson将java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合当中(index设置为0,表示设置在第一个位置,避免被其它转换器接收,从而达不到想要的功能)
converters.add(0, messageConverter);
}

@Bean
public Docket createRestApi() {
//文档类型
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.eastwind.controller"))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("瑞吉外卖")
.version("1.0")
.description("瑞吉外卖接口文档")
.build();
}
}

在拦截器在中设置不需要处理的请求路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//定义不需要处理的请求
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**",
"/common/**",
//对用户登陆操作放行
"/user/login",
"/user/sendMsg",

"/doc.html",
"/webjars/**",
"/swagger-resources",
"/v2/api-docs"
};

启动服务,访问 http://localhost/doc.html ,我这里的端口号用的80,根据自己的需求改,运行之前记得把linux的服务开起来

这里我启动时报了一个异常,

Failed to start bean ‘documentationPluginsBootstrapper’; nested exception is java.lang.NullPointerException

springboot 升级到 2.6.0之后,swagger版本和springboot出现了不兼容情况,因为SpringBoot处理映射匹配的默认策略发生变化:请求路径与 Spring MVC 处理映射匹配的默认策略已从AntPathMatcher更改为PathPatternParser

在application.yml中配置,加在spring的下面,注意层级关系

1
2
3
mvc:
pathmatch:
matching-strategy: ant_path_matcher

访问接口文档image-20230808155517784

此时就显示出来了

常用注解

注解 说明
@Api 用在请求的类上,例如Controller,表示对类的说明
@ApiModel 用在类上,通常是个实体类,表示一个返回响应数据的信息
@ApiModelProperty 用在属性上,描述响应类的属性
@ApiOperation 用在请求的方法上,说明方法的用途、作用
@ApilmplicitParams 用在请求的方法上,表示一组参数说明
@ApilmplicitParam 用在@ApilmplicitParams注解中,指定一个请求参数的各个方面

加上这些注解,可以将我们生成的接口文档更规范,具体使用效果可以看看文档,这里不做太多介绍

项目部署

配置环境说明

一共需要三台服务器

  • 192.168.10.135(服务器A)
    • Nginx:部署前端项目、配置反向代理
    • MySql:主从复制结构中的主库
  • 192.168.10.134(服务器B)
    • jdk:运行java项目
    • git:版本控制工具
    • maven:项目构建工具
    • jar:Spring Boot 项目打成jar包基于内置Tomcat运行
    • MySql:主从复制结构中的从库
  • IP地址(服务器C)
    • Redis:缓存中间件

在服务器A中安装Nginx,将前端项目打包目录上传到Nginx的html目录下

修改Nginx配置文件nginx.conf,新增如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
listen 80;
server_name localhost;

location / {
root html/dist;
index index.html;
}
location ^~ /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://192.168.10.134;
}
}

在服务器B中安装JDK,Git,MySql

将项目打成jar包,手动上传并部署(当然你也可以选择git拉取代码,然后shell脚本自动部署)

部署完后端项目之后,我们就能完成正常的登录功能了,也能进入到后台系统进行增删改查操作