JWT + Redis 双 Token 机制:从原理到实战

一套完整的 AccessToken + RefreshToken 解决方案,支持黑名单与自动续期


一、为什么需要双 Token 机制?

1.1 单 Token 的痛点

传统的单 JWT Token 方案存在两个核心问题:

问题 描述
无法主动失效 JWT 一旦签发,在有效期内无法主动使其失效(除非维护黑名单)
安全与体验矛盾 有效期短 → 用户体验差(频繁登录) 有效期长 → 安全风险高(Token 泄露影响大)

1.2 双 Token 方案的设计思想

复制代码
┌─────────────────────────────────────────────────────────────┐
│                      双 Token 分工协作                        │
├─────────────────────────────────────────────────────────────┤
│  AccessToken  │ 短期有效(15分钟)│ 高频使用 │ 泄露影响小      │
│  RefreshToken │ 长期有效(7天)  │ 低频使用 │ 存储在安全位置   │
└─────────────────────────────────────────────────────────────┘

核心思路:用 RefreshToken 的长期有效性,换取 AccessToken 的短期安全性

二、整体架构设计

2.1 架构图

复制代码
┌─────────┐    ① 登录     ┌─────────┐
│         │ ─────────────► │         │
│  客户端  │               │  服务端  │
│         │ ◄───────────── │         │
└─────────┘   ② 返回 Token │         │
     │                      └─────────┘
     │                             │
     │ ③ 携带 AccessToken 请求 API  │
     │ ──────────────────────────► │
     │                             │ ④ 验证 JWT + 黑名单
     │                             │
     │ ⑤ AccessToken 过期(401)    │
     │ ◄────────────────────────── │
     │                             │
     │ ⑥ 携带 RefreshToken 刷新     │
     │ ──────────────────────────► │
     │                             │ ⑦ 验证 Redis 中的 RT
     │                             │
     │ ⑧ 返回新 AccessToken         │
     │ ◄────────────────────────── │
     │                             │
     │ ⑨ 用新 Token 重试原请求       │
     │ ──────────────────────────► │

2.2 Token 生命周期

Token 类型 存储位置 推荐有效期 作用
AccessToken 客户端内存 / localStorage 15分钟 ~ 2小时 访问受保护资源
RefreshToken httpOnly Cookie 7天 ~ 30天 刷新 AccessToken

三、核心代码实现

技术栈:Spring Boot + JWT + Redis

3.1 项目依赖

xml 复制代码
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>4.4.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3.2 Token 实体类

java 复制代码
@Data
@AllArgsConstructor
public class TokenPair {
    private String accessToken;
    private String refreshToken;
}

@Data
public class TokenResponse {
    private String accessToken;
    private String tokenType = "Bearer";
    private Long expiresIn;  // 剩余有效秒数
}

3.3 JWT 工具类

java 复制代码
@Component
public class JwtUtils {
    
    @Value("${jwt.secret}")
    private String secret;
    
    @Value("${jwt.access-token-ttl:900}")  // 默认15分钟
    private Long accessTokenTtl;
    
    /**
     * 生成 AccessToken
     */
    public String generateAccessToken(String userId) {
        return JWT.create()
            .withSubject(userId)
            .withIssuedAt(new Date())
            .withExpiresAt(new Date(System.currentTimeMillis() + accessTokenTtl * 1000))
            .withJWTId(UUID.randomUUID().toString())
            .sign(Algorithm.HMAC256(secret));
    }
    
    /**
     * 验证并解析 AccessToken
     */
    public DecodedJWT verifyAccessToken(String token) throws JWTVerificationException {
        JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret)).build();
        return verifier.verify(token);
    }
    
    /**
     * 获取 Token 剩余有效期(毫秒)
     */
    public long getRemainingTtl(String token) {
        DecodedJWT decoded = JWT.decode(token);
        return decoded.getExpiresAt().getTime() - System.currentTimeMillis();
    }
}

3.4 RefreshToken 服务层

