【Springboot】Maven配置、统一返回体、CRUD、注册拦截器、分页/多表查询、统一日志、异常处理全局及分化、Redis及列表缓存、缓存穿透/击穿/雪崩、持久化、分布式锁
实体类的写法,可以先在数据库建表,然后使用MybatisPlus进行反向编译,需配置相应插件
右键MybatisX-Generator
springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件:
优先级1:项目路径下的config文件夹配置文件
优先级2:项目路径下配置文件
优先级3:资源路径下的config文件夹配置文件
优先级4:资源路径下配置文件优先级由高到底,高优先级的配置会覆盖低优先级的配置;SpringBoot会从这四个位置全部加载主配置文件;互补配置;
静态资源调用优先级:resources>static>public
自动配置原理:SpringBoot启动时会加载大量的自动配置类,看需要的功能有没有在SpringBoot默认写好的自动配置类当中,再来看这个自动配置类中到底配置了哪些组件;(只要要用的组件存在其中,就不需要再去手动配置了,如果不存在再手动配置)给容器中自动配置类添加组件的时候,会从properties类中获取某些属性,只需要在配置文件中指定这些属性即可修改属性就是SpringBoot配置.yaml、.properties这些文件。
XXXXAutoConfiguration自动配置类:给容器添加组件,这些组件要赋值就需要绑定一个XXProperties类
XXXXProperties:里面封装配置文件中相关属性;
Thymeleaf
先导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
只需要把html页面放在类路径下的templates下,thymeleaf就可以自动渲染,不需要配置
常用的Maven配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.ldf</groupId>
<artifactId>demo1128</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo1128</name>
<description>demo1128</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>annotationProcessor</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web-services</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.39</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.20</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
<!-- Swagger 核心 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!-- Swagger UI 页面 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
统一返回体
新建返回类CommonResult和PageResult,Controller返回常见的统一json格式(即状态码code、数据data、状态描述message)
package com.ldf.demo1128.reponse.enums;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
private Integer code;
private String message;
private T data;
/**
* 成功结果(无数据)
*/
public static CommonResult success() {
CommonResult result = new CommonResult();
result.setCode(200);
result.setMessage("成功");
return result;
}
/**
* 成功结果(带数据)
*/
public static <T> CommonResult<T> success(T data) {
CommonResult<T> result = new CommonResult<T>();
result.setData(data);
result.setCode(200);
result.setMessage("成功");
return result;
}
/**
* 成功结果(自定义消息)
*/
public static <T> CommonResult<T> success(T data, String message) {
CommonResult<T> result = new CommonResult<T>();
result.setData(data);
result.setCode(200);
result.setMessage(message);
return result;
}
/**
* 错误结果
*/
public static CommonResult error(Integer code, String msg) {
CommonResult result = new CommonResult();
result.setCode(code);
result.setMessage(msg);
return result;
}
/**
* 错误结果(带数据)
*/
public static <T> CommonResult<T> error(Integer code, String msg, T data) {
CommonResult<T> result = new CommonResult<T>();
result.setCode(code);
result.setMessage(msg);
result.setData(data);
return result;
}
/**
* 系统错误
*/
public static CommonResult systemError() {
return error(500, "系统错误");
}
/**
* 参数错误
*/
public static CommonResult paramError() {
return error(400, "参数错误");
}
/**
* 未授权
*/
public static CommonResult unauthorized() {
return error(401, "未授权");
}
/**
* 判断是否成功
*/
public boolean isSuccess() {
return code != null && code == 200;
}
}
package com.ldf.demo1128.reponse.enums;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Data
@AllArgsConstructor
public class PageResult<T> {
private long count;
private Integer pageNo;
private Integer pageSize;
private Map<String, Object> extend;
private List<T> lists = new ArrayList<>();
// 删除了手动编写的无参构造函数,由 @NoArgsConstructor 自动生成
/**
* 使用列表构造
*/
public PageResult(List<T> lists) {
if (lists != null) {
this.lists = new ArrayList<>(lists);
}
}
/**
* 使用单个元素构造
*/
public PageResult(T element) {
if (element != null) {
this.lists = new ArrayList<>();
this.lists.add(element);
}
}
/**
* 使用列表和总数构造
*/
public PageResult(List<T> lists, long count) {
this(lists);
this.count = count;
}
/**
* 使用列表、页码和页大小构造
*/
public PageResult(List<T> lists, Integer pageNo, Integer pageSize, long count) {
this(lists, count);
this.pageNo = pageNo;
this.pageSize = pageSize;
}
/**
* 添加扩展属性
*/
public void addExtend(String key, Object value) {
if (this.extend == null) {
this.extend = new HashMap<>();
}
this.extend.put(key, value);
}
/**
* 获取扩展属性
*/
public Object getExtend(String key) {
return this.extend != null ? this.extend.get(key) : null;
}
/**
* 判断是否为空
*/
public boolean isEmpty() {
return lists == null || lists.isEmpty();
}
/**
* 获取列表大小
*/
public int getSize() {
return lists != null ? lists.size() : 0;
}
/**
* 计算总页数
*/
public Integer getTotalPages() {
if (pageSize == null || pageSize <= 0) {
return 0;
}
return (int) Math.ceil((double) count / pageSize);
}
/**
* 创建一个空的PageResult
*/
public static <T> PageResult<T> empty() {
return new PageResult<>(new ArrayList<>());
}
/**
* 创建一个成功的PageResult
*/
public static <T> PageResult<T> success(List<T> lists, long count) {
return new PageResult<>(lists, count);
}
/**
* 创建一个带分页信息的成功PageResult
*/
public static <T> PageResult<T> success(List<T> lists, Integer pageNo, Integer pageSize, long count) {
return new PageResult<>(lists, pageNo, pageSize, count);
}
}
CRUD
Service层基本的接口不需要写,IService里都有,除了自定义getUserByName的功能。
package com.ldf.demo1128.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ldf.demo1128.domain.User;
public interface UserService extends IService<User> {
User getUserByName(String name);
}
package com.ldf.demo1128.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ldf.demo1128.domain.User;
import com.ldf.demo1128.mapper.UserMapper;
import com.ldf.demo1128.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
@Override
public User getUserByName(String name) {
return baseMapper.getUserByName(name);
}
}
Controller层要加相应的路径、接口和注解,注意自定义的方法getUserByName()需要调用Service层的,其他的直接使用库里的方法如save()、getById()、list()、updateById()等
package com.ldf.demo1128.controller;
import com.ldf.demo1128.domain.User;
import com.ldf.demo1128.reponse.enums.CommonResult;
import com.ldf.demo1128.service.UserService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
/**
* 原来的:根据用户名查询
* GET /user/getUserByName?name=张三
*/
@GetMapping("/getUserByName")
public CommonResult<User> getUserByName(@RequestParam String name) {
User user = userService.getUserByName(name);
if (user == null) {
return CommonResult.error(404, "用户不存在");
}
return CommonResult.success(user);
}
/**
* 1. 新增用户
* POST /user
* Body(JSON):{"name":"张三","phone":"13800000000"}
*/
@PostMapping
public CommonResult<User> createUser(@RequestBody User user) {
// 新增时,让数据库/MP自己生成id,不要自己传
boolean ok = userService.save(user);
if (!ok) {
return CommonResult.error(500, "新增用户失败");
}
return CommonResult.success(user, "新增用户成功");
}
/**
* 2. 按id查询用户
* GET /user/1
*/
@GetMapping("/{id}")
public CommonResult<User> getUserById(@PathVariable Long id) {
User user = userService.getById(id);
if (user == null) {
return CommonResult.error(404, "用户不存在");
}
return CommonResult.success(user);
}
/**
* 3. 查询所有用户
* GET /user/list
*/
@GetMapping("/list")
public CommonResult<List<User>> listUsers() {
List<User> users = userService.list();
return CommonResult.success(users);
}
/**
* 4. 根据id修改用户
* PUT /user/1
* Body(JSON):{"name":"李四","phone":"13900000000"}
*/
@PutMapping("/{id}")
public CommonResult<User> updateUser(
@PathVariable Long id,
@RequestBody User user) {
// 确保要更新哪条记录
user.setId(id);
boolean ok = userService.updateById(user);
if (!ok) {
return CommonResult.error(500, "更新用户失败");
}
return CommonResult.success(user, "更新用户成功");
}
/**
* 5. 根据id删除用户
* DELETE /user/1
*/
@DeleteMapping("/{id}")
public CommonResult<Void> deleteUser(@PathVariable Long id) {
boolean ok = userService.removeById(id);
if (!ok) {
return CommonResult.error(500, "删除用户失败");
}
return CommonResult.success(null, "删除用户成功");
}
}
在Mapper层写相应的方法getUserByName(),其他的不需要因为baseMapper库里面有
package com.ldf.demo1128.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ldf.demo1128.domain.User;
import com.ldf.demo1128.dto.UserInfoDTO;
import org.apache.ibatis.annotations.*;
/**
* @author 李东峰
* @description 针对表【user】的数据库操作Mapper
* @createDate 2025-11-30 15:49:31
* @Entity generator.domain.User
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 自定义查询:按名字查
@Select("select * from user where name = #{name}")
User getUserByName(@Param("name") String name);
}
登录拦截器
1、写一个拦截器类LoginInterceptor
package com.ldf.demo1128.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
// 标记成 Spring 的 Bean,方便注入
@Component
public class LoginInterceptor implements HandlerInterceptor {
/**
* 1. preHandle:在 Controller 方法执行之前执行
* 返回 true:放行
* 返回 false:拦截,不再往后执行
*/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 1)拿到请求的路径(方便调试)
String uri = request.getRequestURI();
System.out.println("拦截到请求:" + uri);
// 2)这里做你的“校验逻辑”
// 举例:我们约定前端在 header 里传一个 token
String token = request.getHeader("token");
// 假设 token 等于 "123456" 就算登录成功(只是示例)
if ("123456".equals(token)) {
// 校验通过,放行
return true;
}
// 3)校验不通过,返回一个简单提示(JSON 或字符串都可以)
response.setStatus(401); // 未授权
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"未登录或token无效\"}");
// 重要:返回 false,后面的 Controller 不再执行
return false;
}
// 2. postHandle:Controller 执行完,但还没返回给前端的时候
// 你暂时用不到,可以先不写(或空实现)
// 3. afterCompletion:整个请求结束(包括视图渲染完),一般用来清理资源
// 你也可以先不写
}
2、写一个配置类,实现 WebMvcConfigurer,在里面注册刚刚的拦截器
package com.ldf.demo1128.config;
import com.ldf.demo1128.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration // 标记这是一个配置类
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor) // 使用哪个拦截器
.addPathPatterns("/user/**") // 要拦截哪些路径
.excludePathPatterns(
"/user/login", // 不拦登录接口
"/user/register", // 不拦注册
"/error" // 一般也放行错误页
);
}
}
3、Apipost进行测试时,要在头部加token才能登陆成功
分页查询
注意:如果查询条件复杂且种类繁多,可采用动态sql.xml文件,之后在mapper层直接写方法就行 不再需要注解。
1、先配置分页插件:新建配置类 com.ldf.demo1128.config.MybatisPlusConfig:
package com.ldf.demo1128.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("com.ldf.demo1128.mapper") // 扫描 mapper 接口
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 加入分页拦截器,指定数据库类型为 MySQL
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
2、在 Controller 里写分页接口(用 Service 的 page 方法)
// ① 导入分页相关类
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ldf.demo1128.domain.User;
// ② 声明接口:GET /user/page
@GetMapping("/page")
public CommonResult<IPage<User>> pageUsers(
// ③ 接收页码参数,默认 1
@RequestParam(defaultValue = "1") Integer pageNum,
// ④ 接收每页条数,默认 10
//只访问:/user/page → 相当于 pageNum=1&pageSize=10
或者:/user/page?pageNum=3&pageSize=5
@RequestParam(defaultValue = "10") Integer pageSize,
// ⑤ 接收可选的 name 参数(模糊查询用)
@RequestParam(required = false) String name
) {
// ⑥ 构造分页参数:第几页,每页几条
Page<User> page = new Page<>(pageNum, pageSize);
// ⑦ 构造查询条件(可选)
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
if (name != null && !name.trim().isEmpty()) {
// ⑧ 如果传了 name,就按 name 做模糊查询
wrapper.like(User::getName, name);
}
// ⑨ 调用 MyBatis-Plus 默认的分页方法
IPage<User> resultPage = userService.page(page, wrapper);
// ⑩ 用你自己的 CommonResult 包一层返回
return CommonResult.success(resultPage);
}
3、Apipost 测试分页
-
方法:GET
-
URL 示例 1:
http://localhost:8080/user/page?pageNum=1&pageSize=3
不带 name,就是查全部用户,第 1 页 3 条。 -
URL 示例 2(带条件):
http://localhost:8080/user/page?pageNum=1&pageSize=5&name=张
查名字里带“张”的用户,第 1 页 5 条。
多表查询
1、新建数据库表,利用MybitsPlus进行反向编译生成新的实体类
2、新建dto类,com.ldf.demo1128.dto.UserInfoDTO接收多表结果
package com.ldf.demo1128.dto;
import lombok.Data;
@Data
public class UserInfoDTO {
private Long id;
private String name;
private String phone;
private Integer age;
private String address;
}
3、在UserMapper中多加查询规则,由于库里没有,所以需要自定义查询规则,并导入相应的包
// 多表查询:user + user_home
@Select("select u.id, u.name, u.phone, h.age, h.address " +
"from user u " +
"left join user_home h on u.id = h.id " +
"where u.id = #{id}")
UserInfoDTO selectUserInfoById(@Param("id") Long id);
4、在UserService中增加getUserInfoById方法接口
UserInfoDTO getUserInfoById(Long id);
5、UserServiceImpl实现类中 加入getUserInfoById实现规则(即UserMapper的selectUserInfoById()方法)
@Override
public UserInfoDTO getUserInfoById(Long id) {
return baseMapper.selectUserInfoById(id);
}
6、在Controller增加接受请求路径及查询参数
@GetMapping("/info/{id}")
public CommonResult<UserInfoDTO> getUserInfo(@PathVariable Long id) {
UserInfoDTO info = userService.getUserInfoById(id);
if (info == null) {
return CommonResult.error(404, "用户不存在或没有详情");
}
return CommonResult.success(info);
7、Apipost 测试:
-
方法:GET
-
URL:
http://localhost:8080/user/info/1
统一日志
使用注解 @Slf4j
log函数哪里需要放哪里
@GetMapping("/getUserByName")
public CommonResult<User> getUserByName(@RequestParam String name) {
log.info("收到查询用户请求,name={}", name);
User user = userService.getUserByName(name);
return CommonResult.success(user);
}
@PostMapping("/add")
public CommonResult<Boolean> addUser(@RequestBody User user) {
log.info("收到新增用户请求,user={}", user);
boolean ok = userService.save(user);
return CommonResult.success(ok, ok ? "新增成功" : "新增失败");
}
logger类包括的各种函数:优先级从低到高: trace()、调试用日志debug()、业务流程日志info()、warn()、error()
日志配置里如果设置了一个级别,比如
INFO,那:
INFO / WARN / ERROR会打印
DEBUG / TRACE会被过滤掉
debug()用于·:中间变量、查询条件、数据库返回的结果、逻辑分支(if 走了哪条)
info()用于:看系统做过哪些事(新增用户成功、用户登录成功等)
warn()用于:配置缺失但使用默认值、数据不完整但系统可以兜底、重试成功,但第一次是失败的
error()用于:表示程序出问题了,不符合预期需要排查,伴随Exception
异常error必须放参数e
@ExceptionHandler(Exception.class)
public CommonResult<?> handleException(Exception e) {
log.error("系统异常:", e); // 错误级别 + 打印堆栈
return CommonResult.error(500, "系统开小差了,请稍后再试");
}
异常处理
省去写这段话
try {
// 业务逻辑
} catch (Exception e) {
e.printStackTrace();
return CommonResult.error(500, "系统错误");
}
方法:
1、新建一个类 GlobalExceptionHandler全局异常处理
package com.ldf.demo1128.exception;
import com.ldf.demo1128.reponse.enums.CommonResult; // 你这里的包名按自己项目改,之前你写过 CommonResult
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理类
*/
@Slf4j
@RestControllerAdvice // 重点注解:对所有 @RestController 生效
public class GlobalExceptionHandler {
/**
* 专门处理所有 Exception(兜底)
*/
@ExceptionHandler(Exception.class)
public CommonResult<?> handleException(Exception e) {
// 1. 打印日志(方便你排查问题)
log.error("系统异常:", e);
// 2. 返回统一格式给前端
return CommonResult.error(500, "系统开小差了,请稍后再试");
}
}
2、可以再细化一点,如处理非法参数异常
// 举例:处理非法参数异常
@ExceptionHandler(IllegalArgumentException.class)
public CommonResult<?> handleIllegalArgument(IllegalArgumentException e) {
log.warn("参数异常:{}", e.getMessage());
return CommonResult.error(400, e.getMessage());
}
举例:
@PostMapping("/add")
public CommonResult<?> addUser(@RequestBody User user) {
if (user.getName() == null) {
throw new IllegalArgumentException("用户名不能为空");
}
userService.save(user);
return CommonResult.success("新增用户成功");
}
IllegalArgumentException()方法来自自带的库RuntimeException类
常见的异常:
NullPointerException(空指针异常):接收到的参数是 null,还没判空就用了、Mapper 查数据库没查到,返回 null,你当成有值用
IllegalArgumentException(非法参数异常):参数值不符合方法的要求时主动抛,如年龄不能为负数
IllegalStateException(非法状态异常):参数本身没问题,但是当前对象/系统状态不允许这么干,如用户未激活,不能下单。
也可以自定义异常:
public class BusinessException extends RuntimeException {
private Integer code;
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public Integer getCode() {
return code;
}
}
在业务里写上BusinessException
if (user.getName() == null) {
throw new BusinessException(400, "用户名不能为空");
}
Redis
用来缓存热点数据、计数器、分布式锁、消息队列、排行榜、会话 Session 等。
Redis(默认)是单线程执行命令,一次只干一件事,一个线程同时“看着”很多连接,轮流处理请求,效率高。
Redis 快,是因为“使用内存 + 单线程 + IO 多路复用”。
持久化策略:RDB:段时间存一次“快照”(备份)、AOF:把每条写操作追加到日志,可以重放。
1、下载解压以后,先双击redis-server.exe启动服务器

2、双击redis-cli.exe启动命令窗口即可

3、配置pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
4、在application.yml里配置 Redis
spring:
redis:
host: 127.0.0.1
port: 6379
# 如果你在 Redis 里设置了密码,就在这里写
# password: yourpassword
database: 0 # 用第 0 个数据库,默认即可
5、预备工作:新建一个Redis类
package com.ldf.demo1128.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 设置字符串 key-value
*/
public void set(String key, String value) {
stringRedisTemplate.opsForValue().set(key, value);
}
/**
* 设置字符串 key-value,并带过期时间(秒)
*/
public void set(String key, String value, long timeoutSeconds) {
stringRedisTemplate.opsForValue().set(key, value, timeoutSeconds, TimeUnit.SECONDS);
}
/**
* 根据 key 获取字符串
*/
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
/**
* 删除 key
*/
public Boolean delete(String key) {
return stringRedisTemplate.delete(key);
}
}
先查 Redis,没有再查 MySQL,并把结果放到 Redis。
6、在UserServiceImpl实现类中
@Override
public User getUserByName(String name) {
// 1. 先尝试从 Redis 拿
String key = "user:name:" + name; // key 的命名习惯:模块:字段:值
String cacheJson = stringRedisTemplate.opsForValue().get(key);
if (cacheJson != null && !cacheJson.trim().isEmpty()) {
// 命中缓存,直接反序列化返回
System.out.println("【命中缓存】从 Redis 拿到了用户:" + cacheJson);
return JSON.parseObject(cacheJson, User.class);
// 将Redis里的json格式转化成User类
}
System.out.println("【未命中缓存】去查数据库...");
// 2. 查数据库
User user = baseMapper.getUserByName(name);
// 3. 放到 Redis 里,下次就有了
if (user != null) {
System.out.println("【数据库结果】user = " + user);
// 将User类转化成Redis里的json格式
String json = JSON.toJSONString(user);
// 设置一个过期时间,比如 5 分钟
stringRedisTemplate.opsForValue().set(key, json, 5, java.util.concurrent.TimeUnit.MINUTES);
System.out.println("【写入缓存】已将用户信息存入 Redis,key = " + key);
}
return user;
}
stringRedisTemplate中的各常用方法:
①opsForValue():
操作字符串:
// 设置
stringRedisTemplate.opsForValue().set("key", "value");
// 设置并加过期时间(秒)
stringRedisTemplate.opsForValue().set("key", "value", 60, TimeUnit.SECONDS);
// 获取
String v = stringRedisTemplate.opsForValue().get("key");
计数(浏览量、点赞):
stringRedisTemplate.opsForValue().increment("article:1:viewCount"); // +1
删除键:
stringRedisTemplate.delete("user:name:xx");
②opsForHash()相当于 Redis 里的 Map:
// 假设 key = "user:1"
stringRedisTemplate.opsForHash().put("user:1", "name", "张三");
stringRedisTemplate.opsForHash().put("user:1", "age", "18");
// 取单个字段
Object name = stringRedisTemplate.opsForHash().get("user:1", "name");
// 一次取全部字段
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("user:1");
// 删除某个字段
stringRedisTemplate.opsForHash().delete("user:1", "age");
③opsForList()有序列表 / 队列:
// 从左边 push(相当于头部插入)
stringRedisTemplate.opsForList().leftPush("msg-queue", "msg1");
stringRedisTemplate.opsForList().leftPush("msg-queue", "msg2");
// 从右边 pop(相当于从队尾拿出:先进先出)
String msg = stringRedisTemplate.opsForList().rightPop("msg-queue");
// 查看范围内的列表元素
List<String> list = stringRedisTemplate.opsForList().range("msg-queue", 0, -1);
// 获取列表长度
Long size = stringRedisTemplate.opsForList().size("msg-queue");
④opsForSet()无序不重复集合:
// 加入一个集合
stringRedisTemplate.opsForSet().add("login:2025-12-08", "user1", "user2");
// 判断成员是否存在
Boolean has = stringRedisTemplate.opsForSet().isMember("login:2025-12-08", "user1");
// 获取所有成员
Set<String> users = stringRedisTemplate.opsForSet().members("login:2025-12-08");
// 删除成员
stringRedisTemplate.opsForSet().remove("login:2025-12-08", "user2");
⑤Key 相关通用方法:
// 设置过期时间(比如 60 秒)
stringRedisTemplate.expire("user:1", 60, TimeUnit.SECONDS);
// 删除 key
stringRedisTemplate.delete("user:1");
// 判断 key 是否存在
Boolean exist = stringRedisTemplate.hasKey("user:1");
// 匹配一些 key(开发环境可以用,生产慎用)
Set<String> keys = stringRedisTemplate.keys("user:*");
Redis注意事项:
1、写缓存、读缓存时用的 key一定要稳定一致 : "user:name:" + name
2、序列化和反序列化(Redis中存的是Json格式,而返回给Controller时是class格式):写入时用
JSON.toJSONString(user)、读出时一定要JSON.parseObject(cacheJson, User.class)
3、过期时间(TTL)要合理:太短缓存刚写进去就没了,太长数据不更新,可能和数据库信息不一致。
Redis列表缓存
类似单次用户查询,把类型换成list
1、先在Service层新增
List<User> listUsersWithCache();
2、在 UserServiceImpl 里实现列表缓存,其实就是换一下类型,基本流程一样的
// 列表缓存用的 key
private static final String USER_LIST_KEY = "user:list:all";
@Override
public List<User> listUsersWithCache() {
// 1. 先从 Redis 里拿
String cacheJson = stringRedisTemplate.opsForValue().get(USER_LIST_KEY);
if (cacheJson != null && !cacheJson.trim().isEmpty()) {
System.out.println("【命中缓存-列表】从 Redis 拿到了用户列表");
// 注意:这里是列表,用 parseArray
return JSON.parseArray(cacheJson, User.class);
}
System.out.println("【未命中缓存-列表】去查数据库...");
// 2. 查数据库(MyBatis-Plus 自带:select * from user)
List<User> list = this.list();
// 3. 如果查到了,就写入 Redis
if (list != null && !list.isEmpty()) {
String json = JSON.toJSONString(list);
stringRedisTemplate.opsForValue().set(USER_LIST_KEY, json, 5, TimeUnit.MINUTES);
System.out.println("【写入缓存-列表】已将用户列表存入 Redis,key = " + USER_LIST_KEY);
}
return list;
}
3、由于增删改这三种方法会改变用户表,查询用户表时会发生脏数据的情况,因此需要新增:增删改用户列表并缓存,还是在ServiceImpl层
@Override
public boolean save(User user) {
boolean ok = super.save(user); // 调用 MyBatis-Plus 自带的 save()
if (ok) {
// 删掉列表缓存,下次查询会重新查库再放缓存
stringRedisTemplate.delete(USER_LIST_KEY);
}
return ok;
}
@Override
public boolean updateById(User user) {
boolean ok = super.updateById(user);
if (ok) {
stringRedisTemplate.delete(USER_LIST_KEY);
}
return ok;
}
@Override
public boolean removeById(java.io.Serializable id) {
boolean ok = super.removeById(id);
if (ok) {
stringRedisTemplate.delete(USER_LIST_KEY);
}
return ok;
}
4、在Controller层新加查询用户列表带缓存的查询路径
import java.util.List;
//查询用户列表(带缓存)
@GetMapping("/list")
public CommonResult<List<User>> listUsers() {
List<User> users = userService.listUsersWithCache();
return CommonResult.success(users);
}
5、使用Apipost进行测试: GET /user/list
缓存穿透
请求的是数据库里本来就不存在的数据,没有存进Redis缓存,导致每次都重复查缓存再查数据库
解决办法:
1、参数校验:比如 id < 0、name 为空,直接在接口层拦截,不往下传
2、给Redis缓存空值:查数据库发现无用户,也往 Redis 里放一份,设置一个比较短的过期时间
3、布隆过滤器:在 Redis 或其它地方维护一个大集合,用位图判断“这个 id 是否有可能存在”,如 果判定“绝对不可能存在”,直接返回,不查库
缓存击穿
Redis里某个常用的热门key 过期瞬间,Redis里没了,突然大量的请求查Redis发现没有,一起去查数据库。
解决办法:
1、给热点 key 加互斥锁:当发现缓存过期了,只有一个线程能拿到“重建缓存”的锁,其他线程要么等一会再查缓存,要么直接失败/降级。
2、逻辑过期 + 后台异步刷新:key 本身不删,而是内部带一个“逻辑过期时间”,过期后,先返回旧数据给用户,然后后台异步去更新 Redis的key
3、热点数据设置为永不过期/超长过期 + 定时更新:适合一些配置类数据、首页内容
缓存雪崩
大量 key 在短时间内集体失效(如设置相同的过期时间),或者缓存系统整体不可用,压力突然全部砸到数据库。
解决办法:
1、过期时间加随机数(TTL 加随机抖动) :不要所有 key 都 5 分钟,改成:5 分钟 + [0,5] 分钟随机,让大量 key 不至于同时过期
2、限流 + 降级:Redis 崩了 / DB 顶不住,就对接口做限流,或者直接返回“系统繁忙”
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
@Resource
private StringRedisTemplate stringRedisTemplate;
private static final String USER_CACHE_PREFIX = "user:name:";
private static final String USER_NULL_FLAG = "NULL"; // 空值标记
// 其他方法略...
@Override
public User getUserByName(String name) {
// ===== 0. 参数校验(防止无效请求,顺便防点穿透) =====
if (name == null || name.trim().isEmpty()) {
return null;
}
String key = USER_CACHE_PREFIX + name;
// ===== 1. 先查 Redis(包括空值) =====
String cacheJson = stringRedisTemplate.opsForValue().get(key);
if (cacheJson != null) {
if (USER_NULL_FLAG.equals(cacheJson)) {
System.out.println("【命中缓存-空值】用户不存在:" + name);
return null;
}
System.out.println("【命中缓存】从 Redis 拿到了用户:" + cacheJson);
return JSON.parseObject(cacheJson, User.class);
}
System.out.println("【未命中缓存】准备查询数据库...");
// ===== 2. 简单防击穿:对同一个 name 做同步锁 =====
// 说明:intern() 让相同字符串使用同一个对象做锁
synchronized ((USER_CACHE_PREFIX + name).intern()) {
// ----- 再查一遍缓存,防止别的线程已经写好了 -----
cacheJson = stringRedisTemplate.opsForValue().get(key);
if (cacheJson != null) {
if (USER_NULL_FLAG.equals(cacheJson)) {
System.out.println("【命中缓存-空值(二次检查)】用户不存在:" + name);
return null;
}
System.out.println("【命中缓存(二次检查)】从 Redis 拿到了用户:" + cacheJson);
return JSON.parseObject(cacheJson, User.class);
}
// ===== 3. 真正去查数据库 =====
User user = baseMapper.getUserByName(name);
// ===== 4. 计算过期时间:5 分钟 + 0~4 分钟随机(防雪崩) =====
int baseMinutes = 5;
int randomExtra = ThreadLocalRandom.current().nextInt(5); // 0,1,2,3,4
long ttl = baseMinutes + randomExtra;
// ===== 5. 写缓存:分存在和不存在两种情况 =====
if (user == null) {
// 写空值,防穿透
stringRedisTemplate.opsForValue()
.set(key, USER_NULL_FLAG, ttl, TimeUnit.MINUTES);
System.out.println("【写入空值缓存】用户不存在,key=" + key + ",ttl=" + ttl + " 分钟");
return null;
} else {
String json = JSON.toJSONString(user);
stringRedisTemplate.opsForValue()
.set(key, json, ttl, TimeUnit.MINUTES);
System.out.println("【写入缓存】user=" + json + ",ttl=" + ttl + " 分钟");
return user;
}
}
}
}
Redis持久化
① RDB快照(可能丢快照到宕机之间的数据,文件小)
生成 dump.rdb 文件
② AOF日志(几乎不丢数据,但文件大)
将写操作写入 appendonly.aof 文件,Redis 重启时读取 aof重新执行一遍命令
操作:
1、找到 Redis 配置文件 redis.windows.conf
2、开启 RDB快照

