前言

博主最近在学习开发后端项目,因为没有做项目的基础,所以找了黑马的瑞吉外卖项目练练手。开始做这个项目的时候还是在学校,最近也是回到了家里。这几天就把在学校里差的内容做完了,从开始做这个项目到结束差不多一个月的时间。而瑞吉外卖这个项目更多的CRUD,调用API和库,总体上功能简单,没有什么难点,也没有高并发的场景可供调优实践。但是在其中还是学习到很多知识,所以写下这篇博客来总结项目。


后端Controller层返回结果统一封装为R对象

后端的Controller层接收完前端的请求后,要返回什么样的结果是需要按情况变化的,但是如果每一个Controller返回的结果是不一样的,前端也要用不同的数据类型来接收。为了避免麻烦,前后端指定统一的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
public class R<T> implements Serializable {

private Integer code; //编码:1成功,0和其它数字为失败

private String msg; //错误信息

private T data; //数据

private Map map = new HashMap(); //动态数据

public static <T> R<T> success(T object) {
R<T> r = new R<T>();
r.data = object;
r.code = 1;
return r;
}

public static <T> R<T> error(String msg) {
R r = new R();
r.msg = msg;
r.code = 0;
return r;
}

public R<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}

}


可以看到我们可以使用这个类中静态方法,并且静态方法是使用了泛型来定义data数据类型的。我们可以举个手机端退出功能的例子来演示下前端发送请求到后端,后端响应成功并把数据封装到R对象中。可以看到在写Controller方法时确定返回数据类型为R类型对象其中数据类型为String。这样后端返回给前端的信息为code=1 data=”退出成功”,之后再由前端对返回的数据进行判断做出下一步操作。

1
2
3
4
5
6
7
8
9
10
11
/**
* 退出功能
* @param session
* @return
*/
@PostMapping("/loginout")
public R<String> loginout(HttpSession session){
//清理保存在session对象中的属性(user)的值
session.removeAttribute("user");
return R.success("退出成功");
}

定义静态资源映射关系

静态资源映射关系主要用于将前端请求的URL路径与后端服务器资源路径进行映射。在Springboot中静态 资源是默认放在static目录下,如果你要把静态资源放在其他目录下,就必须配置静态资源映射关系。否则前端的请求将匹配不到资源。

设置静态资源映射关系后端代码:

1
2
3
4
5
6
7
8
9
10
11
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
//设置静态资源映射(就是启动服务后通过url访问静态资源)
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/resources/")
.addResourceLocations("classpath:/static/")
.addResourceLocations("classpath:/public/");
}

配置消息资源转换器

瑞吉外卖项目中遇到的问题

数据库中表的主键大都是由mybatis-plus的主键自动生成策略的雪花算法生成的,雪花算法生成的是一个Long类型的数字,而雪花算法生成的主键传输到前端的时候会出现精度缺失现象导致前端拿到的id和数据库中的id不一致。那么前端再发出请求无论是通过id查找数据还是修改数据都会因为id不一致而修改失败。

精读缺失的原因

后端使用64位存储长整数 (Long) ,而前端的JavaScript是使用53位来存储此类型数据。因此超过最大值的数,可能会出现问题。

解决方法

Springboot项目前后端资源可以采用json格式字符串,我们可以添加消息资源转换器MessageConverters,将Long类型的数据序列化为字符串,添加后Spring web mvc在处理controller返回值的时候会采用自定义的序列化策略自动将Long类型数据序列化为字符串,这样就可以解决Long类型数据精度缺失问题。

导入fastjson的maven依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.13.graal</version>
</dependency>

编写对象映射器:JacksonObjectMapper

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
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {

public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}


编写完对象映射器后,在配置扩展消息资源转换器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class MyWebMvcConfig extends WebMvcConfigurationSupport {
/*
* 拓展消息资源转换器
* */

@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter messageConverter=new MappingJackson2HttpMessageConverter();
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将我们自定义的消息转换器,添加进行集合中,并把优先级设置为最高
converters.add(0,messageConverter);
}
}