java 复制代码
@Service
@Slf4j
public class RefreshTokenService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Value("${jwt.refresh-token-ttl:604800}")  // 默认7天
    private Long refreshTokenTtl;
    
    /**
     * 创建 RefreshToken
     */
    public String createRefreshToken(String userId) {
        String refreshToken = UUID.randomUUID().toString();
        String key = buildKey(refreshToken);
        
        redisTemplate.opsForValue().set(key, userId, refreshTokenTtl, TimeUnit.SECONDS);
        
        // 记录用户的所有 RefreshToken(用于多设备管理)
        redisTemplate.opsForSet().add("user:tokens:" + userId, refreshToken);
        
        return refreshToken;
    }
    
    /**
     * 验证 RefreshToken
     */
    public String validateAndGetUserId(String refreshToken) {
        String key = buildKey(refreshToken);
        return redisTemplate.opsForValue().get(key);
    }
    
    /**
     * 刷新 RefreshToken(旋转机制)
     */
    public String rotateRefreshToken(String oldRefreshToken, String userId) {
        // 检查是否已被使用
        String usedKey = "used:" + oldRefreshToken;
        if (Boolean.TRUE.equals(redisTemplate.hasKey(usedKey))) {
            // 可能被窃取,注销所有 Token
            revokeAllUserTokens(userId);
            throw new SecurityException("RefreshToken 可能已被窃取,已注销所有会话");
        }
        
        // 标记旧 Token 已使用
        redisTemplate.opsForValue().set(usedKey, "1", 10, TimeUnit.MINUTES);
        
        // 删除旧 Token
        redisTemplate.delete(buildKey(oldRefreshToken));
        redisTemplate.opsForSet().remove("user:tokens:" + userId, oldRefreshToken);
        
        // 生成新 Token
        return createRefreshToken(userId);
    }
    
    /**
     * 注销 RefreshToken
     */
    public void revokeRefreshToken(String refreshToken) {
        String userId = redisTemplate.opsForValue().get(buildKey(refreshToken));
        if (userId != null) {
            redisTemplate.delete(buildKey(refreshToken));
            redisTemplate.opsForSet().remove("user:tokens:" + userId, refreshToken);
        }
    }
    
    /**
     * 注销用户所有 Token
     */
    public void revokeAllUserTokens(String userId) {
        Set<String> tokens = redisTemplate.opsForSet().members("user:tokens:" + userId);
        if (tokens != null) {
            for (String token : tokens) {
                redisTemplate.delete(buildKey(token));
            }
        }
        redisTemplate.delete("user:tokens:" + userId);
    }
    
    private String buildKey(String refreshToken) {
        return "refresh:" + refreshToken;
    }
}

3.5 黑名单服务

java 复制代码
@Service
public class BlacklistService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 将 AccessToken 加入黑名单(登出时使用)
     */
    public void blacklistAccessToken(String accessToken, long ttlMillis) {
        if (ttlMillis > 0) {
            redisTemplate.opsForValue().set(
                "blacklist:" + accessToken,
                "1",
                ttlMillis,
                TimeUnit.MILLISECONDS
            );
        }
    }
    
    /**
     * 检查 Token 是否在黑名单中
     */
    public boolean isBlacklisted(String accessToken) {
        return Boolean.TRUE.equals(redisTemplate.hasKey("blacklist:" + accessToken));
    }
}

3.6 JWT 过滤器(核心)

