前言
在现代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黑名单机制
通过持续的优化和迭代,可以构建出更加高效、安全的用户认证系统。