文件上传

什么是文件上传

文件上传也称为upload,是指将本地图片、视频、音频文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发微博,发微信都用到了文件上传功能。

文件上传时,对页面的form表单有如下要求:

  • method=”post”,采用post方式提交数据
  • enctype=”multipart/form-data”,采用multipart格式上传文件
  • type=”file”,使用input的file控件上传

瑞吉外卖项目中的文件上传

瑞吉外卖项目中使用Spring框架在Spring-web包下对文件上传进行封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件。注意这里的basePath的值,他的定义的值为applicaton.yml配置文件中配置的值,这样做的好处是如果后期我们将项目部署到服务器上,我们只需要修改配置文件就可以达到修改文件上传位置的更改,方便维护。

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
@RestController
@RequestMapping("/common")
public class CommonController {
@Value("${reggie.path}")
private String basePath;
@PostMapping("/upload")
//MultipartFile类型的参数即可接收上传的文件
public R<String> upload(MultipartFile file){
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
//System.out.println(file);

//原始文件名,获取原始文件的后缀名称
String originalFilename = file.getOriginalFilename();
//substring获取指定位置字符
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));

//使用UUID重新生成新的文件名,防止文件名称重复造成文件覆盖
String fileName = UUID.randomUUID().toString()+suffix;

//创建一个目录对象
File dir = new File(basePath);
//判断当前目录是否存在
if (!dir.exists()){
//目录不存在,需要创建
dir.mkdir();
}

//将临时文件(.temp)转存到指定位置(/x.jpg)
try {
file.transferTo(new File(basePath+fileName));
} catch (IOException e) {
throw new RuntimeException(e);
}
//回显给前端页面图片文件的名称,供给文件下载方法使用
return R.success(fileName);
}
}

文件下载

什么是文件下载

文件下载也称为download,是指将文件从服务器传输到本地计算机的过程。通过浏览器进行文件下载,通常有两种表现形式:1.以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录。2.直接在浏览器页面展示

其实浏览器上进行文件下载,本质上就是服务端将文件以流的方式写回浏览器的过程


瑞吉项目中的文件下载

查看菜品管理页面中文件上传的组件,img标签展示图片,src代表图片存在的位置

1
2
3
4
5
6
7
8
9
<el-upload class="avatar-uploader"
action="/common/upload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeUpload"
ref="upload">
<img v-if="imageUrl" :src="imageUrl" class="avatar"></img>
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>

可以看到如果菜品管理页面中的文件上传成功,则**保存文件到服务器中然后返回给前端页面文件名称 (请看文件上传代码最后回显给前端文件名称)**。文件上传成功后触发回调函数on-success=”handleAvatarSuccess”,发送Get请求,其中请求参数name的值为文件上传回显中的数据,也就是文件名称。

1
2
3
handleAvatarSuccess (response, file, fileList) {
this.imageUrl = `/common/download?name=${response.data }`
},

我们在后端处理文件下载请求代码:

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
/**
* 文件下载
* @param name
* @param response
*/
@GetMapping("/download")
public void download( String name, HttpServletResponse response){
try {
//输入流,通过输入流读取文件内容(字节)
FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));

//输出流,通过输出流将文件写回浏览器,在浏览器回显展示图片
ServletOutputStream outputStream = response.getOutputStream();

//确定回显的类型
response.setContentType("image/jpeg");

//通过边读边写的方式写回到浏览器中
int len =0;
byte[] bytes=new byte[1024];
while ((len = fileInputStream.read(bytes)) != -1){
outputStream.write(bytes,0,len);
outputStream.flush();
}
//关闭资源
outputStream.close();
fileInputStream.close();

} catch (Exception e) {
throw new RuntimeException(e);
}
}

文件下载,通过文件上传返回给页面的文件名称+basePath找到服务器中存储的文件,然后通过IO流边读边写到浏览器中,这时候imageUrl 在浏览器中展示的就是一副对应的图片,这时候前端再通过img标签进行展示就可以展示我们上传或者服务器中的图片。


