Redis 在权限系统多场景应用实践
Redis 在权限系统多场景应用实践
引言
Redis 作为高性能内存数据库,在企业级权限系统中扮演着不可替代的角色。权限系统对响应速度和数据一致性要求极高——每次API请求都需要校验权限,如果每次都查询数据库,将成为系统瓶颈。通过 Redis 的多场景应用,可以实现毫秒级权限校验、分布式环境下的会话共享、登录安全防护等关键能力。
本文将系统介绍 Redis 在权限系统中的六大应用场景,涵盖配置方法、代码示例和最佳实践。
核心内容
Redis 在权限系统中的六大应用场景
graph TD
A[Redis在权限系统中的应用] --> B[会话共享]
A --> C[权限缓存]
A --> D[分布式锁]
A --> E[登录失败计数]
A --> F[字典缓存]
A --> G[向量存储]
B --> B1[多实例会话同步]
B --> B2[单点登录控制]
C --> C1[用户权限缓存]
C --> C2[角色权限缓存]
D --> D1[防止重复操作]
D --> D2[定时任务互斥]
E --> E1[暴力破解防护]
E --> E2[账号锁定机制]
F --> F1[数据字典缓存]
F --> F2[配置参数缓存]
G --> G1[AI向量检索]
G --> G2[知识库RAG]
一、基础配置
Redis 连接配置
# application.yml
spring:
data:
redis:
host: 127.0.0.1
port: 6379
password: ${REDIS_PASSWORD:}
database: 0
timeout: 5000ms
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
max-wait: 3000ms
Redis 配置类
@Configuration
public class RedisConfig {
/**
* 自定义RedisTemplate,配置序列化策略
* Key使用String序列化,Value使用JSON序列化
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// Key序列化:String
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// Value序列化:JSON
Jackson2JsonRedisSerializer<Object> jsonSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL
);
jsonSerializer.setObjectMapper(om);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
二、场景一:会话共享
在多实例部署环境下,用户登录状态需要跨实例共享。结合 Sa-Token 框架,将 Token 数据存储到 Redis 中。
sequenceDiagram
participant 用户
participant 网关
participant 实例A
participant 实例B
participant Redis
用户->>网关: 登录请求
网关->>实例A: 转发请求
实例A->>Redis: 存储Token信息
实例A-->>用户: 返回Token
用户->>网关: 业务请求(携带Token)
网关->>实例B: 转发请求
实例B->>Redis: 读取Token信息
Redis-->>实例B: 返回Token数据
实例B-->>用户: 业务响应
Sa-Token Redis 集成配置
<!-- Sa-Token 整合 Redis -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>${sa-token.version}</version>
</dependency>
# Sa-Token 配置
sa-token:
token-name: Authorization
timeout: 86400 # Token有效期24小时
active-timeout: 1800 # 活跃超时30分钟
is-concurrent: true # 允许多端同时登录
is-share: false # 不共享Token
token-style: uuid
is-log: false
单点登录控制
@Service
public class SsoService {
/**
* 登录并控制单设备在线
* 同一账号只允许一个设备在线,新登录会踢掉旧会话
*/
public LoginResult login(String username, String password) {
// 校验账号密码
User user = userService.validateUser(username, password);
// 获取当前账号已登录的Token
StpUtil.login(user.getId());
// 踢掉之前登录的设备
List<String> tokenList = StpUtil.getTokenValueListByLoginId(user.getId());
for (String oldToken : tokenList) {
if (!StpUtil.getTokenValue().equals(oldToken)) {
StpUtil.kickoutByTokenValue(oldToken);
}
}
return new LoginResult(StpUtil.getTokenValue(), user);
}
}
三、场景二:权限缓存
权限数据是高频访问但低频变更的典型缓存场景。每次请求都需要校验权限,缓存命中可大幅减少数据库查询。
权限缓存设计
graph TD
A[API请求] --> B{Redis缓存命中?}
B -->|是| C[返回缓存权限]
B -->|否| D[查询数据库]
D --> E[写入Redis缓存]
E --> C
C --> F[权限校验]
G[权限变更事件] --> H[清除相关缓存]
H --> I[下次请求重新加载]
style B fill:#fff9c4
style H fill:#ffcdd2
@Service
public class PermissionCacheService {
private static final String PERM_CACHE_PREFIX = "perm:user:";
private static final String ROLE_CACHE_PREFIX = "perm:role:";
private static final long CACHE_TTL_HOURS = 2;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 获取用户权限列表(带缓存)
* @param userId 用户ID
* @return 权限标识集合
*/
@SuppressWarnings("unchecked")
public Set<String> getUserPermissions(Long userId) {
String cacheKey = PERM_CACHE_PREFIX + userId;
// 1. 尝试从缓存获取
Object cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return (Set<String>) cached;
}
// 2. 缓存未命中,查询数据库
Set<String> permissions = permissionMapper.selectPermissionsByUserId(userId);
// 3. 写入缓存,设置过期时间
redisTemplate.opsForValue().set(cacheKey, permissions,
CACHE_TTL_HOURS, TimeUnit.HOURS);
return permissions;
}
/**
* 清除用户权限缓存
* 在用户角色变更、权限变更时调用
*/
public void clearUserPermissionCache(Long userId) {
redisTemplate.delete(PERM_CACHE_PREFIX + userId);
}
/**
* 清除角色关联的所有用户权限缓存
* 角色权限变更时,需要清除该角色下所有用户的缓存
*/
public void clearRoleUsersCache(Long roleId) {
List<Long> userIds = userRoleMapper.selectUserIdsByRoleId(roleId);
List<String> keys = userIds.stream()
.map(id -> PERM_CACHE_PREFIX + id)
.toList();
redisTemplate.delete(keys);
}
}
四、场景三:分布式锁
分布式锁用于防止分布式环境下的重复操作,如防止重复提交、定时任务互斥执行等。
@Service
public class DistributedLockService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 尝试获取分布式锁
* @param lockKey 锁的key
* @param requestId 请求标识(用于安全释放锁)
* @param expireSeconds 锁的过期时间(秒)
* @return 是否获取成功
*/
public boolean tryLock(String lockKey, String requestId, long expireSeconds) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
/**
* 释放分布式锁(Lua脚本保证原子性)
* 只有锁的持有者才能释放锁
*/
public boolean unlock(String lockKey, String requestId) {
String script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
""";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(redisScript,
Collections.singletonList(lockKey), requestId);
return Long.valueOf(1L).equals(result);
}
}
防重复提交注解
/** 防重复提交注解 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
/** 间隔时间(秒),默认3秒 */
int interval() default 3;
}
/** 防重复提交切面 */
@Aspect
@Component
public class RepeatSubmitAspect {
@Autowired
private DistributedLockService lockService;
@Around("@annotation(repeatSubmit)")
public Object around(ProceedingJoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
String lockKey = "repeat:" + request.getRequestURI() + ":" + getUserId();
String requestId = UUID.randomUUID().toString();
if (!lockService.tryLock(lockKey, requestId, repeatSubmit.interval())) {
throw new BusinessException("请勿重复提交");
}
try {
return point.proceed();
} finally {
lockService.unlock(lockKey, requestId);
}
}
}
五、场景四:登录失败计数
通过 Redis 的原子递增操作实现登录失败计数,配合过期时间实现自动解锁。
@Service
public class LoginSecurityService {
private static final String FAIL_COUNT_PREFIX = "login:fail:";
private static final String LOCK_PREFIX = "login:lock:";
private static final int MAX_FAIL_COUNT = 5;
private static final long LOCK_DURATION_MINUTES = 30;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 记录登录失败
* @param username 用户名
* @return 当前失败次数
*/
public int recordLoginFail(String username) {
String countKey = FAIL_COUNT_PREFIX + username;
// 原子递增失败次数
Long failCount = redisTemplate.opsForValue().increment(countKey);
// 首次失败时设置过期时间(15分钟窗口)
if (failCount != null && failCount == 1) {
redisTemplate.expire(countKey, 15, TimeUnit.MINUTES);
}
// 达到最大失败次数,锁定账号
if (failCount != null && failCount >= MAX_FAIL_COUNT) {
String lockKey = LOCK_PREFIX + username;
redisTemplate.opsForValue().set(lockKey, "locked",
LOCK_DURATION_MINUTES, TimeUnit.MINUTES);
}
return failCount != null ? failCount.intValue() : 1;
}
/**
* 检查账号是否被锁定
*/
public boolean isAccountLocked(String username) {
String lockKey = LOCK_PREFIX + username;
return Boolean.TRUE.equals(redisTemplate.hasKey(lockKey));
}
/**
* 登录成功,清除失败计数
*/
public void clearLoginFailCount(String username) {
redisTemplate.delete(FAIL_COUNT_PREFIX + username);
}
}
六、场景五:字典缓存
数据字典是系统中频繁访问的配置数据,适合使用 Redis Hash 结构缓存。
@Service
public class DictCacheService {
private static final String DICT_CACHE_PREFIX = "dict:";
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 根据字典类型获取字典项列表
* @param dictType 字典类型编码
* @return 字典项列表
*/
@SuppressWarnings("unchecked")
public List<DictItem> getDictItems(String dictType) {
String cacheKey = DICT_CACHE_PREFIX + dictType;
// 从Hash缓存中获取
Map<Object, Object> entries = redisTemplate.opsForHash().entries(cacheKey);
if (!entries.isEmpty()) {
return entries.values().stream()
.map(v -> (DictItem) v)
.toList();
}
// 缓存未命中,查询数据库
List<DictItem> items = dictMapper.selectItemsByType(dictType);
// 写入Hash缓存
Map<String, DictItem> itemMap = items.stream()
.collect(Collectors.toMap(DictItem::getValue, Function.identity()));
redisTemplate.opsForHash().putAll(cacheKey, itemMap);
redisTemplate.expire(cacheKey, 24, TimeUnit.HOURS);
return items;
}
/**
* 刷新字典缓存
* 在字典数据变更时调用
*/
public void refreshDictCache(String dictType) {
redisTemplate.delete(DICT_CACHE_PREFIX + dictType);
// 预热缓存
getDictItems(dictType);
}
}
七、场景六:向量存储
Redis 7.x+ 支持向量搜索功能,可用于 AI 知识库的语义检索(RAG)场景。
@Service
public class VectorSearchService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String VECTOR_INDEX_PREFIX = "vector:knowledge:";
/**
* 存储文档向量
* @param docId 文档ID
* @param content 文档内容
* @param embedding 向量数据
*/
public void storeEmbedding(String docId, String content, float[] embedding) {
String key = VECTOR_INDEX_PREFIX + docId;
Map<String, Object> fields = new HashMap<>();
fields.put("content", content);
fields.put("embedding", convertToBytes(embedding));
fields.put("timestamp", System.currentTimeMillis());
redisTemplate.opsForHash().putAll(key, fields);
}
/**
* 向量相似度搜索
* @param queryEmbedding 查询向量
* @param topK 返回最相似的K个结果
* @return 相似文档列表
*/
public List<SearchResult> searchSimilar(float[] queryEmbedding, int topK) {
// 使用Redis Search模块的向量搜索能力
// 实际项目中可结合RedisSearch客户端实现
String query = String.format(
"*=>[KNN %d @embedding $query_vec]",
topK
);
// 返回搜索结果
return executeVectorSearch(query, queryEmbedding);
}
/** float数组转字节数组 */
private byte[] convertToBytes(float[] embedding) {
ByteBuffer buffer = ByteBuffer.allocate(embedding.length * 4);
for (float f : embedding) {
buffer.putFloat(f);
}
return buffer.array();
}
}
八、缓存一致性策略
graph TD
A[数据变更] --> B{变更类型}
B -->|单条更新| C[删除对应缓存Key]
B -->|批量更新| D[删除相关缓存Key集合]
B -->|结构变更| E[清除整个缓存分组]
C --> F[下次请求触发缓存重建]
D --> F
E --> F
G[缓存异常] --> H{降级策略}
H -->|缓存穿透| I[缓存空值+短TTL]
H -->|缓存雪崩| J[随机TTL偏移]
H -->|缓存击穿| K[互斥锁重建]
style C fill:#c8e6c9
style D fill:#c8e6c9
style E fill:#fff9c4
style I fill:#e1f5fe
style J fill:#e1f5fe
style K fill:#e1f5fe
结论与建议
Redis Key 命名规范
| 场景 | Key格式 | TTL | 示例 |
|---|---|---|---|
| 会话 | session:{token} |
24h | session:abc123 |
| 权限缓存 | perm:user:{userId} |
2h | perm:user:1001 |
| 分布式锁 | lock:{业务}:{标识} |
业务决定 | lock:order:2001 |
| 登录计数 | login:fail:{username} |
15min | login:fail:admin |
| 字典缓存 | dict:{dictType} |
24h | dict:gender |
| 向量数据 | vector:knowledge:{docId} |
永久 | vector:knowledge:doc001 |
最佳实践建议
-
缓存穿透防护:对查询结果为空的Key缓存空值,设置短TTL(如5分钟),避免恶意请求穿透到数据库。
-
缓存雪崩防护:同类缓存的TTL添加随机偏移量(如2小时±10分钟),避免大量缓存同时过期。
-
缓存击穿防护:对热点Key使用互斥锁重建缓存,避免大量请求同时穿透到数据库。
-
序列化选择:内部系统推荐JSON序列化(可读性好),高性能场景推荐Protobuf或Kryo。
-
监控告警:监控Redis内存使用率、命中率、慢查询,设置合理的告警阈值。