java 复制代码
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtUtils jwtUtils;
    
    @Autowired
    private BlacklistService blacklistService;
    
    private static final List<String> WHITE_LIST = Arrays.asList(
        "/api/auth/login",
        "/api/auth/refresh",
        "/api/auth/logout",
        "/swagger-ui/**",
        "/v3/api-docs/**"
    );
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        
        // 白名单放行
        String path = request.getRequestURI();
        if (WHITE_LIST.stream().anyMatch(path::startsWith)) {
            chain.doFilter(request, response);
            return;
        }
        
        String accessToken = extractAccessToken(request);
        
        if (StringUtils.isEmpty(accessToken)) {
            sendUnauthorized(response, "缺少 AccessToken");
            return;
        }
        
        try {
            // 1. 验证黑名单
            if (blacklistService.isBlacklisted(accessToken)) {
                sendUnauthorized(response, "Token 已被注销");
                return;
            }
            
            // 2. 验证 JWT
            DecodedJWT decoded = jwtUtils.verifyAccessToken(accessToken);
            String userId = decoded.getSubject();
            
            // 3. 存入上下文
            request.setAttribute("userId", userId);
            
            chain.doFilter(request, response);
            
        } catch (TokenExpiredException e) {
            // AccessToken 过期,通知前端使用 RefreshToken 刷新
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setHeader("X-Token-Expired", "true");
            response.setContentType("application/json");
            response.getWriter().write("{\"code\":401,\"message\":\"AccessToken 已过期\"}");
            
        } catch (JWTVerificationException e) {
            log.warn("Token 验证失败: {}", e.getMessage());
            sendUnauthorized(response, "Token 无效");
        }
    }
    
    private String extractAccessToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
    
    private void sendUnauthorized(HttpServletResponse response, String message) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json");
        response.getWriter().write("{\"code\":401,\"message\":\"" + message + "\"}");
    }
}

3.7 认证控制器

java 复制代码
@RestController
@RequestMapping("/api/auth")
@Slf4j
public class AuthController {
    
    @Autowired
    private JwtUtils jwtUtils;
    
    @Autowired
    private RefreshTokenService refreshTokenService;
    
    @Autowired
    private BlacklistService blacklistService;
    
    /**
     * 登录接口
     */
    @PostMapping("/login")
    public ResponseEntity<TokenResponse> login(@RequestBody LoginRequest request) {
        // 验证用户名密码(省略)
        String userId = authenticate(request);
        
        // 生成 Token 对
        String accessToken = jwtUtils.generateAccessToken(userId);
        String refreshToken = refreshTokenService.createRefreshToken(userId);
        
        // 设置 httpOnly Cookie
        ResponseCookie cookie = ResponseCookie.from("refreshToken", refreshToken)
            .httpOnly(true)
            .secure(true)  // HTTPS 环境
            .sameSite("Strict")
            .path("/api/auth")
            .maxAge(Duration.ofDays(7))
            .build();
        
        TokenResponse response = new TokenResponse(
            accessToken,
            "Bearer",
            jwtUtils.getRemainingTtl(accessToken) / 1000
        );
        
        return ResponseEntity.ok()
            .header(HttpHeaders.SET_COOKIE, cookie.toString())
            .body(response);
    }
    
    /**
     * 刷新 Token(自动续期核心)
     */
    @PostMapping("/refresh")
    public ResponseEntity<TokenResponse> refreshToken(
            @CookieValue(value = "refreshToken", required = false) String refreshToken,
            HttpServletResponse response) {
        
        if (StringUtils.isEmpty(refreshToken)) {
            return ResponseEntity.status(401).build();
        }
        
        // 1. 验证 RefreshToken
        String userId = refreshTokenService.validateAndGetUserId(refreshToken);
        if (userId == null) {
            return ResponseEntity.status(401).body(null);
        }
        
        // 2. 旋转 RefreshToken(安全增强)
        String newRefreshToken;
        try {
            newRefreshToken = refreshTokenService.rotateRefreshToken(refreshToken, userId);
        } catch (SecurityException e) {
            return ResponseEntity.status(401).body(null);
        }
        
        // 3. 生成新 AccessToken
        String newAccessToken = jwtUtils.generateAccessToken(userId);
        
        // 4. 设置新的 RefreshToken Cookie
        ResponseCookie cookie = ResponseCookie.from("refreshToken", newRefreshToken)
            .httpOnly(true)
            .secure(true)
            .sameSite("Strict")
            .path("/api/auth")
            .maxAge(Duration.ofDays(7))
            .build();
        
        TokenResponse tokenResponse = new TokenResponse(
            newAccessToken,
            "Bearer",
            jwtUtils.getRemainingTtl(newAccessToken) / 1000
        );
        
        return ResponseEntity.ok()
            .header(HttpHeaders.SET_COOKIE, cookie.toString())
            .body(tokenResponse);
    }
    
