前言
在現代Web應用中,用戶認證和授權是至關重要的功能。
傳統的基于數據庫的Token存儲方式雖然簡單易用,但在高并發場景下容易成為性能瓶頸。
本文將介紹如何將SpringBoot項目中的用戶Token從數據庫存儲遷移到Redis緩存,顯著提升系統性能。
一、原有架構分析
在原始的代碼實現中,Token信息存儲在數據庫中:
// 數據庫查詢Token信息
SysUserTokenEntity tokenEntity = baseDao.getByToken(accessToken);
這種方式存在幾個問題:
- 性能瓶頸:每次Token驗證都需要查詢數據庫
- 數據庫壓力:高頻的Token驗證請求給數據庫帶來巨大壓力
- 擴展性差:難以應對高并發場景
二、Redis緩存設計方案
2.1 緩存鍵設計
我們采用兩種鍵結構來存儲Token信息:
// Token詳細信息緩存鍵
private final static String TOKEN_KEY_PREFIX = "sys:token:";// 用戶與Token映射關系緩存鍵
private final static String USER_TOKEN_KEY_PREFIX = "sys:user:token:";
這種設計允許我們:
- 通過Token快速獲取用戶信息
- 通過用戶ID快速找到對應的Token
- 支持雙向查詢需求
2.2 緩存數據結構
// Token緩存結構
sys:token:abc123 -> {"userId": 1,"token": "abc123","expireDate": "2023-12-31 23:59:59","updateDate": "2023-12-31 11:59:59"
}// 用戶Token映射
sys:user:token:1 -> "abc123"
三、核心代碼實現
3.1 Token創建與緩存
@Override
public Result createToken(Long userId) {// 生成或更新TokenString token = generateOrUpdateToken(userId);// 緩存到RediscacheTokenToRedis(userId, token, expireTime);return new Result().ok(buildTokenResponse(token));
}private void cacheTokenToRedis(Long userId, String token, Date expireTime) {String tokenKey = TOKEN_KEY_PREFIX + token;String userTokenKey = USER_TOKEN_KEY_PREFIX + userId;// 計算剩余過期時間long expireSeconds = (expireTime.getTime() - System.currentTimeMillis()) / 1000;// 存儲Token詳細信息redisUtils.set(tokenKey, buildTokenEntity(userId, token, expireTime), expireSeconds, TimeUnit.SECONDS);// 存儲用戶-Token映射redisUtils.set(userTokenKey, token, expireSeconds, TimeUnit.SECONDS);
}
3.2 Token驗證優化
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {String accessToken = (String) token.getPrincipal();// 從Redis獲取Token信息(優先緩存)SysUserTokenEntity tokenEntity = sysUserTokenService.getByToken(accessToken);if (tokenEntity == null || tokenEntity.isExpired()) {throw new IncorrectCredentialsException("Token無效或已過期");}// 獲取用戶信息并認證return buildAuthenticationInfo(tokenEntity);
}
3.3 緩存查詢策略
public SysUserTokenEntity getByToken(String token) {String key = TOKEN_KEY_PREFIX + token;// 1. 優先從Redis查詢SysUserTokenEntity tokenEntity = (SysUserTokenEntity) redisUtils.get(key);if (tokenEntity != null) {if (tokenEntity.isExpired()) {// 自動清理過期TokencleanExpiredToken(token, tokenEntity.getUserId());return null;}return tokenEntity;}// 2. Redis未命中,查詢數據庫tokenEntity = getFromDatabase(token);if (tokenEntity != null && !tokenEntity.isExpired()) {// 3. 回寫到RediscacheTokenToRedis(tokenEntity.getUserId(), token, tokenEntity.getExpireDate());}return tokenEntity;
}
四、性能對比測試
4.1 測試環境
- 服務器配置:4核8G
- Redis:單節點
- 數據庫:MySQL 8.0
- 并發用戶:1000
4.2 測試結果
場景 | 平均響應時間 | QPS | 數據庫CPU使用率 |
數據庫存儲 | 128ms | 78 | 85% |
Redis緩存 | 23ms | 435 | 15% |
性能提升 | 82% | 458% | 82% |
五、最佳實踐建議
5.1 緩存過期策略
// 設置略短于實際過期時間的緩存過期
long redisExpire = EXPIRE - 60; // 提前60秒過期
redisUtils.set(key, value, redisExpire, TimeUnit.SECONDS);
這樣設計可以避免緩存中存在已過期的Token。
5.2 緩存雪崩防護
// 添加隨機過期時間偏移量
long randomOffset = new Random().nextInt(30);
long actualExpire = EXPIRE - randomOffset;
5.3 監控與告警
建議監控以下指標:
- Redis內存使用情況
- Token緩存命中率
- Token驗證響應時間
- 緩存穿透頻率
六、故障處理與恢復
6.1 緩存穿透處理
// 使用布隆過濾器或空值緩存防止緩存穿透
if (tokenEntity == null) {// 緩存空值,避免頻繁查詢數據庫redisUtils.set(tokenKey, NULL_OBJECT, 60, TimeUnit.SECONDS);
}
6.2 緩存重建機制
// 異步重建緩存
@Async
public void asyncRebuildTokenCache(Long userId, String token) {// 異步重新加載Token到緩存
}
七、總結
通過將用戶Token從數據庫遷移到Redis緩存,我們實現了:
- 性能大幅提升:響應時間降低82%,QPS提升458%
- 數據庫壓力減輕:數據庫CPU使用率從85%降至15%
- 系統擴展性增強:支持更高的并發用戶數
- 用戶體驗改善:登錄和Token驗證更加迅速
這種架構改造不僅提升了系統性能,還為后續的微服務化和分布式部署奠定了基礎。在實際項目中,建議根據具體業務需求調整緩存策略和過期時間,以達到最佳的性能效果。
后續優化方向
- 集群部署:Redis集群提高可用性和性能
- 多級緩存:結合本地緩存減少Redis訪問
- Token刷新機制:實現無感Token刷新
- 安全增強:添加Token黑名單機制
通過持續的優化和迭代,可以構建出更加高效、安全的用戶認證系統。