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来进行资源的拷贝准备工作,依赖如下

java 复制代码
<?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配置如下项

java 复制代码
server:
  port: 8080
      
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/company_db?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: root
        
  redis:
    host: 192.168.1.115


mybatis:
  type-aliases-package: com.jr.pojo
  mapper-locations: classpath:com/jr/mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

第二步-实体类

这个就是正常根据数据库(建议名称和表名一样,字段和属性相同),项目是springBoot,记得要添加Compenent注解

java 复制代码
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;
    }

    @Override
    public String toString() {
        return "Dept{" +
                "deptno=" + deptno +
                ", dname='" + dname + '\'' +
                ", loc='" + loc + '\'' +
                '}';
    }
}

第三步-基础工具类

RedisConfig.java

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会将返回值注册为Bean
    public 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

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

java 复制代码
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

java 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//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接口

java 复制代码
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实现类

java 复制代码
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层()

java 复制代码
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){
            // 生成唯一Token
            String 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. 生成缓存key
        String 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 {
            // 使用通配符查找所有分页缓存的key
            java.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

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

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

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设计](#完整Redis Key设计)
  8. [RedisTemplate API总结](#RedisTemplate API总结)
  9. 完整业务流程图
  10. 性能优化对比

1. Redis配置与初始化

1.1 配置文件:application.yml

yaml 复制代码
spring:
  redis:
    host: 192.168.1.115  # Redis服务器地址
    # port: 6379         # 默认端口(可省略)
    # password:          # 如果有密码
    # database: 0        # 使用的数据库编号

1.2 Redis配置类:RedisConfig.java

java 复制代码
@Configuration
public class RedisConfig {
    
    @Bean
    public 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() 方法

java 复制代码
@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,                  // 过期时间: 2
            TimeUnit.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:token
            token,                                 // 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_A

10: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() 方法

java 复制代码
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小时");
            
            // 续期Token
            redisTemplate.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 增删改操作的缓存策略

添加部门
java 复制代码
@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;
}
删除部门
java 复制代码
@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;
}
更新部门
java 复制代码
@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() 方法

java 复制代码
@RequestMapping("/getDeptsWithPagination")
@ResponseBody
public Result getDeptsWithPagination(int page, int size) {
    // 1. 生成缓存Key
    String 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)
java 复制代码
Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked");
  • 只有key不存在时才设置成功
  • 保证只有一个请求能获取锁
  • 实现原子操作
② 锁过期时间(防止死锁)
java 复制代码
redisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
  • 防止获取锁的线程崩溃导致死锁
  • 10秒后自动释放
  • 保证系统不会永久阻塞
③ 双重检查(Double Check)
java 复制代码
// 获取锁后再次检查缓存
cachedData = redisTemplate.opsForValue().get(cacheKey);
if (cachedData != null) {
    return cachedData;
}
  • 避免重复查询数据库
  • 提高并发性能
④ finally块释放锁
java 复制代码
try {
    // 查询数据库
} finally {
    redisTemplate.delete(lockKey);  // 确保释放
}
  • 确保锁一定会被释放
  • 即使发生异常也能释放
⑤ 降级处理
java 复制代码
// 如果等待后还是没有缓存,直接查询数据库
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存储示例

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 本项目使用的方法

java 复制代码
// 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类型
java 复制代码
// 存储
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() - 模糊查询
java 复制代码
// 查找所有以deptPage开头的key
Set<String> keys = redisTemplate.keys("deptPage:*");

// 遍历
for (String key : keys) {
    System.out.println(key);
}
expire() - 设置过期时间
java 复制代码
// 设置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() - 获取剩余时间
java 复制代码
// 获取剩余秒数
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命令速查

常用命令

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

相关推荐
李慕婉学姐1 分钟前
Springboot加盟平台推荐可视化系统ktdx2ldg(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
小满、2 小时前
MySQL :实用函数、约束、多表查询与事务隔离
数据库·mysql·事务·数据库函数·多表查询
百***35333 小时前
PostgreSQL_安装部署
数据库·postgresql
thekenofdis5 小时前
Lua脚本执行多个redis命令提示“CROSSSLOT Keys in request don‘t hash to the same slot“问题
redis·lua·哈希算法
rayylee5 小时前
生活抱怨与解决方案app
数据库·生活
没有bug.的程序员6 小时前
Spring Cloud Gateway 性能优化与限流设计
java·spring boot·spring·nacos·性能优化·gateway·springcloud
Lucifer三思而后行7 小时前
使用 BR 备份 TiDB 到 AWS S3 存储
数据库·tidb·aws
百***17078 小时前
Oracle分页sql
数据库·sql·oracle
qq_436962188 小时前
数据中台:打破企业数据孤岛,实现全域资产化的关键一步
数据库·人工智能·信息可视化·数据挖掘·数据分析