Mybatis-Plus的使用

项目中使用Mybatis-Plus

在瑞吉外卖项目中通过使用Mybatis-Plus框架确实明显提高了开发效率,不需要像以往在mapper映射文件中写单独的配置文件mapper.xml。可以简化LambdaQueryWrapper类和LambdaUpdateWrapper类构造查询条件或者修改条件就可以代替在xml配置文件中写sql语句,大大简化开发。同时mapper接口和Service接口和实现类都只需要实现或继承框架指定的类就可以。

使用mybatis-plus我们需要在springboot项目中的application.yml进行相关配置:

1
2
3
4
5
6
7
8
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 #mybatis-plus中提供的雪花算法

mapper接口:

只需要继承mybatis-plus中提供BaseMapper接口就可以实现基础的CRUD操作

1
2
3
@Mapper
public interface DishMapper extends BaseMapper<Dish> {
}

service接口:

1
2
public interface DishService extends IService<Dish> {
}

serviceImpl类:

1
2
3
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
}

Mybatis-plus分页查询组件的使用

Mybatis-plus为我们提供了一个十分好用的分页查询功能,它是基于AOP的思想实现的。要使用这个功能的话,我们只需要写一个配置类,为mybatis-plus提供分页插件拦截器PaginationInnerInterceptor类,对mybatis-plus框架功能进行增强。

  1. 分页插件的配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /*
    * mybatis-plus分页插件的配置
    * */
    @Configuration
    public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor getMybatisPlusInterceptor(){
    MybatisPlusInterceptor mybatisPlusInterceptor=new MybatisPlusInterceptor();
    mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
    return mybatisPlusInterceptor;
    }
    }

  2. 分页插件的使用
    我们只需要返回给页面page类型的数据,那么执行的SQL语句就会加上Limit来实现查询固定数量数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /*
    * 分类数据的分页查询
    * */
    @RequestMapping(value = "/backend/page/category/queryCategoryForPage.do")
    public R<Page<Category>> queryCategoryForPage(Integer page,Integer pageSize){
    //准备分页条件构造器
    Page<Category> pageInfo=new Page<>(page,pageSize);
    //进行排序条件的构造
    //排序条件: 先按type排序,type相同按sort排序
    LambdaQueryWrapper<Category> queryWrapper=new LambdaQueryWrapper<>();
    queryWrapper.orderByAsc(Category::getType,Category::getSort);
    //在进行完分页查询后,会把查询结果回调设置会pageInfo里面
    categoryService.page(pageInfo,queryWrapper);
    return R.success(pageInfo);
    }


Mybatis-plus 提供的公共字段自动填充功能的使用

公共字段的含义:
在数据库表与表中共同含有的字段,在瑞吉外卖项目中如:createUser、createTime、updateUser、updateTime这些字段都是通用的每一张表中都有这些字段。此时如果每个表的每次操作都考虑这些填充字段是十分繁琐的,代码重复率高。mybatis-plus可以通过简单配置MetaObjectHandler (元数据处理器) 就能够在每一个sql语句到达数据库之前检查对象是否有这些字段并进行自动注入。

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
/*
* 自定义元数据对象处理器
* 完成公共字段自动填充功能
* 难点:如何动态的获得当前用户的id
* */
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
if(metaObject.hasSetter("createUser")){
metaObject.setValue("createUser", UserIdContextHolder.getContextHolder());
}
if(metaObject.hasSetter("createTime")){
metaObject.setValue("createTime", DateUtils.formatDateTime(new Date()));
}
}

@Override
public void updateFill(MetaObject metaObject) {
if(metaObject.hasSetter("updateUser")){
metaObject.setValue("updateUser", UserIdContextHolder.getContextHolder());
}
if(metaObject.hasSetter("updateTime")){
metaObject.setValue("updateTime", DateUtils.formatDateTime(new Date()));
}
}
}

除了编写元数据处理器外,还需要在每一个实体类中的属性打上注解:

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
@Data
@TableName("employee")//对应数据库中的表名
@EqualsAndHashCode(callSuper = false)
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;