    /**
     * 登出接口
     */
    @PostMapping("/logout")
    public ResponseEntity<Void> logout(
            @RequestHeader("Authorization") String authorization,
            @CookieValue("refreshToken") String refreshToken) {
        
        String accessToken = authorization.substring(7);
        
        // 将 AccessToken 加入黑名单
        long remainingTtl = jwtUtils.getRemainingTtl(accessToken);
        blacklistService.blacklistAccessToken(accessToken, remainingTtl);
        
        // 注销 RefreshToken
        refreshTokenService.revokeRefreshToken(refreshToken);
        
        // 清除 Cookie
        ResponseCookie cookie = ResponseCookie.from("refreshToken", "")
            .httpOnly(true)
            .maxAge(0)
            .path("/api/auth")
            .build();
        
        return ResponseEntity.ok()
            .header(HttpHeaders.SET_COOKIE, cookie.toString())
            .build();
    }
}

四、前端实现(自动续期核心)

4.1 Axios 拦截器实现

javascript 复制代码
// auth.js

const API_BASE_URL = 'https://api.example.com';

// 是否正在刷新中
let isRefreshing = false;
// 等待队列
let refreshSubscribers = [];

class AuthService {
    
    constructor() {
        this.setupInterceptors();
    }
    
    /**
     * 设置 Axios 拦截器
     */
    setupInterceptors() {
        // 请求拦截器:添加 AccessToken
        axios.interceptors.request.use(config => {
            const token = localStorage.getItem('accessToken');
            if (token) {
                config.headers['Authorization'] = `Bearer ${token}`;
            }
            return config;
        });
        
        // 响应拦截器:处理 Token 过期
        axios.interceptors.response.use(
            response => response,
            async error => {
                const originalRequest = error.config;
                
                // 检查是否是 Token 过期
                if (error.response?.status === 401 && 
                    error.response?.headers['x-token-expired'] === 'true' &&
                    !originalRequest._retry) {
                    
                    originalRequest._retry = true;
                    
                    try {
                        const newToken = await this.refreshAccessToken();
                        if (newToken) {
                            // 重试原请求
                            originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
                            return axios(originalRequest);
                        }
                    } catch (refreshError) {
                        // 刷新失败,跳转登录
                        this.redirectToLogin();
                        return Promise.reject(refreshError);
                    }
                }
                
                return Promise.reject(error);
            }
        );
    }
    
    /**
     * 刷新 AccessToken
     */
    async refreshAccessToken() {
        // 防止并发刷新
        if (!isRefreshing) {
            isRefreshing = true;
            
            try {
                // RefreshToken 会自动通过 Cookie 携带
                const response = await axios.post(`${API_BASE_URL}/api/auth/refresh`, {}, {
                    withCredentials: true  // 允许携带 Cookie
                });
                
                const { accessToken, expiresIn } = response.data;
                
                // 存储新 Token
                localStorage.setItem('accessToken', accessToken);
                
                // 设置定时器,提前刷新
                this.scheduleAutoRefresh(expiresIn);
                
                // 通知等待队列
                this.onRefreshed(accessToken);
                
                return accessToken;
                
            } catch (error) {
                this.onRefreshed(null);
                throw error;
            } finally {
                isRefreshing = false;
                refreshSubscribers = [];
            }
        }
        
        // 正在刷新中,加入等待队列
        return new Promise(resolve => {
            refreshSubscribers.push(token => resolve(token));
        });
    }
    
    /**
     * 刷新完成后的回调
     */
    onRefreshed(token) {
        refreshSubscribers.forEach(callback => callback(token));
    }
    
    /**
     * 定时自动刷新(可选)
     * 在 Token 过期前 1 分钟主动刷新
     */
    scheduleAutoRefresh(expiresInSeconds) {
        const refreshTime = (expiresInSeconds - 60) * 1000; // 提前1分钟
        
        if (refreshTime > 0) {
            setTimeout(async () => {
                try {
                    await this.refreshAccessToken();
                    console.log('自动刷新 Token 成功');
                } catch (error) {
                    console.warn('自动刷新失败', error);
                }
            }, refreshTime);
        }
    }
    
