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

相关推荐
神的孩子都在歌唱2 小时前
PostgreSQL向量检索:pgvector入门指南
数据库·postgresql
编程充电站pro2 小时前
SQL 多表查询实用技巧:ON 和 WHERE 的区别速览
数据库·sql
Swift社区2 小时前
SQL 执行异常排查 java.sql.SQLException:从 SQLException 说起
java·数据库·sql
ss2732 小时前
手写MyBatis第88弹:从XML配置到可执行SQL的完整旅程
java·开发语言·mybatis
qq_417908442 小时前
com.mysql.cj.jdbc.Driver 解析
数据库·mysql
Terio_my3 小时前
Spring Boot 集成 EHCache 缓存解决方案
spring boot·spring·缓存
岁岁岁平安3 小时前
SpringBoot3+WebSocket+Vue3+TypeScript实现简易在线聊天室(附完整源码参考)
java·spring boot·websocket·网络协议·typescript·vue
胡斌附体3 小时前
springbatch使用记录
数据库·接口·shell·命令·批量·springbatch·并发抽取
java水泥工5 小时前
网上摄影工作室|基于SpringBoot和Vue的网上摄影工作室(源码+数据库+文档)
数据库·vue.js·spring boot