private Long id;

private String username;

private String name;

private String password;

private String phone;

private String sex;

private String idNumber;//身份证号码

private Integer status;

@TableField(fill = FieldFill.INSERT) //插入时填充字段
private LocalDateTime createTime;

@TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
private LocalDateTime updateTime;

@TableField(fill = FieldFill.INSERT) //插入时填充字段
private Long createUser;

@TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
private Long updateUser;
}

全局异常处理器的使用与配置

当请求发送到controller之后,调用service进行业务操作。一旦报错,我们一般会在controller中使用try-catch进行异常捕获。但是这个方法有一定的弊端,try-catch全部写到一个类中,代码更简洁,复用性更高。

定义全局异常类:

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
/**
* 全局异常处理
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class}) //对Controller进行拦截
@ResponseBody //响应数据封装为json数据
@Slf4j
public class GlobaExceptionHandler {
/**
* 异常处理方法
* @param ex
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
if (ex.getMessage().contains("Duplicate entry")){
String[] split = ex.getMessage().split(" ");//split()主要是用于对一个字符串进行分割成多个字符串数组,这里是使用空格来分割
String msg = split[2]+"已经存在";
return R.error(msg);
}
return R.error("未知错误,请重试");
}

/**
* 自己的异常处理类方法
* @param ex
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
//在日志中打印报错的日志信息
log.error(ex.getMessage());
//回显给页面异常信息
return R.error(ex.getMessage());
}
}

我们也可以在全局异常处理器中定义自己的异常处理方法如上,我们自己定义的CustomException类如下:

1
2
3
4
5
6
7
8
/**
* 自定义业务异常类
*/
public class CustomException extends RuntimeException {
public CustomException(String message){
super(message);//对RuntimeException的引用
}
}

我们在业务层写方法的时候,就可以去抛出我们自定义业务异常类,来被全局异常类获取。举个瑞吉外卖项目中的例子,我们项目后台系统中的分类管理页面中的删除方法。因为分类管理页面展示的是分类信息。这些信息都联系了其他信息。所以如果关联了信息就不能删除,而且要给页面回显信息。因为我们定义了全局异常处理器,所以这里我们抛出了异常,全局异常处理类就会处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 根据id删除,category表中的id对应着Dish表中的category_id,Setmeal表中的Category_id
* @param id
* @return
*/
@Override

public void deleteById(Long id) {
//查询当前分类是否关联了菜品,如果已经关联,抛出一个业务异常
List<DishDto> dishes = dishService.getByCategoryId(id);
if (dishes!=null){
//如果关联了菜品,抛出一个业务异常
throw new CustomException("当前分类下关联了菜品,不能删除");
}

//查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
List<Setmeal> setmeals = setmealService.getByCategoryId(id);
if (setmeals!=null){
//如果关联了套餐,抛出一个业务异常
throw new CustomException("当前分类下关联了套餐,不能删除");
}
//正常删除
categoryDao.deleteById(id);
}

DTO数据传输对象的使用

在WEB项目中经常会遇到一种情况,前端传输的参数在后端controller层中原有的对象无法全部接收到前端传输的所有参数,因此我们可以创建一个原有对象对应的DTO对象继承原有对象,拓展新的属性以便接收前端传输的全部参数。

这一点在后端controller层返回值中也可以体现,瑞吉外卖项目中,controller的返回值封装成R对象中的data属性,即我们需要用一个对象封装前端想要的所有参数而返回,但有时候前端想要的所有数据可能后端已有的类都无法一个对象封装所有参数。因此我们可以在原有的类基础上继承一个子类拓展属性来满足要求。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DishDTO extends Dish {
//封装了口味集合
private List<DishFlavor> flavors;

//菜品分类名称 后端数据库有的是categoryId但前端需要菜品分类的名称,原有的Dish对象不再能满足需求。在DishDTO中拓展categoryName属性满足条件.
private String categoryName;

//菜品的份数 前端传输菜品数据的时候会一并传输用户点这菜的份数,而后端的Dish对象无法封装菜品数目,于是在DishDTO中拓展copies属性以满足需要。
private Integer copies;

@Override
public String toString() {
return "DishDTO{" +
"flavors=" + flavors +
", categoryName='" + categoryName + '\'' +
", copies=" + copies +
'}';
}
}