生成文件dir ./data/dbfilename dump.rdb
3、开启 AOF日志
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec

必须重启 Redis redis-server redis.conf
4、用 ApiPost 验证 Redis 持久化
访问接口 GET /user/getByName?name=张三,关掉 Redis再启动Redis,再查数据,若命中缓存则持久化成功
分布式锁
1、配置依赖
<!-- Redis 分布式锁(推荐) -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.5</version>
</dependency>
2、新建RedisLockService类
package com.ldf.demo1128.lock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Component
public class RedisLockService {
@Resource
private RedissonClient redissonClient;
/**
* 尝试加锁
*/
public RLock lock(String lockKey) {
// 根据 key 从 Redis 拿一把“分布式锁”
RLock lock = redissonClient.getLock(lockKey);
// 加锁:最多等 1 秒,10 秒后自动释放(防死锁)
lock.lock(10, TimeUnit.SECONDS);
return lock;
}
/**
* 解锁
*/
public void unlock(RLock lock) {
// 只允许当前线程解锁
if (lock != null && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
3、在UserServiceImpl中改进save()
@Override
public boolean save(User user) {
// 定义锁的 key(业务级,确保不同用户分开锁,给每个用户单独生成锁的 key)
String lockKey = "user:add:" + user.getName();
// 获取锁,如果别的线程已经拿走了,会等待锁释放
RLock lock = redisLockService.lock(lockKey);
try {
// 保存用户到数据库
boolean success = saveUserToDatabase(user);
// 如果保存成功,发送消息、邮件和异步日志
if (success) {
String message = "用户 " + user.getName() + " 创建成功";
messageProducer.sendMessage(message); // 发送 RabbitMQ 消息
sendEmailAsync(user.getEmail(), "欢迎您注册!"); // 异步发送邮件
logUserInfoAsync(user); // 异步记录日志
}
return success;
} finally {
// 一定要解锁(防死锁)
redisLockService.unlock(lock);
}
}
lockKey即user:add:张三 user的add操作名为张三的锁,单独生成锁的 key唯一标识
第二线程拿锁时,Redisson内部执行以下操作(源码方便理解)
while (true) {
// 尝试拿锁
boolean success = redis.setIfAbsent(lockKey, threadId, timeout);
if (success) {
// 成功拿到锁,退出循环
break;
}
// 拿不到锁,睡一小会儿,再重试
Thread.sleep(50); // 50ms 后再尝试
}
更多推荐



所有评论(0)