SSM框架下的redis使用以及token认证
- pom.xml + application.yml(基础配置)
↓ - 实体类 Dept.java(数据模型)
↓ - 基础工具类(Result.java、RedisConfig.java)
↓ - Mapper层(DeptMapper.java + DeptMapper.xml)
↓ - Service层(DeptService.java + DeptServiceImpl.java)
↓ - Controller层(DeptController.java - 定义所有接口路径)✨关键
↓ - 拦截器配置(TokenInterceptor.java + WebMvcConfig.java)
↓ - 启动类(SpringBootMain.java)
↓ - 前端页面
第一步-基础配置
首先创建maven框架下的java项目
为什么要是用maven这个管理工具
使用maven最直观的就是不用再导入很多的jar包,然后包括项目的打包发布都可以交给maven来进行管理
创建完的项目结构是这样的
pom.xml
然后我们需要开始配置pom.xml文件,针对于pom.xml文件中的内容,会包括一些项目的启动器,以及一些需要的工具库,同时我们还需要继承来进行版本控制,然后还需要build来进行资源的拷贝准备工作,依赖如下
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.jr.dz18</groupId><artifactId>springbootMvcMybatis</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.3.RELEASE</version></parent><dependencies><!--添加stringBoot启动器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.1.10.RELEASE</version><type>pom</type><scope>import</scope></dependency><!-- Spring MVC启动器 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Mybatis启动器 --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.1</version></dependency><!-- MySQL数据库驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.30</version></dependency><!-- Thymeleaf模板引擎 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><!-- Lombok注解工具 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency><!-- Redis数据访问 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency></dependencies><build><!--添加tomcat插件 --><plugins><plugin><groupId>org.apache.tomcat.maven</groupId><artifactId>tomcat7-maven-plugin</artifactId><version>2.2</version><configuration><port>8080</port><path>/</path></configuration></plugin></plugins><!--资源拷贝的插件 --><resources><resource><directory>src/main/java</directory><includes><include>**/*.xml</include></includes></resource><resource><directory>src/main/resources</directory><includes><include>**/*.yml</include><include>**/*.xml</include><include>**/*.html</include><include>**/*.js</include><include>**/*.properties</include></includes></resource></resources></build></project>
application.yml
接下来我们开始配置application.yml文件,是一个配置文件,通过server告诉springBoot监听哪个端口,通过datasource来确定数据源,通过redis来确定连接的redis的端口号,通过mabits配置如下项
server:port: 8080spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/company_db?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueusername: rootpassword: rootredis:host: 192.168.1.115mybatis:type-aliases-package: com.jr.pojomapper-locations: classpath:com/jr/mapper/*.xmlconfiguration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
第二步-实体类
这个就是正常根据数据库(建议名称和表名一样,字段和属性相同),项目是springBoot,记得要添加Compenent注解
package com.jr.pojo;import org.springframework.stereotype.Component;/*** 部门实体类* 对应数据库中的dept表*/
@Component // 标识这是一个Spring组件,会被Spring容器管理,可以用于依赖注入
public class Dept {private Integer deptno;private String dname;private String loc;public Integer getDeptno() {return deptno;}public void setDeptno(Integer deptno) {this.deptno = deptno;}public String getDname() {return dname;}public void setDname(String dname) {this.dname = dname;}public String getLoc() {return loc;}public void setLoc(String loc) {this.loc = loc;}public Dept() {}public Dept(Integer deptno, String dname, String loc) {this.deptno = deptno;this.dname = dname;this.loc = loc;}@Overridepublic String toString() {return "Dept{" +"deptno=" + deptno +", dname='" + dname + '\'' +", loc='" + loc + '\'' +'}';}
}
第三步-基础工具类
RedisConfig.java
package com.jr.util;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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;/*** Redis配置类* 配置Redis连接和序列化方式*/
@Configuration // 标识这是一个配置类,Spring会自动扫描并加载其中的配置
public class RedisConfig {@Bean // 标识这是一个Bean定义方法,Spring会将返回值注册为Beanpublic RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(factory);redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());return redisTemplate;}}
Result.java
package com.jr.util;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;import java.io.Serializable;/*** 统一返回结果封装类* 用于封装API接口的返回数据*/
@Component // 标识这是一个Spring组件,会被Spring容器管理
@AllArgsConstructor // Lombok注解,自动生成包含所有字段的构造方法
@NoArgsConstructor // Lombok注解,自动生成无参构造方法
@Data // Lombok注解,自动生成getter、setter、toString、equals、hashCode方法
public class Result implements Serializable {private Integer code;private String mess;private Object data;private Boolean boo;}
第四步-Mapper层
mapper接口
这步要记得通过@Mapper表示当前类是mapper,通过Component添加为bean
package com.jr.mapper;import com.jr.pojo.Dept;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Component;import java.util.List;/*** 部门数据访问层接口* 定义与数据库交互的方法*/
@Component
@Mapper // MyBatis注解,标识这是一个Mapper接口,MyBatis会自动为其创建实现类
public interface DeptMapper {/*** 查询所有部门*/List<Dept> selectAll();/*** 根据部门编号和部门名称查询部门*/Dept selectDept(Dept dept);/*** 根据部门编号查询部门*/Dept selectByDeptno(Integer deptno);/*** 删除部门*/int deleteByDeptno(Integer deptno);/*** 更新部门信息*/int updateDept(Dept dept);/*** 新增部门*/int insertDept(Dept dept);/*** 分页查询部门*/List<Dept> selectAllWithPagination(int offset, int size);/*** 获取部门总数*/int getTotalCount();}
mapper接口.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jr.mapper.DeptMapper"><!-- 查询所有部门 --><select id="selectAll" resultType="dept">select * from dept</select><!-- 根据部门编号和部门名称查询部门 --><select id="selectDept" resultType="dept">select * from dept where deptno=#{deptno} and dname=#{dname}</select><!-- 根据部门编号查询部门 --><select id="selectByDeptno" resultType="dept">select * from dept where deptno=#{deptno}</select><!-- 删除部门 --><delete id="deleteByDeptno">delete from dept where deptno=#{deptno}</delete><!-- 更新部门信息 --><update id="updateDept">update dept set dname=#{dname}, loc=#{loc} where deptno=#{deptno}</update><!-- 新增部门 --><insert id="insertDept">insert into dept(deptno, dname, loc) values(#{deptno}, #{dname}, #{loc})</insert><!-- 分页查询部门 --><select id="selectAllWithPagination" resultType="dept">select * from dept limit #{offset}, #{size}</select><!-- 获取部门总数 --><select id="getTotalCount" resultType="int">select count(*) from dept</select></mapper>
第五步-Service层
service接口
package com.jr.service;import com.jr.pojo.Dept;
import java.util.List;/*** 部门服务接口* 定义部门相关的业务逻辑方法* 注意:接口本身不需要注解,因为它是被实现类实现的*/
public interface DeptService {/*** 查询所有部门* @return 部门列表*/List<Dept> selectAll();/*** 根据部门编号和部门名称查询部门* @param dept 部门对象* @return 部门信息*/Dept selectDept(Dept dept);/*** 根据部门编号查询部门* @param deptno 部门编号* @return 部门信息*/Dept selectByDeptno(Integer deptno);/*** 删除部门* @param deptno 部门编号* @return 影响行数*/int deleteBydeptno(Integer deptno);/*** 更新部门信息* @param dept 部门对象* @return 影响行数*/int updateDept(Dept dept);/*** 新增部门* @param dept 部门对象* @return 影响行数*/int insertDept(Dept dept);/*** 分页查询部门* @param page 页码(从1开始)* @param size 每页大小* @return 部门列表*/List<Dept> selectAllWithPagination(int page, int size);/*** 获取部门总数* @return 部门总数*/int getTotalCount();
}
service实现类
package com.jr.service.impl;import com.jr.mapper.DeptMapper;
import com.jr.pojo.Dept;
import com.jr.service.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.List;/*** 部门服务实现类* 实现DeptService接口中定义的所有业务方法*/
@Service // 标识这是一个服务层组件,会被Spring容器管理,用于业务逻辑处理
public class DeptServiceImpl implements DeptService {@Autowired // 自动注入部门数据访问层,Spring会自动找到DeptMapper的实现并注入private DeptMapper deptMapper;@Override // 重写接口方法@Transactional(readOnly = true) // 声明式事务管理,readOnly=true表示只读事务,提高查询性能public List<Dept> selectAll() {return deptMapper.selectAll();}@Override // 重写接口方法public Dept selectDept(Dept dept) {return deptMapper.selectDept(dept);}@Override // 重写接口方法@Transactional(readOnly = true) // 声明式事务管理,只读事务public Dept selectByDeptno(Integer deptno) {return deptMapper.selectByDeptno(deptno);}@Override // 重写接口方法@Transactional // 声明式事务管理,默认情况下支持读写事务,如果出现异常会自动回滚public int deleteBydeptno(Integer deptno) {return deptMapper.deleteByDeptno(deptno);}@Override // 重写接口方法@Transactional // 声明式事务管理,支持读写事务,异常时自动回滚public int updateDept(Dept dept) {return deptMapper.updateDept(dept);}@Override // 重写接口方法@Transactional // 声明式事务管理,支持读写事务,异常时自动回滚public int insertDept(Dept dept) {return deptMapper.insertDept(dept);}@Override // 重写接口方法@Transactional(readOnly = true) // 声明式事务管理,只读事务public List<Dept> selectAllWithPagination(int page, int size) {int offset = (page - 1) * size;return deptMapper.selectAllWithPagination(offset, size);}@Override // 重写接口方法@Transactional(readOnly = true) // 声明式事务管理,只读事务public int getTotalCount() {return deptMapper.getTotalCount();}
}
第六步-Controller层()
package com.jr.controller;import com.jr.pojo.Dept;
import com.jr.service.DeptService;
import com.jr.util.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;/*** 部门控制器* 处理部门相关的HTTP请求*/
@Controller // 标识这是一个Spring MVC控制器类,会被Spring容器管理
public class DeptController {@Autowired // 自动注入部门服务,Spring会自动找到DeptService的实现类并注入private DeptService deptService;@Autowired // 自动注入结果封装类,用于统一返回格式private Result rs;@Autowired // 自动注入Redis模板,用于操作Redis数据库private RedisTemplate<String,Object> redisTemplate;@RequestMapping("/{url}") // 映射URL路径,{url}是路径变量public String index(@PathVariable String url){ // @PathVariable: 从URL路径中获取变量值return url;}@RequestMapping("/login") // 映射登录请求路径@ResponseBody // 将返回值直接写入HTTP响应体,而不是返回视图页面public Result login(Dept dept){Dept d= deptService.selectDept(dept);if(d!=null){// 生成唯一TokenString token = UUID.randomUUID().toString();System.out.println("生成Token: " + token);// 优化后的Token存储策略:// 1. token -> 用户信息(用于快速验证Token)redisTemplate.opsForValue().set("token:" + token, d, 2L, TimeUnit.HOURS);// 2. deptno -> token(用于查询用户的Token,实现单设备登录)String oldToken = (String) redisTemplate.opsForValue().get("user:" + d.getDeptno() + ":token");if (oldToken != null) {// 删除旧Token(实现单设备登录,踢掉之前的登录)redisTemplate.delete("token:" + oldToken);System.out.println("删除旧Token,实现单设备登录");}redisTemplate.opsForValue().set("user:" + d.getDeptno() + ":token", token, 2L, TimeUnit.HOURS);rs.setBoo(true);rs.setCode(200);rs.setMess("登录成功");rs.setData(token);}else{rs.setBoo(false);rs.setCode(100);rs.setMess("登录失败");rs.setData(null);}return rs;}@RequestMapping("/del") // 映射删除请求路径@ResponseBody // 将返回值直接写入HTTP响应体public Result del(Integer deptno){int i= deptService.deleteBydeptno(deptno);if (i>0){// 只删除部门列表缓存,不删除用户Token(删除部门不应该让用户下线)redisTemplate.delete("deptList");// 清除所有分页缓存clearAllPageCache();List<Dept> list = deptService.selectAll();redisTemplate.opsForValue().set("deptList",list,1,TimeUnit.HOURS);rs.setBoo(true);rs.setData(list);rs.setCode(200);rs.setMess("删除成功");}else{rs.setBoo(false);rs.setData(redisTemplate.opsForValue().get("deptList"));rs.setCode(100);rs.setMess("删除失败");}return rs;}@RequestMapping("/selBydeptno") // 映射根据部门编号查询请求路径@ResponseBody // 将返回值直接写入HTTP响应体public Result selBydeptno(Integer deptno){Dept dept = deptService.selectByDeptno(deptno);if(dept != null){rs.setBoo(true);rs.setCode(200);rs.setMess("查询成功");rs.setData(dept);}else{rs.setBoo(false);rs.setCode(100);rs.setMess("查询失败");rs.setData(null);}return rs;}@RequestMapping("/update") // 映射更新请求路径@ResponseBody // 将返回值直接写入HTTP响应体public Result update(Dept dept){int i = deptService.updateDept(dept);if(i > 0){// 只删除部门列表缓存,不删除用户Token(修改部门信息不应该让用户下线)redisTemplate.delete("deptList");// 清除所有分页缓存clearAllPageCache();// 重新缓存部门列表List<Dept> list = deptService.selectAll();redisTemplate.opsForValue().set("deptList", list, 1, TimeUnit.HOURS);rs.setBoo(true);rs.setData(list);rs.setCode(200);rs.setMess("修改成功");}else{rs.setBoo(false);rs.setData(redisTemplate.opsForValue().get("deptList"));rs.setCode(100);rs.setMess("修改失败");}return rs;}@RequestMapping("/add") // 映射添加请求路径@ResponseBody // 将返回值直接写入HTTP响应体public Result add(Dept dept){int i = deptService.insertDept(dept);if(i > 0){// 删除相关缓存redisTemplate.delete("deptList");// 清除所有分页缓存clearAllPageCache();// 重新缓存部门列表List<Dept> list = deptService.selectAll();redisTemplate.opsForValue().set("deptList", list, 1, TimeUnit.HOURS);rs.setBoo(true);rs.setData(list);rs.setCode(200);rs.setMess("添加成功");}else{rs.setBoo(false);rs.setData(redisTemplate.opsForValue().get("deptList"));rs.setCode(100);rs.setMess("添加失败");}return rs;}@RequestMapping("/getDeptsWithPagination") // 映射分页查询请求路径@ResponseBody // 将返回值直接写入HTTP响应体public Result getDeptsWithPagination(int page, int size){// 1. 生成缓存keyString cacheKey = "deptPage:" + page + ":" + size;try {// 2. 先从Redis查询缓存Object cachedData = redisTemplate.opsForValue().get(cacheKey);if (cachedData != null) {System.out.println("✅ 从Redis缓存获取分页数据:" + cacheKey);rs.setBoo(true);rs.setCode(200);rs.setMess("查询成功(缓存)");rs.setData(cachedData);return rs;}// 3. 缓存未命中,使用分布式锁防止缓存击穿String lockKey = "lock:" + cacheKey;Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked");if (Boolean.TRUE.equals(lockAcquired)) {// 设置锁的过期时间为10秒(防止死锁)redisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);}if (Boolean.TRUE.equals(lockAcquired)) {// 3.1 成功获取锁,查询数据库try {System.out.println("🔒 获取分布式锁成功,查询数据库:" + cacheKey);// 双重检查:再次查询缓存(可能其他线程已经缓存了)cachedData = redisTemplate.opsForValue().get(cacheKey);if (cachedData != null) {System.out.println("✅ 双重检查:缓存已存在");rs.setBoo(true);rs.setCode(200);rs.setMess("查询成功(缓存)");rs.setData(cachedData);return rs;}// 查询数据库List<Dept> list = deptService.selectAllWithPagination(page, size);int totalCount = deptService.getTotalCount();int totalPages = (int) Math.ceil((double) totalCount / size);// 创建分页结果对象java.util.Map<String, Object> pageData = new java.util.HashMap<>();pageData.put("list", list);pageData.put("currentPage", page);pageData.put("pageSize", size);pageData.put("totalCount", totalCount);pageData.put("totalPages", totalPages);// 存入Redis缓存(30分钟过期)redisTemplate.opsForValue().set(cacheKey, pageData, 30, TimeUnit.MINUTES);System.out.println("💾 分页数据已缓存到Redis:" + cacheKey);rs.setBoo(true);rs.setCode(200);rs.setMess("查询成功");rs.setData(pageData);} finally {// 3.2 释放锁redisTemplate.delete(lockKey);System.out.println("🔓 释放分布式锁:" + lockKey);}} else {// 3.3 未获取到锁,等待并重试System.out.println("⏳ 未获取到锁,等待重试:" + cacheKey);Thread.sleep(100); // 等待100毫秒// 重试获取缓存cachedData = redisTemplate.opsForValue().get(cacheKey);if (cachedData != null) {System.out.println("✅ 重试成功,从缓存获取数据");rs.setBoo(true);rs.setCode(200);rs.setMess("查询成功(缓存)");rs.setData(cachedData);} else {// 如果还是没有缓存,直接查询数据库(降级处理)System.out.println("⚠️ 缓存仍未命中,降级查询数据库");java.util.Map<String, Object> pageData = queryDatabaseDirectly(page, size);rs.setBoo(true);rs.setCode(200);rs.setMess("查询成功");rs.setData(pageData);}}} catch (Exception e) {System.out.println("❌ 分页查询异常:" + e.getMessage());e.printStackTrace();rs.setBoo(false);rs.setCode(100);rs.setMess("查询失败");rs.setData(null);}return rs;}/*** 直接查询数据库(降级方法)* 用于分布式锁获取失败时的降级处理*/private java.util.Map<String, Object> queryDatabaseDirectly(int page, int size) {List<Dept> list = deptService.selectAllWithPagination(page, size);int totalCount = deptService.getTotalCount();int totalPages = (int) Math.ceil((double) totalCount / size);java.util.Map<String, Object> pageData = new java.util.HashMap<>();pageData.put("list", list);pageData.put("currentPage", page);pageData.put("pageSize", size);pageData.put("totalCount", totalCount);pageData.put("totalPages", totalPages);return pageData;}@RequestMapping("/logout") // 映射退出登录请求路径@ResponseBody // 将返回值直接写入HTTP响应体public Result logout(HttpServletRequest request){try {String token = request.getHeader("token");if (token != null && !token.trim().isEmpty()) {// 退出登录时不删除任何缓存,只需要前端清除localStorage中的token即可// Redis中的token会自动过期(2小时后)}rs.setBoo(true);rs.setCode(200);rs.setMess("退出成功");rs.setData(null);} catch (Exception e) {rs.setBoo(false);rs.setCode(100);rs.setMess("退出失败");rs.setData(null);}return rs;}/*** 清除所有分页缓存* 当数据发生变化(增删改)时调用此方法*/private void clearAllPageCache() {try {// 使用通配符查找所有分页缓存的keyjava.util.Set<String> keys = redisTemplate.keys("deptPage:*");if (keys != null && !keys.isEmpty()) {redisTemplate.delete(keys);System.out.println("已清除 " + keys.size() + " 个分页缓存");}} catch (Exception e) {System.out.println("清除分页缓存异常:" + e.getMessage());}}
}
第七步-拦截器配置
TokenInterceptor.java
package com.jr.util;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;/*** Token拦截器* 用于验证用户登录状态*/
@Component // 标识这是一个Spring组件,会被Spring容器管理
public class TokenInterceptor implements HandlerInterceptor {@Autowired // 自动注入Redis模板,用于操作Redis数据库private RedisTemplate<String, Object> redisTemplate;@Override // 重写HandlerInterceptor接口中的方法,在请求处理之前执行public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取请求路径String requestURI = request.getRequestURI();// 获取token(从请求头中获取)String token = request.getHeader("token");// 打印调试信息System.out.println("拦截器检查 - 请求路径: " + requestURI + ", Token: " + token);// 如果token为空,跳转到登录页if (token == null || token.trim().isEmpty()) {System.out.println("Token为空,跳转到登录页");// 设置响应状态码response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);// 跳转到登录页response.sendRedirect("/index");return false;}// 检查token是否有效并自动续期if (isTokenValidAndRenew(token)) {System.out.println("Token验证通过,放行请求");return true;} else {System.out.println("Token无效或已过期,跳转到登录页");response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.sendRedirect("/index");return false;}}/*** 检查token是否有效,智能续期* 优化后:直接通过token查询,不再遍历所有key* 只有当剩余时间少于100秒时才续期2小时*/private boolean isTokenValidAndRenew(String token) {try {// 直接通过token查询用户信息(O(1)时间复杂度)String tokenKey = "token:" + token;Object userInfo = redisTemplate.opsForValue().get(tokenKey);if (userInfo == null) {System.out.println("Token无效或已过期");return false;}// 获取token的剩余过期时间(秒)Long remainingTime = redisTemplate.getExpire(tokenKey, TimeUnit.SECONDS);if (remainingTime == null || remainingTime < 0) {System.out.println("Token已过期");return false;}System.out.println("Token剩余时间: " + remainingTime + " 秒");// 智能续期:只有当剩余时间少于100秒时才续期if (remainingTime <= 100) {System.out.println("Token即将过期,自动续期2小时");redisTemplate.expire(tokenKey, 2, TimeUnit.HOURS);// 同时续期用户的token映射if (userInfo instanceof com.jr.pojo.Dept) {com.jr.pojo.Dept dept = (com.jr.pojo.Dept) userInfo;String userTokenKey = "user:" + dept.getDeptno() + ":token";redisTemplate.expire(userTokenKey, 2, TimeUnit.HOURS);}} else {System.out.println("Token还有充足时间,无需续期");}return true;} catch (Exception e) {System.out.println("Token验证异常: " + e.getMessage());e.printStackTrace();return false;}}
}
WebMvcConfig.java
package com.jr.util;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;/*** Web MVC配置类* 配置拦截器*/
@Configuration // 标识这是一个配置类,Spring会自动扫描并加载其中的配置
public class WebMvcConfig implements WebMvcConfigurer {@Autowired // 自动注入Token拦截器,Spring会自动找到TokenInterceptor并注入private TokenInterceptor tokenInterceptor;@Override // 重写WebMvcConfigurer接口中的方法public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(tokenInterceptor)// 拦截所有请求.addPathPatterns("/**")// 排除不需要拦截的路径.excludePathPatterns("/index", // 登录页面"/login", // 登录接口"/static/**", // 静态资源"/css/**", // CSS文件"/js/**", // JavaScript文件"/images/**", // 图片文件"/favicon.ico" // 网站图标);}
}
第八步-启动类
SpringBootMain.java
package com.jr;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/*** Spring Boot 主启动类* 这是整个Spring Boot应用的入口点*/
@SpringBootApplication // 这是一个组合注解,包含以下三个注解:// @Configuration: 标识这是一个配置类// @EnableAutoConfiguration: 启用Spring Boot的自动配置机制// @ComponentScan: 启用组件扫描,自动发现和注册Bean
public class SpringBootMain {public static void main(String[] args) {// 启动Spring Boot应用SpringApplication.run(SpringBootMain.class,args);}}
终极-Redis 使用全流程详解
📋 目录
- Redis配置与初始化
- Redis在项目中的四大用途
- 场景1:用户登录与Token管理
- 场景2:Token验证与智能续期
- 场景3:部门列表缓存
- 场景4:分页查询缓存与击穿保护
- 完整Redis Key设计
- RedisTemplate API总结
- 完整业务流程图
- 性能优化对比
1. Redis配置与初始化
1.1 配置文件:application.yml
spring:redis:host: 192.168.1.115 # Redis服务器地址# port: 6379 # 默认端口(可省略)# password: # 如果有密码# database: 0 # 使用的数据库编号
1.2 Redis配置类:RedisConfig.java
@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(factory);// Key序列化:使用String序列化器(存储为字符串)template.setKeySerializer(new StringRedisSerializer());// Value序列化:使用JSON序列化器(可存储对象)template.setValueSerializer(new GenericJackson2JsonRedisSerializer());return template;}
}
作用说明:
- StringRedisSerializer:将Key序列化为字符串,如
"token:a1b2c3d4"
- GenericJackson2JsonRedisSerializer:将Value序列化为JSON,可存储Java对象
2. Redis在项目中的四大用途
用途 | 说明 | 过期时间 | 关键技术 |
---|---|---|---|
🔐 Token认证 | 存储用户登录Token,实现无状态认证 | 2小时 | 双向映射、单设备登录 |
⏰ Token续期 | 智能续期机制,提升用户体验 | 动态续期 | 剩余时间检测 |
📦 数据缓存 | 缓存部门列表,减少数据库查询 | 1小时 | 读写分离 |
🛡️ 击穿保护 | 分页查询缓存+分布式锁 | 30分钟 | 分布式锁、双重检查 |
3. 场景1:用户登录与Token管理
3.1 登录接口代码
位置: DeptController.java
- login()
方法
@RequestMapping("/login")
@ResponseBody
public Result login(Dept dept) {// 1. 验证用户名密码Dept d = deptService.selectDept(dept);if (d != null) {// 2. 生成唯一Token (UUID)String token = UUID.randomUUID().toString();System.out.println("生成Token: " + token);// 3. 双向存储Token(核心优化!)// 3.1 token -> 用户信息(用于快速验证Token)redisTemplate.opsForValue().set("token:" + token, // Key: token:a1b2c3d4-e5f6-...d, // Value: Dept对象2L, // 过期时间: 2TimeUnit.HOURS // 时间单位: 小时);// 3.2 检查是否有旧Token(实现单设备登录)String oldToken = (String) redisTemplate.opsForValue().get("user:" + d.getDeptno() + ":token");if (oldToken != null) {// 删除旧Token(踢掉之前的登录)redisTemplate.delete("token:" + oldToken);System.out.println("删除旧Token,实现单设备登录");}// 3.3 deptno -> token(用于查询用户的Token)redisTemplate.opsForValue().set("user:" + d.getDeptno() + ":token", // Key: user:10:tokentoken, // Value: Token字符串2L, // 过期时间: 2小时TimeUnit.HOURS);// 4. 返回Token给前端rs.setData(token);return rs;}
}
3.2 Redis存储结构
用户10登录后的Redis数据:
┌─────────────────────────────────────────────────────────────┐
│ Key: token:a1b2c3d4-e5f6-7890-abcd-ef1234567890 │
│ Value: {"deptno":10,"dname":"研发部","loc":"北京"} │
│ TTL: 7200秒 (2小时) │
└─────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────┐
│ Key: user:10:token │
│ Value: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" │
│ TTL: 7200秒 (2小时) │
└─────────────────────────────────────────────────────────────┘
3.3 为什么要双向存储?
存储方向 | Key格式 | 用途 | 时间复杂度 |
---|---|---|---|
Token → 用户 | token:{uuid} | 快速验证Token是否有效 | O(1) |
用户 → Token | user:{deptno}:token | 实现单设备登录、查询用户Token | O(1) |
3.4 单设备登录原理
时间线:
10:00 用户10在设备A登录↓ 生成Token_A↓ 存储: token:Token_A → 用户10↓ 存储: user:10:token → Token_A10:30 用户10在设备B登录↓ 生成Token_B↓ 查询: user:10:token → Token_A (发现旧Token)↓ 删除: token:Token_A (设备A的Token失效)↓ 存储: token:Token_B → 用户10↓ 更新: user:10:token → Token_B结果:设备A使用Token_A访问 → 验证失败,被踢下线 ❌设备B使用Token_B访问 → 验证成功 ✅
4. 场景2:Token验证与智能续期
4.1 拦截器代码
位置: TokenInterceptor.java
- isTokenValidAndRenew()
方法
private boolean isTokenValidAndRenew(String token) {try {// 1. 直接通过token查询用户信息(O(1)复杂度)String tokenKey = "token:" + token;Object userInfo = redisTemplate.opsForValue().get(tokenKey);if (userInfo == null) {System.out.println("Token无效或已过期");return false;}// 2. 获取Token的剩余过期时间Long remainingTime = redisTemplate.getExpire(tokenKey, TimeUnit.SECONDS);if (remainingTime == null || remainingTime < 0) {System.out.println("Token已过期");return false;}System.out.println("Token剩余时间: " + remainingTime + " 秒");// 3. 智能续期:只有剩余时间 ≤ 100秒时才续期if (remainingTime <= 100) {System.out.println("Token即将过期,自动续期2小时");// 续期TokenredisTemplate.expire(tokenKey, 2, TimeUnit.HOURS);// 同时续期用户Token映射if (userInfo instanceof Dept) {Dept dept = (Dept) userInfo;String userTokenKey = "user:" + dept.getDeptno() + ":token";redisTemplate.expire(userTokenKey, 2, TimeUnit.HOURS);}} else {System.out.println("Token还有充足时间,无需续期");}return true;} catch (Exception e) {System.out.println("Token验证异常: " + e.getMessage());return false;}
}
4.2 智能续期机制
用户登录(10:00)↓
Token过期时间:12:00 (2小时后)↓
用户操作(10:30,剩余5400秒)↓
拦截器检查:5400秒 > 100秒 → 无需续期 ✅↓
用户操作(11:58,剩余120秒)↓
拦截器检查:120秒 > 100秒 → 无需续期 ✅↓
用户操作(11:59,剩余60秒)↓
拦截器检查:60秒 ≤ 100秒 → 自动续期2小时!⏰↓
新过期时间:13:59 (从当前时间延长2小时)
4.3 续期策略对比
策略 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
不续期 | 安全性高 | 用户体验差,频繁要求登录 | 银行、支付系统 |
每次续期 | 用户体验好 | Redis压力大,安全性低 | - |
智能续期(本项目) | 平衡体验和性能 | 实现稍复杂 | ✅ 大多数业务系统 |
5. 场景3:部门列表缓存
5.1 增删改操作的缓存策略
添加部门
@RequestMapping("/add")
@ResponseBody
public Result add(Dept dept) {int i = deptService.insertDept(dept); // 插入数据库if (i > 0) {// 1. 删除旧缓存(缓存失效)redisTemplate.delete("deptList");// 2. 清除所有分页缓存clearAllPageCache();// 3. 查询最新数据List<Dept> list = deptService.selectAll();// 4. 重新缓存(1小时过期)redisTemplate.opsForValue().set("deptList", list, 1, TimeUnit.HOURS);rs.setData(list);}return rs;
}
删除部门
@RequestMapping("/del")
@ResponseBody
public Result del(Integer deptno) {int i = deptService.deleteBydeptno(deptno);if (i > 0) {// ✅ 只删除缓存,不删除用户Token(优化点!)redisTemplate.delete("deptList");clearAllPageCache();// 重新缓存最新数据List<Dept> list = deptService.selectAll();redisTemplate.opsForValue().set("deptList", list, 1, TimeUnit.HOURS);rs.setData(list);}return rs;
}
更新部门
@RequestMapping("/update")
@ResponseBody
public Result update(Dept dept) {int i = deptService.updateDept(dept);if (i > 0) {// 同样的缓存更新策略redisTemplate.delete("deptList");clearAllPageCache();List<Dept> list = deptService.selectAll();redisTemplate.opsForValue().set("deptList", list, 1, TimeUnit.HOURS);rs.setData(list);}return rs;
}
5.2 缓存更新策略:Cache-Aside Pattern
┌─────────────────────────────────────────────────────────────┐
│ 写操作(增删改) │
└─────────────────────────────────────────────────────────────┘↓┌──────────────┴──────────────┐│ │1. 更新数据库 2. 删除缓存│ │└──────────────┬──────────────┘↓┌──────────────────────────────┐│ 3. 查询数据库获取最新数据 │└──────────────┬───────────────┘↓┌──────────────────────────────┐│ 4. 重新写入缓存(1小时) │└──────────────────────────────┘┌─────────────────────────────────────────────────────────────┐
│ 读操作 │
└─────────────────────────────────────────────────────────────┘↓┌──────────────────────────────┐│ 1. 查询缓存 │└──────────────┬───────────────┘↓缓存命中?┌──────┴──────┐是 │ │ 否↓ ↓直接返回 查询数据库并缓存结果
6. 场景4:分页查询缓存与击穿保护
6.1 完整代码
位置: DeptController.java
- getDeptsWithPagination()
方法
@RequestMapping("/getDeptsWithPagination")
@ResponseBody
public Result getDeptsWithPagination(int page, int size) {// 1. 生成缓存KeyString cacheKey = "deptPage:" + page + ":" + size;try {// 2. 先查询Redis缓存Object cachedData = redisTemplate.opsForValue().get(cacheKey);if (cachedData != null) {System.out.println("✅ 从Redis缓存获取分页数据:" + cacheKey);rs.setData(cachedData);return rs;}// 3. 缓存未命中 → 使用分布式锁防止缓存击穿String lockKey = "lock:" + cacheKey;Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked");if (lockAcquired) {// 设置锁过期时间(防止死锁)redisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);}if (Boolean.TRUE.equals(lockAcquired)) {// 3.1 获取锁成功try {System.out.println("🔒 获取分布式锁成功,查询数据库:" + cacheKey);// 双重检查:再次查询缓存cachedData = redisTemplate.opsForValue().get(cacheKey);if (cachedData != null) {System.out.println("✅ 双重检查:缓存已存在");rs.setData(cachedData);return rs;}// 查询数据库List<Dept> list = deptService.selectAllWithPagination(page, size);int totalCount = deptService.getTotalCount();int totalPages = (int) Math.ceil((double) totalCount / size);// 创建分页结果Map<String, Object> pageData = new HashMap<>();pageData.put("list", list);pageData.put("currentPage", page);pageData.put("pageSize", size);pageData.put("totalCount", totalCount);pageData.put("totalPages", totalPages);// 缓存结果(30分钟)redisTemplate.opsForValue().set(cacheKey, pageData, 30, TimeUnit.MINUTES);System.out.println("💾 分页数据已缓存到Redis:" + cacheKey);rs.setData(pageData);} finally {// 3.2 释放锁(确保一定释放)redisTemplate.delete(lockKey);System.out.println("🔓 释放分布式锁:" + lockKey);}} else {// 3.3 未获取到锁 → 等待并重试System.out.println("⏳ 未获取到锁,等待重试:" + cacheKey);Thread.sleep(100); // 等待100ms// 重试获取缓存cachedData = redisTemplate.opsForValue().get(cacheKey);if (cachedData != null) {System.out.println("✅ 重试成功,从缓存获取数据");rs.setData(cachedData);} else {// 降级处理:直接查询数据库System.out.println("⚠️ 缓存仍未命中,降级查询数据库");Map<String, Object> pageData = queryDatabaseDirectly(page, size);rs.setData(pageData);}}} catch (Exception e) {System.out.println("❌ 分页查询异常:" + e.getMessage());rs.setCode(100);rs.setMess("查询失败");}return rs;
}
6.2 缓存击穿保护原理
场景:1000个并发请求同时访问第1页,且缓存刚好过期
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━请求1 请求2 请求3 ... 请求1000↓ ↓ ↓ ↓└──────┴──────┴───────────┘↓所有请求发现缓存不存在↓所有请求尝试获取锁:lock:deptPage:1:5↓┌──────┴────────────────────────┐│ │
请求1获取锁成功 🔒 其他999个请求失败│ │
查询数据库 等待100ms│ │
缓存到Redis 重试获取缓存│ │
释放锁 🔓 ✅ 从缓存获取数据│ │
返回数据 返回数据━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
结果:1000个请求 = 1次数据库查询 + 999次Redis查询
数据库压力:✅ 正常运行(没有被击穿)
6.3 关键技术点
① SETNX(Set If Not Exists)
Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked");
- 只有key不存在时才设置成功
- 保证只有一个请求能获取锁
- 实现原子操作
② 锁过期时间(防止死锁)
redisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
- 防止获取锁的线程崩溃导致死锁
- 10秒后自动释放
- 保证系统不会永久阻塞
③ 双重检查(Double Check)
// 获取锁后再次检查缓存
cachedData = redisTemplate.opsForValue().get(cacheKey);
if (cachedData != null) {return cachedData;
}
- 避免重复查询数据库
- 提高并发性能
④ finally块释放锁
try {// 查询数据库
} finally {redisTemplate.delete(lockKey); // 确保释放
}
- 确保锁一定会被释放
- 即使发生异常也能释放
⑤ 降级处理
// 如果等待后还是没有缓存,直接查询数据库
Map<String, Object> pageData = queryDatabaseDirectly(page, size);
- 保证服务可用性
- 避免用户长时间等待
7. 完整Redis Key设计
7.1 所有Key一览表
Key格式 | 示例 | Value类型 | TTL | 说明 |
---|---|---|---|---|
token:{uuid} | token:a1b2c3d4-... | Dept对象 | 2小时 | Token→用户信息映射 |
user:{deptno}:token | user:10:token | String | 2小时 | 用户→Token映射 |
deptList | deptList | List<Dept> | 1小时 | 全部门列表缓存 |
deptPage:{page}:{size} | deptPage:1:5 | Map | 30分钟 | 分页查询缓存 |
lock:deptPage:{page}:{size} | lock:deptPage:1:5 | String | 10秒 | 分布式锁 |
7.2 Key命名规范
业务模块:业务对象:业务标识示例:
token:a1b2c3d4-xxxx → Token模块
user:10:token → 用户模块:用户10:Token
deptPage:1:5 → 部门分页:第1页:每页5条
lock:deptPage:1:5 → 锁:部门分页:第1页:每页5条
7.3 实际Redis存储示例
# 用户10登录后
127.0.0.1:6379> KEYS *
1) "token:a1b2c3d4-e5f6-7890-abcd-ef1234567890"
2) "user:10:token"
3) "deptList"
4) "deptPage:1:5"
5) "deptPage:2:5"# 查看Token内容
127.0.0.1:6379> GET "token:a1b2c3d4-..."
"{\"deptno\":10,\"dname\":\"研发部\",\"loc\":\"北京\"}"# 查看TTL
127.0.0.1:6379> TTL "token:a1b2c3d4-..."
(integer) 7195 # 剩余7195秒# 查看分页缓存
127.0.0.1:6379> GET "deptPage:1:5"
"{\"list\":[...],\"currentPage\":1,\"totalCount\":20,...}"
8. RedisTemplate API总结
8.1 本项目使用的方法
// 1. 存储数据(带过期时间)
redisTemplate.opsForValue().set(key, value, time, TimeUnit.HOURS);// 2. 存储数据(不带过期时间)
redisTemplate.opsForValue().set(key, value);// 3. 获取数据
Object value = redisTemplate.opsForValue().get(key);// 4. 删除单个Key
redisTemplate.delete(key);// 5. 批量删除Key
Set<String> keys = redisTemplate.keys("pattern:*");
redisTemplate.delete(keys);// 6. 模糊查询Key
Set<String> keys = redisTemplate.keys("deptPage:*");// 7. 获取剩余过期时间
Long seconds = redisTemplate.getExpire(key, TimeUnit.SECONDS);// 8. 设置过期时间(续期)
redisTemplate.expire(key, 2, TimeUnit.HOURS);// 9. SETNX(不存在时才设置)
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value);
8.2 方法详解
opsForValue() - 操作String类型
// 存储
redisTemplate.opsForValue().set("key1", "value1");// 存储带过期时间
redisTemplate.opsForValue().set("key1", "value1", 1, TimeUnit.HOURS);// 获取
String value = (String) redisTemplate.opsForValue().get("key1");// SETNX(分布式锁)
Boolean success = redisTemplate.opsForValue().setIfAbsent("lock", "1");
keys() - 模糊查询
// 查找所有以deptPage开头的key
Set<String> keys = redisTemplate.keys("deptPage:*");// 遍历
for (String key : keys) {System.out.println(key);
}
expire() - 设置过期时间
// 设置2小时后过期
redisTemplate.expire("token:xxx", 2, TimeUnit.HOURS);// 设置30分钟后过期
redisTemplate.expire("deptPage:1:5", 30, TimeUnit.MINUTES);// 设置10秒后过期
redisTemplate.expire("lock:xxx", 10, TimeUnit.SECONDS);
getExpire() - 获取剩余时间
// 获取剩余秒数
Long seconds = redisTemplate.getExpire("token:xxx", TimeUnit.SECONDS);// 获取剩余分钟数
Long minutes = redisTemplate.getExpire("token:xxx", TimeUnit.MINUTES);// 判断是否即将过期
if (seconds != null && seconds <= 100) {// 续期redisTemplate.expire("token:xxx", 2, TimeUnit.HOURS);
}
9. 完整业务流程图
9.1 用户登录流程
前端 后端 Redis│ │ ││──── POST /login ──────────>│ ││ (deptno=10, dname=研发部) │ ││ │ ││ │── 验证用户 ───> MySQL ││ │<── 用户信息 ── ││ │ ││ │── 生成Token (UUID) ───────────┐│ │ ││ │── SET token:xxx → Dept对象 ─>││ │ (2小时过期) ││ │ ││ │── GET user:10:token ───────>││ │<── oldToken ────────────────││ │ ││ │── DEL token:oldToken ──────>│ (踢掉旧登录)│ │ ││ │── SET user:10:token → xxx ─>││ │ (2小时过期) ││ │ ││<─── 返回Token ─────────────│ ││ {code:200, data:"xxx"} │ ││ │ ││── localStorage.token=xxx │ │
9.2 Token验证与续期流程
前端 拦截器 Redis│ │ ││── GET /show ────────>│ ││ Header: token=xxx │ ││ │ ││ │── GET token:xxx ───────────>││ │<── Dept对象 ─────────────────││ │ ││ │── PTTL token:xxx ──────────>││ │<── 3600秒 ────────────────────│ (剩余1小时)│ │ ││ │ 剩余时间 > 100秒? ││ │ 是 → 无需续期 ││ │ ││ │── 放行请求 ──────────────────>││<─── 返回页面 ─────────│ ││ │ ││ │ ││ (1小时59分后) │ ││── GET /getDeptsWithPagination ────>│ ││ Header: token=xxx │ ││ │ ││ │── GET token:xxx ───────────>││ │<── Dept对象 ─────────────────││ │ ││ │── PTTL token:xxx ──────────>││ │<── 60秒 ──────────────────────│ (即将过期)│ │ ││ │ 剩余时间 ≤ 100秒? ││ │ 是 → 自动续期! ││ │ ││ │── EXPIRE token:xxx 7200 ───>│ (续期2小时)│ │── EXPIRE user:10:token 7200─>││ │ ││ │── 放行请求 ──────────────────>││<─── 返回数据 ─────────│ │
9.3 分页查询缓存击穿保护流程
1000个并发请求 Redis MySQL│ │ ││──── GET /page?page=1 ──────>│ ││ │ │所有请求查询缓存 │ ││── GET deptPage:1:5 ───────>│ ││<── nil (缓存不存在) ─────────│ ││ │ │所有请求尝试获取锁 │ ││── SETNX lock:deptPage:1:5 ─>│ ││ │ │请求1 │ ││<── OK (获取成功) 🔒 ─────────│ ││── EXPIRE lock:xxx 10秒 ───>│ ││ │ ││── 双重检查缓存 ──────────────>│ ││<── nil ──────────────────────│ ││ │ ││────────────────── 查询数据库 ──────────────────────────>││<───────────────── 返回数据 ────────────────────────────││ │ ││── SET deptPage:1:5 (30分钟)─>│ ││ │ ││── DEL lock:deptPage:1:5 ───>│ (释放锁) 🔓 ││ │ │请求2-1000 │ ││<── nil (获取锁失败) ─────────│ ││ │ ││── 等待100ms │ ││ │ ││── GET deptPage:1:5 ───────>│ (重试获取缓存) ││<── 分页数据 ✅ ───────────────│ ││ │ │━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
结果:1000个请求 = 1次MySQL查询 + 1000次Redis查询数据库压力:✅ 正常(未被击穿)
10. 性能优化对比
10.1 Token验证性能
场景 | 优化前 | 优化后 | 提升 |
---|---|---|---|
方法 | keys("dept:*:token") 遍历 | 直接get("token:xxx") | - |
时间复杂度 | O(n) | O(1) | - |
10个用户 | 遍历10次 | 查询1次 | 10倍 |
100个用户 | 遍历100次 | 查询1次 | 100倍 |
1000个用户 | 遍历1000次 | 查询1次 | 1000倍 |
响应时间(1000用户) | ~50ms | ~0.5ms | 100倍 |
10.2 缓存击穿保护效果
场景 | 无保护 | 有保护 |
---|---|---|
并发请求数 | 1000 | 1000 |
数据库查询次数 | 1000次 💥 | 1次 ✅ |
Redis查询次数 | 1000次 | 1000次 |
数据库压力 | 极高,可能崩溃 | 正常 |
响应时间 | 5-10秒 | 100-200ms |
10.3 分页查询性能
首次查询(缓存未命中)
无缓存版本:查询数据库 → 返回耗时:~50ms有缓存版本:查询数据库 → 缓存到Redis → 返回耗时:~55ms (增加5ms缓存写入时间)
后续查询(缓存命中)
无缓存版本:查询数据库 → 返回耗时:~50ms有缓存版本:查询Redis → 返回耗时:~2ms性能提升:25倍!
30分钟内1000次查询
无缓存版本:1000次数据库查询总耗时:50秒数据库负载:高有缓存版本:1次数据库查询 + 999次Redis查询总耗时:~2.05秒数据库负载:低性能提升:24倍!
📚 附录:Redis命令速查
常用命令
# 查看所有Key
KEYS *# 模糊查询
KEYS token:*
KEYS deptPage:*# 查看Value
GET token:a1b2c3d4-...# 查看TTL(秒)
TTL token:xxx# 查看TTL(毫秒)
PTTL token:xxx# 删除Key
DEL token:xxx# 批量删除
DEL key1 key2 key3# 设置过期时间
EXPIRE token:xxx 7200# 查看Key的类型
TYPE token:xxx# 检查Key是否存在
EXISTS token:xxx
🎯 总结
核心优化点
-
Token验证:O(n) → O(1)
- 避免keys遍历
- 双向映射设计
- 实现单设备登录
-
缓存击穿保护
- 分布式锁
- 双重检查
- 降级处理
-
智能续期机制
- 平衡性能和用户体验
- 减少Redis操作
-
分层缓存策略
- 部门列表:1小时
- 分页数据:30分钟
- Token:2小时
Redis在本项目中的价值
- ✅ 减少数据库查询 95%
- ✅ 提升响应速度 10-100倍
- ✅ 支持水平扩展(多实例共享Session)
- ✅ 防止缓存击穿(保护数据库)
- ✅ 实现无状态认证(Token)
文档版本: v2.0
最后更新: 2025-10-03
联系方式: 13364626905@163.com