    /**
     * 跳转登录页
     */
    redirectToLogin() {
        localStorage.removeItem('accessToken');
        window.location.href = '/login';
    }
    
    /**
     * 登录
     */
    async login(username, password) {
        const response = await axios.post(`${API_BASE_URL}/api/auth/login`, {
            username,
            password
        }, { withCredentials: true });
        
        const { accessToken, expiresIn } = response.data;
        localStorage.setItem('accessToken', accessToken);
        this.scheduleAutoRefresh(expiresIn);
        
        return response.data;
    }
    
    /**
     * 登出
     */
    async logout() {
        try {
            await axios.post(`${API_BASE_URL}/api/auth/logout`, {}, {
                withCredentials: true,
                headers: {
                    'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
                }
            });
        } finally {
            localStorage.removeItem('accessToken');
            window.location.href = '/login';
        }
    }
}

export default new AuthService();

五、自动续期的工作原理

5.1 核心流程图

复制代码
用户操作     前端                   后端
   │         │                      │
   │  发起请求携带 AccessToken       │
   ├────────►│                      │
   │         │  验证 AccessToken     │
   │         ├─────────────────────►│
   │         │                      │
   │         │  过期,返回 401       │
   │         │◄─────────────────────┤
   │         │                      │
   │         │  调用 /refresh        │
   │         ├─────────────────────►│
   │         │                      │
   │         │  验证 RefreshToken    │
   │         │  生成新 AccessToken   │
   │         │◄─────────────────────┤
   │         │                      │
   │         │  用新 Token 重试请求  │
   │         ├─────────────────────►│
   │         │                      │
   │◄────────┤  返回正常响应         │
   │         │◄─────────────────────┤
   │         │                      │
   │  用户无感知完成续期             │

5.2 为什么能做到"自动"?

层面 实现方式
检测层 前端拦截器识别 401 + X-Token-Expired
刷新层 自动调用 /refresh 接口获取新 Token
重试层 用新 Token 自动重试原始请求
用户感知 整个过程异步完成,用户无感知

5.3 并发请求处理

当多个请求同时发现 Token 过期时:

复制代码
请求A ──┐
请求B ──┼── 检测到过期 ──► 请求A 发起刷新 ──► 新Token
请求C ──┘                              │
                                       ▼
                      请求B、C 等待 ──► 使用新Token重试

六、安全增强机制

6.1 安全措施汇总

复制代码
┌─────────────────────────────────────────────────────────────┐
│                      多层安全防护                            │
├─────────────────────────────────────────────────────────────┤
│  ✓ RefreshToken 使用 httpOnly Cookie     → 防 XSS 窃取      │
│  ✓ RefreshToken 旋转机制                 → 防重放攻击       │
│  ✓ AccessToken 黑名单                    → 主动失效能力     │
│  ✓ 设备指纹绑定(可选)                   → 防 Token 盗用    │
│  ✓ 异常检测 + 全家桶注销                  → 检测到攻击响应   │
│  ✓ HTTPS 传输                           → 防中间人攻击     │
└─────────────────────────────────────────────────────────────┘

6.2 RefreshToken 旋转详解

java 复制代码
// 旋转机制核心逻辑
public String rotateRefreshToken(String oldToken, String userId) {
    // 1. 检查旧 Token 是否已被使用
    if (redisTemplate.hasKey("used:" + oldToken)) {
        // 攻击检测:同一 Token 被使用两次
        revokeAllUserTokens(userId);  // 注销用户所有会话
        throw new SecurityException("Token replay attack detected");
    }
    
    // 2. 标记旧 Token 已使用
    redisTemplate.opsForValue().set("used:" + oldToken, "1", 10, TimeUnit.MINUTES);
    
    // 3. 删除旧 Token
    redisTemplate.delete("refresh:" + oldToken);
    
    // 4. 生成新 Token
    return createRefreshToken(userId);
}

6.3 设备指纹绑定(进阶)

