当前位置: 首页 > news >正文

SSM框架下的redis使用以及token认证

  1. pom.xml + application.yml(基础配置)
  2. 实体类 Dept.java(数据模型)
  3. 基础工具类(Result.java、RedisConfig.java)
  4. Mapper层(DeptMapper.java + DeptMapper.xml)
  5. Service层(DeptService.java + DeptServiceImpl.java)
  6. Controller层(DeptController.java - 定义所有接口路径)✨关键
  7. 拦截器配置(TokenInterceptor.java + WebMvcConfig.java)
  8. 启动类(SpringBootMain.java)
  9. 前端页面

第一步-基础配置

首先创建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 使用全流程详解

📋 目录

  1. Redis配置与初始化
  2. Redis在项目中的四大用途
  3. 场景1:用户登录与Token管理
  4. 场景2:Token验证与智能续期
  5. 场景3:部门列表缓存
  6. 场景4:分页查询缓存与击穿保护
  7. 完整Redis Key设计
  8. RedisTemplate API总结
  9. 完整业务流程图
  10. 性能优化对比

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)
用户 → Tokenuser:{deptno}:token实现单设备登录、查询用户TokenO(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}:tokenuser:10:tokenString2小时用户→Token映射
deptListdeptListList<Dept>1小时全部门列表缓存
deptPage:{page}:{size}deptPage:1:5Map30分钟分页查询缓存
lock:deptPage:{page}:{size}lock:deptPage:1:5String10秒分布式锁

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.5ms100倍

10.2 缓存击穿保护效果

场景无保护有保护
并发请求数10001000
数据库查询次数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

🎯 总结

核心优化点

  1. Token验证:O(n) → O(1)

    • 避免keys遍历
    • 双向映射设计
    • 实现单设备登录
  2. 缓存击穿保护

    • 分布式锁
    • 双重检查
    • 降级处理
  3. 智能续期机制

    • 平衡性能和用户体验
    • 减少Redis操作
  4. 分层缓存策略

    • 部门列表:1小时
    • 分页数据:30分钟
    • Token:2小时

Redis在本项目中的价值

  • ✅ 减少数据库查询 95%
  • ✅ 提升响应速度 10-100倍
  • ✅ 支持水平扩展(多实例共享Session)
  • ✅ 防止缓存击穿(保护数据库)
  • ✅ 实现无状态认证(Token)

文档版本: v2.0

最后更新: 2025-10-03

联系方式: 13364626905@163.com

http://www.dtcms.com/a/439310.html

相关文章:

  • 十堰seo百度搜索引擎优化方式
  • 连锁酒店网站建设微信版本的wordpress
  • 网站建设分金手指专业一网络运营主要做什么
  • 人工智能专业术语详解(C)
  • EfficientNet:复合缩放
  • 淄博网站的建设wordpress好用的编辑器
  • MyBatis 基础
  • 自建网站和租用空间网站网站公司的客户怎么来
  • Spark的Broadcast Join以及其它的Join策略
  • 宝安做网站的公司网站快速排名的方法
  • 重庆网站建设公司的网站西安做商铺的网站
  • 嵌入式开发学习日志33——stm32之PWM舵机简单项目
  • 桂林旅游网站建设品牌营销的四大策略
  • 为什么Java线程栈容易溢出?
  • 怎么做福彩网站营销系统
  • Java 后端与 AI 融合:技术路径、实战案例与未来趋势
  • 一键建站公司wordpress 404 插件
  • 大连网站设计培训班网站建设公司推荐互赢网络
  • 网站一般建什么百度公司官网招聘
  • 如何使用unity制作游戏
  • Mosquitto 安全架构深度解析:security.c 与 security_default.c 的作用与协同机制
  • 国外打开网站会不会乱码龙岗做商城网站建设
  • css选择器继承性
  • 做投资的网站高端网站建设成都
  • 丹阳网站怎么做seo主机屋 WordPress 问题 多
  • 中文名字英文名字日本名字txt合集
  • 搜狗seo快速排名公司山东东营网络seo
  • 如何做百度的网站手工制作花朵
  • 【2025最新】ArcGIS for JS 实现地图卷帘效果
  • 网站域名备案密码新网站 被百度收录