Redis

瑞吉外卖项目中,我们使用redis来分担数据库的压力。而redis是二进制安全的,在redis中存储的数据其实是经过序列化的字节流,而redis中数据类型仅仅代表数据的组织结构,并不是值其真实存储的数据。在实际项目中我们需要将redis作为缓存使用,将从数据库中查询出来的数据存储在redis中,而查询出来的数据一般都是对象,List集合,甚至需要将map存进redis当中,这时后我们就需要考虑要使用redis提供的啥数据类型进行存储。


Redis Template

在项目中,我们可以统一用redis中的字符串类型来存储,将对象序列化为字节数组然后以字符串的形式保存在数据库当中。这样我们只需要配置RedisTemplate的value序列方式为JdkSerializationRedisSerializer,就可以将jave中的对象序列化为字符串,然后读出来的时候以同样的方式反序列化。而Redis支持很多语言,我们以JDK序列化器序列化的对象,别的语言写的服务器就无法正确的反序列化可能会导致乱码问题。如果真有这种需求可以考虑统一序列化为json格式的字符串,那么所有类型都能够访问。

1
2
3
4
5
6
7
8
9
10
11
@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.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}

使用Spring Chche简化开发

缓存一般都是用来解决读请求的,来降低落到mysql的访问压力,而当数据发生写操作时,根据实际需求可能需要删除redis缓存或者同步缓存和数据库的数据。对于一些简单的逻辑我们完全可以用注解来实现,比如需要使用缓存的读请求,一般都是先看缓存中有没有,如果有直接从缓存中拿,没有去mysql中拿并回写到缓存中。spring cache框架支持用简单的注解来满足简单的使用缓存的需求,但若是有较为复杂的逻辑还需要自己来实现。

配置:

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis --> // 导入redis的依赖关系
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-cache --> // 导入spring-cache的依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>2.7.0</version>
</dependency>

application.yml

1
2
3
4
5
6
7
8
9
spring:
redis:
host: 192.168.233.141
port: 6379 #端口
password: root@123456 #redis数据库密码
database: 0 #选择redis0号数据库
cache:
redis:
time-to-live: 3600000 # redis中设置的key的默认过期时间,实际应用中为了避免缓存雪崩问题,设置的默认过期时间应该尽可能分散。

在启动类上开启注解缓存方式:

1
2
3
4
5
6
7
8
9
10
11
@ServletComponentScan
@SpringBootApplication
@EnableTransactionManagement //开始mysql事务
@EnableCaching //开启spring-cache注解
public class ReggieApplication {

public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class, args);
}

}

使用时可以在类中加上Spring Cache注解来使用:

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

MySQL主从复制

mysql的主从复制的目的和redis主从复制的目的几乎都是一样的,为了解决单点故障问题,主mysql数据库挂了,从mysql数据库可以继续干活。可以进行读写分离,在并发量大的时候并且是读多写少的环境下,我们可以进行读写分离,让从mysql数据库为只读,主mysql数据库即可读也可以写,相当于分担了主msyql读的并发压力,系统可用性更高。


使用Sharing-JDBC框架实现MySQL读写分离

Sharing-JDBC定位为轻量级Java框架,在Java的JDBC层提供的额外服务。它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。我们在项目中导入jar包,写好配置类,就可以实现MySQL读写分离。

pom.xml:

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

application.yml配置读写分离的相关参数,就可以实现读写分离了。注意一下配置文件中格式就好。

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
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.10.101:3306/xfy?useUnicode=true&characterEncoding=UTF-8
username: root
password: '!Zbyzby1124123'
# 从数据源
slave:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.10.102:3306/xfy?useUnicode=true&characterEncoding=UTF-8
username: root
password: '!Zbyzby1124123'
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