java 复制代码
// 生成 RefreshToken 时绑定设备指纹
public String createRefreshToken(String userId, String deviceFingerprint) {
    String refreshToken = UUID.randomUUID().toString();
    RefreshTokenEntity entity = new RefreshTokenEntity(userId, deviceFingerprint);
    
    redisTemplate.opsForValue().set(
        "refresh:" + refreshToken,
        JSON.toJSONString(entity),
        7, TimeUnit.DAYS
    );
    
    return refreshToken;
}

// 验证时检查设备指纹
public boolean validate(String refreshToken, String deviceFingerprint) {
    String json = redisTemplate.opsForValue().get("refresh:" + refreshToken);
    RefreshTokenEntity entity = JSON.parseObject(json, RefreshTokenEntity.class);
    
    if (!entity.getDeviceFingerprint().equals(deviceFingerprint)) {
        // 设备变更,可能存在风险
        revokeAllUserTokens(entity.getUserId());
        return false;
    }
    
    return true;
}

七、配置与调优

7.1 配置文件

yaml 复制代码
# application.yml
jwt:
  secret: ${JWT_SECRET:your-256-bit-secret-key-here}
  access-token-ttl: 900      # 15分钟(秒)
  refresh-token-ttl: 604800  # 7天(秒)

spring:
  redis:
    host: ${REDIS_HOST:localhost}
    port: ${REDIS_PORT:6379}
    password: ${REDIS_PASSWORD:}
    timeout: 5000ms
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0

7.2 有效期推荐配置

场景 AccessToken RefreshToken
高安全(银行类) 5分钟 1小时
普通 Web 应用 15分钟 7天
移动 App 1小时 30天
内部系统 2小时 7天

八、常见问题与解决方案

Q1:为什么不用 Redis 存储所有 Token?

方案 优点 缺点
纯 Redis 可主动失效 每次请求查 Redis,性能差
纯 JWT 无状态,性能好 无法主动失效
JWT + Redis 黑名单 兼顾性能与可控 实现稍复杂

Q2:RefreshToken 被窃取了怎么办?

java 复制代码
// 方案1:设备指纹检测
// 方案2:异常行为检测(IP、地理位置突变)
// 方案3:提供"注销所有设备"功能

Q3:多设备登录如何处理?

java 复制代码
// 每个设备独立的 RefreshToken
// Redis 结构:user:tokens:{userId} -> Set<refreshToken>

// 查询用户所有设备
Set<String> tokens = redisTemplate.opsForSet().members("user:tokens:" + userId);

// 远程注销指定设备
public void revokeDevice(String userId, String deviceId) {
    // 根据 deviceId 找到对应的 RefreshToken 并删除
}

九、总结

核心要点

  1. AccessToken 短时效 + JWT → 无状态高性能
  2. RefreshToken 长时效 + Redis → 可主动撤销
  3. 前端拦截器 + 401 检测 → 实现无感自动续期
  4. 黑名单机制 → 解决 JWT 无法主动失效问题
  5. Token 旋转 → 防止重放攻击

架构优势

维度 效果
安全性 ⭐⭐⭐⭐⭐ 多层防护
用户体验 ⭐⭐⭐⭐⭐ 无感续期
性能 ⭐⭐⭐⭐ JWT 无状态验证
可维护性 ⭐⭐⭐⭐ 清晰的分层设计

相关推荐
splage2 小时前
maven导入spring框架
数据库·spring·maven
HalvmånEver2 小时前
MySQL数据库基础入门总结(从0到1)
linux·数据库·mysql
执笔画情ora2 小时前
My-Oracle数据库优化-with as 分析优化
数据库·sql
xhuiting2 小时前
Redis专题
redis·缓存
专注API从业者2 小时前
淘宝 API 调用链路追踪实战:基于 SkyWalking/Pinpoint 的全链路监控搭建
大数据·开发语言·数据库·skywalking
stone52 小时前
一个主从库主键同步的方案(未完)
数据库·oracle
菜菜小狗的学习笔记3 小时前
黑马程序员Redis--问题整理(黑马点评)
数据库·redis·缓存
leo_messi943 小时前
2026版商城项目(二)-- 压力测试&缓存
java·缓存·压力测试·springcloud
不会聊天真君6473 小时前
pgsql笔记
数据库·笔记