feat: 实现完整的 RBAC 权限管理系统与基础设施增强

在初始认证基础上,新增完整的 RBAC 权限模型(角色、权限、菜单三级管理),
  集成审计日志、接口限流、登录失败锁定、Refresh Token 机制、Redis 分布式缓存与锁、
  RocketMQ 消息队列,并引入 Flyway 数据库版本管理,同时补充项目文档与使用示例
This commit is contained in:
2026-04-10 10:58:22 +08:00
parent 3a9bf61839
commit 40c85c3c1f
97 changed files with 13434 additions and 351 deletions

View File

@@ -8,37 +8,167 @@ import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import java.util.UUID;
/**
* JWT 工具类
* 用于生成和验证 JWT Token
*
* 主要功能:
* 1. Token 生成:生成包含用户信息的 JWT Token
* 2. Token 解析:解析 Token 并提取信息
* 3. Token 验证:验证 Token 签名和有效期
*
* Token 结构:
* - Header算法类型和 Token 类型
* - Payload用户信息、签发时间、过期时间、jtiJWT ID
* - Signature使用密钥签名的签名部分
*
* @author Claude
* @since 2024-04-09
*/
@Component
public class JwtUtil {
/**
* JWT 密钥
* 从配置文件读取
* HS512 算法要求密钥至少 64 字符512 位)
*/
@Value("${jwt.secret}")
private String secret;
private final long expiration = 1000 * 60 * 60 *24; // 24小时
/**
* Access Token 过期时间(秒)
* 从配置文件读取,默认 3600 秒1 小时)
*/
@Value("${jwt.access-token-expiration:3600}") // Default 1 hour
private long accessTokenExpirationSeconds;
/**
* 获取签名密钥
* 说明:
* - HS512 算法要求密钥至少 512 位64 字节)
* - 如果密钥不足 64 字符,抛出异常
*
* @return 签名密钥
*/
private Key getSigningKey() {
// HS512 要求密钥至少 512 位 = 64 字节
if (secret.length() < 64) {
throw new IllegalArgumentException("JWT secret must be at least 64 characters long for HS512");
throw new IllegalArgumentException("JWT 密钥必须至少 64 个字符HS512 算法要求)");
}
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public String generateToken(Long userId,String username) {
/**
* 为用户生成 Access Token
* 步骤:
* 1. 生成唯一的 jtiJWT ID
* 2. 设置用户信息用户名、用户ID
* 3. 设置签发时间和过期时间
* 4. 使用密钥签名
*
* @param userId 用户ID
* @param username 用户名
* @return JWT Token
*/
public String generateToken(Long userId, String username) {
return generateToken(userId, username, accessTokenExpirationSeconds);
}
/**
* 生成自定义过期时间的 Token
* 步骤:
* 1. 生成唯一的 jtiJWT ID用于黑名单追踪
* 2. 计算过期时间(毫秒)
* 3. 构建 Token
*
* @param userId 用户ID
* @param username 用户名
* @param expirationSeconds 过期时间(秒)
* @return JWT Token
*/
public String generateToken(Long userId, String username, long expirationSeconds) {
// 1. 生成唯一的 jtiJWT ID
String jti = UUID.randomUUID().toString();
long expirationMillis = expirationSeconds * 1000;
// 2. 构建 Token
return Jwts.builder()
.setSubject(username)
.claim("id", userId)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey(), SignatureAlgorithm.HS512)
.setSubject(username) // 主题:用户名
.claim("id", userId) // 自定义声明用户ID
.claim("jti", jti) // 自定义声明JWT ID用于黑名单
.setIssuedAt(new Date()) // 签发时间:当前时间
.setExpiration(new Date(System.currentTimeMillis() + expirationMillis)) // 过期时间
.signWith(getSigningKey(), SignatureAlgorithm.HS512) // 签名算法和密钥
.compact();
}
/**
* 从 Token 中提取用户名
*
* @param token JWT Token
* @return 用户名
*/
public String extractUsername(String token) {
return parseClaims(token).getBody().getSubject();
}
/**
* 从 Token 中提取用户ID
*
* @param token JWT Token
* @return 用户ID
*/
public Long extractUserId(String token) {
Claims claims = parseClaims(token).getBody();
return claims.get("id", Long.class);
}
/**
* 从 Token 中提取 JWT IDjti
* 说明:
* - jti 是 JWT 的唯一标识
* - 用于将 Token 加入黑名单(登出功能)
*
* @param token JWT Token
* @return JWT ID
*/
public String extractJti(String token) {
Claims claims = parseClaims(token).getBody();
return claims.get("jti", String.class);
}
/**
* 从 Token 中提取过期时间
*
* @param token JWT Token
* @return 过期时间
*/
public Date extractExpiration(String token) {
return parseClaims(token).getBody().getExpiration();
}
/**
* 获取 Access Token 的过期时间(秒)
*
* @return 过期时间(秒)
*/
public long getExpirationSeconds() {
return accessTokenExpirationSeconds;
}
/**
* 验证 Token 签名和结构
* 说明:
* - 验证签名是否正确
* - 验证 Token 结构是否完整
* - 不验证过期时间
*
* @param token JWT Token
* @return true 表示 Token 有效
*/
public boolean validateToken(String token) {
try {
parseClaims(token);
@@ -48,6 +178,31 @@ public class JwtUtil {
}
}
/**
* 检查 Token 是否过期
*
* @param token JWT Token
* @return true 表示 Token 已过期
*/
public boolean isTokenExpired(String token) {
try {
Date expiration = extractExpiration(token);
return expiration.before(new Date());
} catch (JwtException e) {
return true;
}
}
/**
* 解析并验证 JWT Token
* 说明:
* - 验证签名
* - 解析 Claims
*
* @param token JWT Token
* @return 解析后的 JWT 对象(包含 Claims
* @throws JwtException 如果 Token 无效
*/
private Jws<Claims> parseClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())

View File

@@ -0,0 +1,762 @@
package com.aisi.template.utils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Redis 缓存工具类
* 提供各种场景下的缓存操作方法
*
* 使用场景:
* 1. 对象缓存:缓存实体对象,减少数据库查询
* 2. 列表缓存:缓存列表数据
* 3. 集合缓存:缓存去重数据
* 4. 哈希缓存:缓存对象字段
* 5. 计数器:文章阅读数、点赞数等
*
* @author Claude
* @since 2024-04-09
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RedisCache {
/**
* Redis 模板
*/
private final RedisTemplate<String, Object> redisTemplate;
// ==================== 1. String 类型操作 ====================
/**
* 设置缓存(永不过期)
* 注意:生产环境建议设置过期时间,防止内存溢出
*
* @param key 键
* @param value 值
*/
public void set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
log.debug("设置缓存成功 - key: {}", key);
} catch (Exception e) {
log.error("设置缓存失败 - key: {}", key, e);
}
}
/**
* 设置缓存(指定过期时间)
* 步骤:
* 1. 将值序列化后存入 Redis
* 2. 设置过期时间
*
* @param key 键
* @param value 值
* @param timeout 过期时间
* @param timeUnit 时间单位
*/
public void set(String key, Object value, long timeout, TimeUnit timeUnit) {
try {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
log.debug("设置缓存成功 - key: {}, timeout: {} {}", key, timeout, timeUnit);
} catch (Exception e) {
log.error("设置缓存失败 - key: {}", key, e);
}
}
/**
* 设置缓存(使用 Duration
*
* @param key 键
* @param value 值
* @param duration 过期时间
*/
public void set(String key, Object value, Duration duration) {
try {
redisTemplate.opsForValue().set(key, value, duration);
log.debug("设置缓存成功 - key: {}, duration: {}", key, duration);
} catch (Exception e) {
log.error("设置缓存失败 - key: {}", key, e);
}
}
/**
* 获取缓存
* 步骤:
* 1. 从 Redis 获取值
* 2. 反序列化为对象
*
* @param key 键
* @return 值,不存在返回 null
*/
public Object get(String key) {
try {
Object value = redisTemplate.opsForValue().get(key);
log.debug("获取缓存 - key: {}, found: {}", key, value != null);
return value;
} catch (Exception e) {
log.error("获取缓存失败 - key: {}", key, e);
return null;
}
}
/**
* 获取缓存(指定类型)
*
* @param key 键
* @param type 返回值类型
* @return 值
*/
@SuppressWarnings("unchecked")
public <T> T get(String key, Class<T> type) {
try {
Object value = redisTemplate.opsForValue().get(key);
if (value != null && type.isInstance(value)) {
return (T) value;
}
return null;
} catch (Exception e) {
log.error("获取缓存失败 - key: {}", key, e);
return null;
}
}
/**
* 删除缓存
*
* @param key 键
* @return 是否删除成功
*/
public boolean delete(String key) {
try {
Boolean result = redisTemplate.delete(key);
log.debug("删除缓存 - key: {}, result: {}", key, result);
return Boolean.TRUE.equals(result);
} catch (Exception e) {
log.error("删除缓存失败 - key: {}", key, e);
return false;
}
}
/**
* 批量删除缓存
* 步骤:
* 1. 遍历所有键
* 2. 一次性删除
*
* @param keys 键集合
* @return 删除的数量
*/
public long delete(Collection<String> keys) {
try {
Long count = redisTemplate.delete(keys);
log.debug("批量删除缓存 - count: {}", count);
return count != null ? count : 0;
} catch (Exception e) {
log.error("批量删除缓存失败", e);
return 0;
}
}
/**
* 判断键是否存在
*
* @param key 键
* @return 是否存在
*/
public boolean hasKey(String key) {
try {
Boolean result = redisTemplate.hasKey(key);
return Boolean.TRUE.equals(result);
} catch (Exception e) {
log.error("判断键是否存在失败 - key: {}", key, e);
return false;
}
}
/**
* 设置过期时间
*
* @param key 键
* @param timeout 过期时间
* @param unit 时间单位
* @return 是否设置成功
*/
public boolean expire(String key, long timeout, TimeUnit unit) {
try {
Boolean result = redisTemplate.expire(key, timeout, unit);
return Boolean.TRUE.equals(result);
} catch (Exception e) {
log.error("设置过期时间失败 - key: {}", key, e);
return false;
}
}
/**
* 获取过期时间
*
* @param key 键
* @param unit 时间单位
* @return 剩余过期时间,-1 表示永不过期,-2 表示键不存在
*/
public long getExpire(String key, TimeUnit unit) {
try {
Long expire = redisTemplate.getExpire(key, unit);
return expire != null ? expire : -2;
} catch (Exception e) {
log.error("获取过期时间失败 - key: {}", key, e);
return -2;
}
}
/**
* 自增操作(原子性)
* 使用场景:计数器、点赞数、阅读数等
* 步骤:
* 1. 原子性自增
* 2. 返回自增后的值
*
* @param key 键
* @param delta 增量
* @return 自增后的值
*/
public long increment(String key, long delta) {
try {
Long result = redisTemplate.opsForValue().increment(key, delta);
log.debug("自增操作 - key: {}, delta: {}, result: {}", key, delta, result);
return result != null ? result : 0;
} catch (Exception e) {
log.error("自增操作失败 - key: {}", key, e);
return 0;
}
}
/**
* 自减操作(原子性)
*
* @param key 键
* @param delta 减量
* @return 自减后的值
*/
public long decrement(String key, long delta) {
try {
Long result = redisTemplate.opsForValue().decrement(key, delta);
log.debug("自减操作 - key: {}, delta: {}, result: {}", key, delta, result);
return result != null ? result : 0;
} catch (Exception e) {
log.error("自减操作失败 - key: {}", key, e);
return 0;
}
}
// ==================== 2. Hash 类型操作 ====================
/**
* 设置哈希字段
* 使用场景:缓存对象的单个字段
* 步骤:
* 1. 将字段和值存入哈希表
* 2. 适合部分更新对象字段
*
* @param key 键
* @param field 字段
* @param value 值
*/
public void hSet(String key, String field, Object value) {
try {
redisTemplate.opsForHash().put(key, field, value);
log.debug("设置哈希字段 - key: {}, field: {}", key, field);
} catch (Exception e) {
log.error("设置哈希字段失败 - key: {}, field: {}", key, field, e);
}
}
/**
* 获取哈希字段
*
* @param key 键
* @param field 字段
* @return 值
*/
public Object hGet(String key, String field) {
try {
return redisTemplate.opsForHash().get(key, field);
} catch (Exception e) {
log.error("获取哈希字段失败 - key: {}, field: {}", key, field, e);
return null;
}
}
/**
* 批量设置哈希字段
*
* @param key 键
* @param map 字段-值映射
*/
public void hSetAll(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
log.debug("批量设置哈希字段 - key: {}", key);
} catch (Exception e) {
log.error("批量设置哈希字段失败 - key: {}", key, e);
}
}
/**
* 获取所有哈希字段
*
* @param key 键
* @return 字段-值映射
*/
public Map<Object, Object> hGetAll(String key) {
try {
return redisTemplate.opsForHash().entries(key);
} catch (Exception e) {
log.error("获取所有哈希字段失败 - key: {}", key, e);
return Map.of();
}
}
/**
* 删除哈希字段
*
* @param key 键
* @param fields 字段集合
* @return 删除的数量
*/
public long hDelete(String key, Object... fields) {
try {
return redisTemplate.opsForHash().delete(key, fields);
} catch (Exception e) {
log.error("删除哈希字段失败 - key: {}", key, e);
return 0;
}
}
/**
* 判断哈希字段是否存在
*
* @param key 键
* @param field 字段
* @return 是否存在
*/
public boolean hExists(String key, String field) {
try {
return redisTemplate.opsForHash().hasKey(key, field);
} catch (Exception e) {
log.error("判断哈希字段是否存在失败 - key: {}, field: {}", key, field, e);
return false;
}
}
/**
* 哈希字段自增
*
* @param key 键
* @param field 字段
* @param delta 增量
* @return 自增后的值
*/
public long hIncrement(String key, String field, long delta) {
try {
return redisTemplate.opsForHash().increment(key, field, delta);
} catch (Exception e) {
log.error("哈希字段自增失败 - key: {}, field: {}", key, field, e);
return 0;
}
}
// ==================== 3. List 类型操作 ====================
/**
* 从左侧推入列表
* 使用场景:消息队列、最新消息列表
* 步骤:
* 1. 将元素推入列表左侧
* 2. 返回当前列表长度
*
* @param key 键
* @param value 值
* @return 推入后的列表长度
*/
public long lLeftPush(String key, Object value) {
try {
Long size = redisTemplate.opsForList().leftPush(key, value);
log.debug("从左侧推入列表 - key: {}, size: {}", key, size);
return size != null ? size : 0;
} catch (Exception e) {
log.error("从左侧推入列表失败 - key: {}", key, e);
return 0;
}
}
/**
* 从右侧推入列表
*
* @param key 键
* @param value 值
* @return 推入后的列表长度
*/
public long lRightPush(String key, Object value) {
try {
Long size = redisTemplate.opsForList().rightPush(key, value);
log.debug("从右侧推入列表 - key: {}, size: {}", key, size);
return size != null ? size : 0;
} catch (Exception e) {
log.error("从右侧推入列表失败 - key: {}", key, e);
return 0;
}
}
/**
* 从左侧弹出列表元素
*
* @param key 键
* @return 弹出的元素
*/
public Object lLeftPop(String key) {
try {
return redisTemplate.opsForList().leftPop(key);
} catch (Exception e) {
log.error("从左侧弹出列表元素失败 - key: {}", key, e);
return null;
}
}
/**
* 从右侧弹出列表元素
*
* @param key 键
* @return 弹出的元素
*/
public Object lRightPop(String key) {
try {
return redisTemplate.opsForList().rightPop(key);
} catch (Exception e) {
log.error("从右侧弹出列表元素失败 - key: {}", key, e);
return null;
}
}
/**
* 获取列表范围
* 使用场景:分页查询列表数据
* 步骤:
* 1. 获取指定范围的元素
* 2. 支持负索引(-1 表示最后一个元素)
*
* @param key 键
* @param start 开始索引
* @param end 结束索引
* @return 元素列表
*/
public List<Object> lRange(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
log.error("获取列表范围失败 - key: {}", key, e);
return List.of();
}
}
/**
* 获取列表长度
*
* @param key 键
* @return 列表长度
*/
public long lSize(String key) {
try {
Long size = redisTemplate.opsForList().size(key);
return size != null ? size : 0;
} catch (Exception e) {
log.error("获取列表长度失败 - key: {}", key, e);
return 0;
}
}
/**
* 移除列表元素
*
* @param key 键
* @param count 移除数量(>0 从左往右,<0 从右往左,=0 全部)
* @param value 要移除的值
* @return 实际移除的数量
*/
public long lRemove(String key, long count, Object value) {
try {
Long removed = redisTemplate.opsForList().remove(key, count, value);
return removed != null ? removed : 0;
} catch (Exception e) {
log.error("移除列表元素失败 - key: {}", key, e);
return 0;
}
}
// ==================== 4. Set 类型操作 ====================
/**
* 添加到集合
* 使用场景:标签系统、共同好友、去重
* 步骤:
* 1. 添加元素到集合
* 2. 自动去重
*
* @param key 键
* @param values 值集合
* @return 添加的元素数量(不包含已存在的)
*/
public long sAdd(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
log.debug("添加到集合 - key: {}, count: {}", key, count);
return count != null ? count : 0;
} catch (Exception e) {
log.error("添加到集合失败 - key: {}", key, e);
return 0;
}
}
/**
* 获取集合所有元素
*
* @param key 键
* @return 元素集合
*/
public Set<Object> sMembers(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
log.error("获取集合元素失败 - key: {}", key, e);
return Set.of();
}
}
/**
* 判断元素是否在集合中
*
* @param key 键
* @param value 值
* @return 是否存在
*/
public boolean sIsMember(String key, Object value) {
try {
Boolean result = redisTemplate.opsForSet().isMember(key, value);
return Boolean.TRUE.equals(result);
} catch (Exception e) {
log.error("判断元素是否在集合中失败 - key: {}", key, e);
return false;
}
}
/**
* 获取集合大小
*
* @param key 键
* @return 集合大小
*/
public long sSize(String key) {
try {
Long size = redisTemplate.opsForSet().size(key);
return size != null ? size : 0;
} catch (Exception e) {
log.error("获取集合大小失败 - key: {}", key, e);
return 0;
}
}
/**
* 移除集合元素
*
* @param key 键
* @param values 要移除的值
* @return 实际移除的数量
*/
public long sRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count != null ? count : 0;
} catch (Exception e) {
log.error("移除集合元素失败 - key: {}", key, e);
return 0;
}
}
/**
* 集合交集
* 使用场景:共同好友、共同标签
* 步骤:
* 1. 获取多个集合的交集
* 2. 返回共有的元素
*
* @param key1 第一个集合键
* @param key2 第二个集合键
* @return 交集元素
*/
public Set<Object> sIntersect(String key1, String key2) {
try {
return redisTemplate.opsForSet().intersect(key1, key2);
} catch (Exception e) {
log.error("获取集合交集失败 - key1: {}, key2: {}", key1, key2, e);
return Set.of();
}
}
/**
* 集合并集
*
* @param key1 第一个集合键
* @param key2 第二个集合键
* @return 并集元素
*/
public Set<Object> sUnion(String key1, String key2) {
try {
return redisTemplate.opsForSet().union(key1, key2);
} catch (Exception e) {
log.error("获取集合并集失败 - key1: {}, key2: {}", key1, key2, e);
return Set.of();
}
}
// ==================== 5. ZSet 类型操作(有序集合)====================
/**
* 添加到有序集合
* 使用场景:排行榜、权重队列
* 步骤:
* 1. 添加元素及分数
* 2. 按分数自动排序
*
* @param key 键
* @param value 值
* @param score 分数
* @return 添加是否成功(新增返回 true更新返回 false
*/
public boolean zAdd(String key, Object value, double score) {
try {
Boolean result = redisTemplate.opsForZSet().add(key, value, score);
log.debug("添加到有序集合 - key: {}, value: {}, score: {}", key, value, score);
return Boolean.TRUE.equals(result);
} catch (Exception e) {
log.error("添加到有序集合失败 - key: {}", key, e);
return false;
}
}
/**
* 获取有序集合范围(按分数)
* 使用场景:排行榜分页查询
* 步骤:
* 1. 获取指定分数范围的元素
* 2. 按分数升序排列
*
* @param key 键
* @param min 最小分数
* @param max 最大分数
* @return 元素集合
*/
public Set<Object> zRangeByScore(String key, double min, double max) {
try {
return redisTemplate.opsForZSet().rangeByScore(key, min, max);
} catch (Exception e) {
log.error("获取有序集合范围失败 - key: {}", key, e);
return Set.of();
}
}
/**
* 获取有序集合排名(倒序)
* 使用场景:排行榜查询(分数高的排名靠前)
* 步骤:
* 1. 获取元素的排名从0开始
* 2. 按分数倒序排列
*
* @param key 键
* @param value 值
* @return 排名,不存在返回 null
*/
public Long zReverseRank(String key, Object value) {
try {
return redisTemplate.opsForZSet().reverseRank(key, value);
} catch (Exception e) {
log.error("获取有序集合排名失败 - key: {}", key, e);
return null;
}
}
/**
* 获取元素分数
*
* @param key 键
* @param value 值
* @return 分数
*/
public Double zScore(String key, Object value) {
try {
return redisTemplate.opsForZSet().score(key, value);
} catch (Exception e) {
log.error("获取元素分数失败 - key: {}", key, e);
return null;
}
}
/**
* 增加元素分数
*
* @param key 键
* @param value 值
* @param delta 增量
* @return 增加后的分数
*/
public Double zIncrementScore(String key, Object value, double delta) {
try {
return redisTemplate.opsForZSet().incrementScore(key, value, delta);
} catch (Exception e) {
log.error("增加元素分数失败 - key: {}", key, e);
return null;
}
}
/**
* 移除有序集合元素
*
* @param key 键
* @param values 要移除的值
* @return 实际移除的数量
*/
public long zRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForZSet().remove(key, values);
return count != null ? count : 0;
} catch (Exception e) {
log.error("移除有序集合元素失败 - key: {}", key, e);
return 0;
}
}
/**
* 获取有序集合大小
*
* @param key 键
* @return 集合大小
*/
public long zSize(String key) {
try {
Long size = redisTemplate.opsForZSet().size(key);
return size != null ? size : 0;
} catch (Exception e) {
log.error("获取有序集合大小失败 - key: {}", key, e);
return 0;
}
}
}

View File

@@ -0,0 +1,301 @@
package com.aisi.template.utils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Redis 分布式锁工具类
* 基于 Redis SETNX + EXPIRE 实现,支持可重入锁
*
* 使用场景:
* 1. 防止重复提交(如表单重复提交)
* 2. 库存扣减(防止超卖)
* 3. 定时任务分布式执行(防止重复执行)
* 4. 限流场景
*
* @author Claude
* @since 2024-04-09
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RedisLock {
/**
* Redis 模板
*/
private final RedisTemplate<String, Object> redisTemplate;
/**
* 锁前缀
*/
private static final String LOCK_PREFIX = "lock:";
/**
* 锁的值前缀
*/
private static final String LOCK_VALUE_PREFIX = "uuid:";
/**
* 默认锁过期时间(秒)
*/
private static final long DEFAULT_EXPIRE_TIME = 30;
/**
* 获取锁的 Lua 脚本
* SETNX + EXPIRE 原子操作
*/
private static final String LOCK_SCRIPT =
"if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " +
"redis.call('expire', KEYS[1], ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
/**
* 释放锁的 Lua 脚本
* 确保只有锁的持有者才能释放锁
*/
private static final String UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
/**
* 尝试获取锁
* 步骤:
* 1. 生成唯一的锁标识UUID
* 2. 执行 Lua 脚本尝试获取锁
* 3. 如果获取成功,返回锁标识;否则返回 null
*
* @param lockKey 锁的键名
* @return 锁标识,如果获取失败返回 null
*/
public String tryLock(String lockKey) {
return tryLock(lockKey, DEFAULT_EXPIRE_TIME);
}
/**
* 尝试获取锁(指定过期时间)
* 步骤:
* 1. 生成唯一的锁标识UUID
* 2. 执行 Lua 脚本尝试获取锁,并设置过期时间
* 3. 如果获取成功,返回锁标识;否则返回 null
*
* @param lockKey 锁的键名
* @param expireTime 锁的过期时间(秒)
* @return 锁标识,如果获取失败返回 null
*/
public String tryLock(String lockKey, long expireTime) {
// 1. 生成唯一的锁标识
String lockValue = LOCK_VALUE_PREFIX + UUID.randomUUID().toString();
String fullLockKey = LOCK_PREFIX + lockKey;
try {
// 2. 执行 Lua 脚本获取锁
// KEYS[1]: 锁的完整键名
// ARGV[1]: 锁的值UUID
// ARGV[2]: 过期时间(秒)
Long result = redisTemplate.execute(
RedisScript.of(LOCK_SCRIPT, Long.class),
Collections.singletonList(fullLockKey),
lockValue,
String.valueOf(expireTime)
);
// 3. 判断是否获取成功
if (result != null && result == 1) {
log.info("获取锁成功 - lockKey: {}, lockValue: {}", lockKey, lockValue);
return lockValue;
} else {
log.warn("获取锁失败 - lockKey: {}", lockKey);
return null;
}
} catch (Exception e) {
log.error("获取锁异常 - lockKey: {}", lockKey, e);
return null;
}
}
/**
* 尝试获取锁(带等待时间)
* 步骤:
* 1. 在等待时间内循环尝试获取锁
* 2. 每次间隔 100 毫秒重试
* 3. 超过等待时间则放弃
*
* @param lockKey 锁的键名
* @param expireTime 锁的过期时间(秒)
* @param waitTime 等待时间(毫秒)
* @return 锁标识,如果获取失败返回 null
*/
public String tryLock(String lockKey, long expireTime, long waitTime) {
long startTime = System.currentTimeMillis();
long timeout = startTime + waitTime;
String lockValue = null;
// 1. 在等待时间内循环尝试
while (System.currentTimeMillis() < timeout) {
// 2. 尝试获取锁
lockValue = tryLock(lockKey, expireTime);
if (lockValue != null) {
return lockValue;
}
// 3. 短暂休眠后重试
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
log.warn("获取锁超时 - lockKey: {}, waitTime: {}ms", lockKey, waitTime);
return null;
}
/**
* 释放锁
* 步骤:
* 1. 验证锁的值是否匹配(确保只有持有者才能释放)
* 2. 如果匹配则删除锁,否则不做操作
*
* @param lockKey 锁的键名
* @param lockValue 锁的标识tryLock 返回的值)
* @return 是否释放成功
*/
public boolean unlock(String lockKey, String lockValue) {
if (lockValue == null) {
return false;
}
String fullLockKey = LOCK_PREFIX + lockKey;
try {
// 1. 执行 Lua 脚本释放锁
// KEYS[1]: 锁的完整键名
// ARGV[1]: 锁的值(用于验证)
Long result = redisTemplate.execute(
RedisScript.of(UNLOCK_SCRIPT, Long.class),
Collections.singletonList(fullLockKey),
lockValue
);
// 2. 判断是否释放成功
if (result != null && result == 1) {
log.info("释放锁成功 - lockKey: {}, lockValue: {}", lockKey, lockValue);
return true;
} else {
log.warn("释放锁失败,锁不存在或已过期 - lockKey: {}, lockValue: {}", lockKey, lockValue);
return false;
}
} catch (Exception e) {
log.error("释放锁异常 - lockKey: {}, lockValue: {}", lockKey, lockValue, e);
return false;
}
}
/**
* 强制释放锁(不验证持有者)
* 注意:这个方法会强制删除锁,可能导致其他持有者的锁被误删
* 仅在特殊场景使用,如清理死锁
*
* @param lockKey 锁的键名
* @return 是否删除成功
*/
public boolean forceUnlock(String lockKey) {
String fullLockKey = LOCK_PREFIX + lockKey;
try {
Boolean result = redisTemplate.delete(fullLockKey);
log.warn("强制释放锁 - lockKey: {}, result: {}", lockKey, result);
return Boolean.TRUE.equals(result);
} catch (Exception e) {
log.error("强制释放锁异常 - lockKey: {}", lockKey, e);
return false;
}
}
/**
* 检查锁是否存在
*
* @param lockKey 锁的键名
* @return 是否存在
*/
public boolean isLocked(String lockKey) {
String fullLockKey = LOCK_PREFIX + lockKey;
try {
Boolean result = redisTemplate.hasKey(fullLockKey);
return Boolean.TRUE.equals(result);
} catch (Exception e) {
log.error("检查锁状态异常 - lockKey: {}", lockKey, e);
return false;
}
}
/**
* 获取锁的剩余过期时间
*
* @param lockKey 锁的键名
* @return 剩余过期时间(秒),-1 表示不存在或已过期
*/
public long getLockExpireTime(String lockKey) {
String fullLockKey = LOCK_PREFIX + lockKey;
try {
Long expireTime = redisTemplate.getExpire(fullLockKey, TimeUnit.SECONDS);
return expireTime != null ? expireTime : -1;
} catch (Exception e) {
log.error("获取锁过期时间异常 - lockKey: {}", lockKey, e);
return -1;
}
}
/**
* 续期锁(延长锁的过期时间)
* 注意:需要验证持有者
*
* @param lockKey 锁的键名
* @param lockValue 锁的标识
* @param addTime 增加的时间(秒)
* @return 是否续期成功
*/
public boolean renewLock(String lockKey, String lockValue, long addTime) {
if (lockValue == null) {
return false;
}
String fullLockKey = LOCK_PREFIX + lockKey;
try {
// 1. 先验证锁的值
String currentValue = (String) redisTemplate.opsForValue().get(fullLockKey);
if (!lockValue.equals(currentValue)) {
log.warn("续期失败,锁的值不匹配 - lockKey: {}", lockKey);
return false;
}
// 2. 续期锁
Boolean result = redisTemplate.expire(fullLockKey, addTime, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(result)) {
log.info("续期锁成功 - lockKey: {}, addTime: {}s", lockKey, addTime);
return true;
} else {
log.warn("续期锁失败 - lockKey: {}", lockKey);
return false;
}
} catch (Exception e) {
log.error("续期锁异常 - lockKey: {}", lockKey, e);
return false;
}
}
}

View File

@@ -1,6 +1,7 @@
package com.aisi.template.utils;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
@@ -11,69 +12,187 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Redis 工具类
* 提供各种 Redis 数据类型的便捷操作方法
*
* 主要功能:
* 1. String 类型:字符串、对象缓存
* 2. Hash 类型:对象字段缓存
* 3. List 类型:队列、列表
* 4. Set 类型:去重、交集、并集
* 5. ZSet 类型:排行榜
* 6. Bitmap位图操作
* 7. HyperLogLog基数统计
*
* @author Claude
* @since 2024-04-09
*/
@Component
@RequiredArgsConstructor
public class RedisUtils {
/**
* Redis 模板
*/
private final RedisTemplate<String, Object> redisTemplate;
// ======================== 通用 ========================
/**
* 字符串 Redis 模板,用于数值自增等原生命令
*/
private final StringRedisTemplate stringRedisTemplate;
// ======================== 1. 通用操作 ========================
/**
* 删除键
*
* @param key 键
* @return 是否删除成功
*/
public Boolean delete(String key) {
return redisTemplate.delete(key);
}
/**
* 批量删除键
*
* @param keys 键集合
* @return 删除的数量
*/
public Long delete(Collection<String> keys) {
return redisTemplate.delete(keys);
}
/**
* 判断键是否存在
*
* @param key 键
* @return 是否存在
*/
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
/**
* 设置过期时间
*
* @param key 键
* @param timeout 过期时间
* @param unit 时间单位
* @return 是否设置成功
*/
public Boolean expire(String key, long timeout, TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获取过期时间
*
* @param key 键
* @return 剩余过期时间(秒),-1 表示永不过期,-2 表示键不存在
*/
public Long getExpire(String key) {
return redisTemplate.getExpire(key);
}
/**
* 在指定时间戳过期
*
* @param key 键
* @param timestamp 时间戳(毫秒)
* @return 是否设置成功
*/
public Boolean expireAt(String key, long timestamp) {
return redisTemplate.expireAt(key, new java.util.Date(timestamp));
}
/**
* 查找匹配模式的键
*
* @param pattern 模式user:*
* @return 匹配的键集合
*/
public Set<String> keys(String pattern) {
return redisTemplate.keys(pattern);
}
/**
* 重命名键
*
* @param oldKey 旧键名
* @param newKey 新键名
* @return 是否重命名成功
*/
public Boolean rename(String oldKey, String newKey) {
redisTemplate.rename(oldKey, newKey);
return true;
}
/**
* 获取键的剩余生存时间(秒)
*
* @param key 键
* @return 剩余秒数
*/
public Long ttl(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
// ======================== String ========================
// ======================== 2. String 类型操作 ========================
/**
* 设置键值
*
* @param key 键
* @param value 值
*/
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 设置键值(带过期时间)
*
* @param key 键
* @param value 值
* @param timeout 过期时间
* @param unit 时间单位
*/
public void set(String key, Object value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
/**
* 如果键不存在则设置(原子操作)
* 使用场景:分布式锁
*
* @param key 键
* @param value 值
* @param timeout 过期时间
* @param unit 时间单位
*/
public void setIfAbsent(String key, Object value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
}
/**
* 获取键值
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 获取键值(指定类型)
*
* @param key 键
* @param clazz 类型
* @return 值
*/
public <T> T get(String key, Class<T> clazz) {
Object value = redisTemplate.opsForValue().get(key);
if (value == null) {
@@ -82,255 +201,680 @@ public class RedisUtils {
return clazz.cast(value);
}
/**
* 获取并设置新值
*
* @param key 键
* @param value 新值
* @return 旧值
*/
public String getAndSet(String key, Object value) {
Object old = redisTemplate.opsForValue().getAndSet(key, value);
return old != null ? old.toString() : null;
}
/**
* 自增(原子操作)
*
* @param key 键
* @return 自增后的值
*/
public Long increment(String key) {
return redisTemplate.opsForValue().increment(key);
return stringRedisTemplate.opsForValue().increment(key);
}
/**
* 自增指定增量(原子操作)
*
* @param key 键
* @param delta 增量
* @return 自增后的值
*/
public Long increment(String key, long delta) {
return redisTemplate.opsForValue().increment(key, delta);
return stringRedisTemplate.opsForValue().increment(key, delta);
}
/**
* 自减(原子操作)
*
* @param key 键
* @return 自减后的值
*/
public Long decrement(String key) {
return redisTemplate.opsForValue().decrement(key);
return stringRedisTemplate.opsForValue().decrement(key);
}
/**
* 自减指定减量(原子操作)
*
* @param key 键
* @param delta 减量
* @return 自减后的值
*/
public Long decrement(String key, long delta) {
return redisTemplate.opsForValue().decrement(key, delta);
return stringRedisTemplate.opsForValue().decrement(key, delta);
}
/**
* 获取字符串长度
*
* @param key 键
* @return 字符串长度
*/
public Long strLength(String key) {
return redisTemplate.opsForValue().size(key);
}
// ======================== Hash ========================
// ======================== 3. Hash 类型操作 ========================
/**
* 设置哈希字段
*
* @param key 键
* @param hashKey 哈希键
* @param value 值
*/
public void hSet(String key, String hashKey, Object value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
/**
* 批量设置哈希字段
*
* @param key 键
* @param map 哈希键值映射
*/
public void hSetAll(String key, Map<String, Object> map) {
redisTemplate.opsForHash().putAll(key, map);
}
/**
* 获取哈希字段
*
* @param key 键
* @param hashKey 哈希键
* @return 值
*/
public Object hGet(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
/**
* 获取所有哈希字段
*
* @param key 键
* @return 哈希键值映射
*/
public Map<Object, Object> hGetAll(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 删除哈希字段
*
* @param key 键
* @param hashKeys 哈希键集合
*/
public void hDelete(String key, Object... hashKeys) {
redisTemplate.opsForHash().delete(key, hashKeys);
}
/**
* 判断哈希字段是否存在
*
* @param key 键
* @param hashKey 哈希键
* @return 是否存在
*/
public Boolean hHasKey(String key, String hashKey) {
return redisTemplate.opsForHash().hasKey(key, hashKey);
}
/**
* 获取哈希大小
*
* @param key 键
* @return 哈希大小
*/
public Long hSize(String key) {
return redisTemplate.opsForHash().size(key);
}
/**
* 哈希字段自增
*
* @param key 键
* @param hashKey 哈希键
* @param delta 增量
* @return 自增后的值
*/
public Long hIncrement(String key, String hashKey, long delta) {
return redisTemplate.opsForHash().increment(key, hashKey, delta);
}
/**
* 获取所有哈希键
*
* @param key 键
* @return 哈希键集合
*/
public Set<Object> hKeys(String key) {
return redisTemplate.opsForHash().keys(key);
}
/**
* 获取所有哈希值
*
* @param key 键
* @return 哈希值列表
*/
public List<Object> hValues(String key) {
return redisTemplate.opsForHash().values(key);
}
// ======================== List ========================
// ======================== 4. List 类型操作 ========================
/**
* 从左侧推入列表
*
* @param key 键
* @param value 值
* @return 列表长度
*/
public Long lPush(String key, Object value) {
return redisTemplate.opsForList().leftPush(key, value);
}
/**
* 从左侧批量推入列表
*
* @param key 键
* @param values 值数组
* @return 列表长度
*/
public Long lPushAll(String key, Object... values) {
return redisTemplate.opsForList().leftPushAll(key, values);
}
/**
* 从右侧推入列表
*
* @param key 键
* @param value 值
* @return 列表长度
*/
public Long rPush(String key, Object value) {
return redisTemplate.opsForList().rightPush(key, value);
}
/**
* 从右侧批量推入列表
*
* @param key 键
* @param values 值数组
* @return 列表长度
*/
public Long rPushAll(String key, Object... values) {
return redisTemplate.opsForList().rightPushAll(key, values);
}
/**
* 从左侧弹出元素
*
* @param key 键
* @return 弹出的元素
*/
public Object lPop(String key) {
return redisTemplate.opsForList().leftPop(key);
}
/**
* 从左侧批量弹出元素
*
* @param key 键
* @param count 数量
* @return 弹出的元素列表
*/
public List<Object> lPop(String key, long count) {
return redisTemplate.opsForList().leftPop(key, count);
}
/**
* 从右侧弹出元素
*
* @param key 键
* @return 弹出的元素
*/
public Object rPop(String key) {
return redisTemplate.opsForList().rightPop(key);
}
/**
* 从右侧批量弹出元素
*
* @param key 键
* @param count 数量
* @return 弹出的元素列表
*/
public List<Object> rPop(String key, long count) {
return redisTemplate.opsForList().rightPop(key, count);
}
/**
* 获取列表指定索引的元素
*
* @param key 键
* @param index 索引0 表示第一个)
* @return 元素
*/
public Object lIndex(String key, long index) {
return redisTemplate.opsForList().index(key, index);
}
/**
* 获取列表长度
*
* @param key 键
* @return 列表长度
*/
public Long lSize(String key) {
return redisTemplate.opsForList().size(key);
}
/**
* 获取列表范围
*
* @param key 键
* @param start 开始索引
* @param end 结束索引
* @return 元素列表
*/
public List<Object> lRange(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
/**
* 裁剪列表(只保留指定范围的元素)
*
* @param key 键
* @param start 开始索引
* @param end 结束索引
*/
public void lTrim(String key, long start, long end) {
redisTemplate.opsForList().trim(key, start, end);
}
/**
* 设置列表指定索引的元素
*
* @param key 键
* @param index 索引
* @param value 值
*/
public void lSet(String key, long index, Object value) {
redisTemplate.opsForList().set(key, index, value);
}
/**
* 移除列表元素
*
* @param key 键
* @param count 移除数量(>0 从左往右,<0 从右往左,=0 全部)
* @param value 要移除的值
* @return 实际移除的数量
*/
public Long lRemove(String key, long count, Object value) {
return redisTemplate.opsForList().remove(key, count, value);
}
// ======================== Set ========================
// ======================== 5. Set 类型操作 ========================
/**
* 添加到集合
*
* @param key 键
* @param values 值数组
* @return 添加的元素数量(不包含已存在的)
*/
public Long sAdd(String key, Object... values) {
return redisTemplate.opsForSet().add(key, values);
}
/**
* 从集合移除元素
*
* @param key 键
* @param values 值数组
* @return 实际移除的数量
*/
public Long sRemove(String key, Object... values) {
return redisTemplate.opsForSet().remove(key, values);
}
/**
* 获取集合所有元素
*
* @param key 键
* @return 元素集合
*/
public Set<Object> sMembers(String key) {
return redisTemplate.opsForSet().members(key);
}
/**
* 判断元素是否在集合中
*
* @param key 键
* @param value 值
* @return 是否存在
*/
public Boolean sIsMember(String key, Object value) {
return redisTemplate.opsForSet().isMember(key, value);
}
/**
* 获取集合大小
*
* @param key 键
* @return 集合大小
*/
public Long sSize(String key) {
return redisTemplate.opsForSet().size(key);
}
/**
* 随机获取集合中的一个元素
*
* @param key 键
* @return 随机元素
*/
public Object sRandomMember(String key) {
return redisTemplate.opsForSet().randomMember(key);
}
/**
* 随机获取集合中的多个元素
*
* @param key 键
* @param count 数量
* @return 随机元素集合
*/
public Set<Object> sRandomMembers(String key, long count) {
return redisTemplate.opsForSet().distinctRandomMembers(key, count);
}
/**
* 集合交集
*
* @param key1 第一个集合键
* @param key2 第二个集合键
* @return 交集元素
*/
public Set<Object> sIntersect(String key1, String key2) {
return redisTemplate.opsForSet().intersect(key1, key2);
}
/**
* 集合并集
*
* @param key1 第一个集合键
* @param key2 第二个集合键
* @return 并集元素
*/
public Set<Object> sUnion(String key1, String key2) {
return redisTemplate.opsForSet().union(key1, key2);
}
/**
* 集合差集key1 - key2
*
* @param key1 第一个集合键
* @param key2 第二个集合键
* @return 差集元素
*/
public Set<Object> sDifference(String key1, String key2) {
return redisTemplate.opsForSet().difference(key1, key2);
}
// ======================== ZSet有序集合========================
// ======================== 6. ZSet 类型操作(有序集合)====================
/**
* 添加到有序集合
*
* @param key 键
* @param value 值
* @param score 分数
* @return 是否添加成功
*/
public Boolean zAdd(String key, Object value, double score) {
return redisTemplate.opsForZSet().add(key, value, score);
}
/**
* 批量添加到有序集合
*
* @param key 键
* @param tuples 分数-值对集合
* @return 添加的数量
*/
public Long zAdd(String key, Set<ZSetOperations.TypedTuple<Object>> tuples) {
return redisTemplate.opsForZSet().add(key, tuples);
}
/**
* 从有序集合移除元素
*
* @param key 键
* @param values 要移除的值
* @return 移除的数量
*/
public Long zRemove(String key, Object... values) {
return redisTemplate.opsForZSet().remove(key, values);
}
/**
* 获取元素排名(升序)
*
* @param key 键
* @param value 值
* @return 排名(从 0 开始),不存在返回 null
*/
public Long zRank(String key, Object value) {
return redisTemplate.opsForZSet().rank(key, value);
}
/**
* 获取元素排名(降序)
*
* @param key 键
* @param value 值
* @return 排名(从 0 开始),不存在返回 null
*/
public Long zReverseRank(String key, Object value) {
return redisTemplate.opsForZSet().reverseRank(key, value);
}
/**
* 获取有序集合范围(升序)
*
* @param key 键
* @param start 开始索引
* @param end 结束索引
* @return 元素集合
*/
public Set<Object> zRange(String key, long start, long end) {
return redisTemplate.opsForZSet().range(key, start, end);
}
/**
* 获取有序集合范围(降序)
*
* @param key 键
* @param start 开始索引
* @param end 结束索引
* @return 元素集合
*/
public Set<Object> zReverseRange(String key, long start, long end) {
return redisTemplate.opsForZSet().reverseRange(key, start, end);
}
/**
* 获取有序集合范围(带分数,升序)
*
* @param key 键
* @param start 开始索引
* @param end 结束索引
* @return 分数-值对集合
*/
public Set<ZSetOperations.TypedTuple<Object>> zRangeWithScores(String key, long start, long end) {
return redisTemplate.opsForZSet().rangeWithScores(key, start, end);
}
/**
* 获取指定分数范围的元素
*
* @param key 键
* @param min 最小分数
* @param max 最大分数
* @return 元素集合
*/
public Set<Object> zRangeByScore(String key, double min, double max) {
return redisTemplate.opsForZSet().rangeByScore(key, min, max);
}
/**
* 获取指定分数范围的元素(分页)
*
* @param key 键
* @param min 最小分数
* @param max 最大分数
* @param offset 偏移量
* @param count 数量
* @return 元素集合
*/
public Set<Object> zRangeByScore(String key, double min, double max, long offset, long count) {
return redisTemplate.opsForZSet().rangeByScore(key, min, max, offset, count);
}
/**
* 统计指定分数范围内的元素数量
*
* @param key 键
* @param min 最小分数
* @param max 最大分数
* @return 元素数量
*/
public Long zCount(String key, double min, double max) {
return redisTemplate.opsForZSet().count(key, min, max);
}
/**
* 获取有序集合大小
*
* @param key 键
* @return 集合大小
*/
public Long zSize(String key) {
return redisTemplate.opsForZSet().size(key);
}
/**
* 获取元素分数
*
* @param key 键
* @param value 值
* @return 分数
*/
public Double zScore(String key, Object value) {
return redisTemplate.opsForZSet().score(key, value);
}
/**
* 移除指定排名范围的元素
*
* @param key 键
* @param start 开始排名
* @param end 结束排名
* @return 移除的数量
*/
public Long zRemoveRange(String key, long start, long end) {
return redisTemplate.opsForZSet().removeRange(key, start, end);
}
/**
* 移除指定分数范围的元素
*
* @param key 键
* @param min 最小分数
* @param max 最大分数
* @return 移除的数量
*/
public Long zRemoveRangeByScore(String key, double min, double max) {
return redisTemplate.opsForZSet().removeRangeByScore(key, min, max);
}
// ======================== Bitmap ========================
// ======================== 7. Bitmap 类型操作 ========================
/**
* 设置位图的指定位
* 使用场景:
* - 用户签到统计
* - 在线状态统计
* - 布隆过滤器
*
* @param key 键
* @param offset 偏移量(位位置)
* @param value 值true=1false=0
* @return 设置前的值
*/
public Boolean setBit(String key, long offset, boolean value) {
return redisTemplate.opsForValue().setBit(key, offset, value);
}
/**
* 获取位图的指定位
*
* @param key 键
* @param offset 偏移量(位位置)
* @return 位值true=1false=0
*/
public Boolean getBit(String key, long offset) {
return redisTemplate.opsForValue().getBit(key, offset);
}
// ======================== HyperLogLog ========================
// ======================== 8. HyperLogLog 类型操作 ========================
/**
* 添加元素到 HyperLogLog
* 使用场景:
* - 统计独立访客数UV
* - 统计独立元素数(基数统计)
* - 优点:内存占用小,适合大数据量
*
* @param key 键
* @param values 值数组
* @return 添加后的基数
*/
public Long pfAdd(String key, Object... values) {
return redisTemplate.opsForHyperLogLog().add(key, values);
}
/**
* 统计基数
*
* @param keys 键数组(可统计多个键的并集基数)
* @return 基数
*/
public Long pfCount(String... keys) {
return redisTemplate.opsForHyperLogLog().size(keys);
}
/**
* 合并多个 HyperLogLog
*
* @param destination 目标键
* @param sourceKeys 源键数组
*/
public void pfMerge(String destination, String... sourceKeys) {
redisTemplate.opsForHyperLogLog().union(destination, sourceKeys);
}

View File

@@ -6,26 +6,56 @@ import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
/**
* Security 工具类
* 提供获取当前登录用户信息的便捷方法
*
* 主要功能:
* 1. 获取当前认证对象
* 2. 获取当前用户名
* 3. 获取当前用户ID
*
* 使用场景:
* - 在 Service 层获取当前用户信息
* - 在拦截器中获取当前用户信息
* - 在日志中记录操作用户
*
* @author Claude
* @since 2024-04-09
*/
@Component
public class SecurityUtils {
/**
* 获取当前 Authentication
* 获取当前 Authentication 对象
* 说明:
* - Authentication 包含认证信息和用户详情
* - 如果用户未认证,返回 null
*
* @return 当前认证对象
*/
public static Authentication getAuthentication(){
public static Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
/**
* 获取登录
* 获取当前登录用户的用户
* 步骤:
* 1. 获取当前认证对象
* 2. 检查用户是否已认证
* 3. 从 Principal 中提取用户名
*
* @return 用户名,未登录返回 null
*/
public static String getUsername(){
public static String getUsername() {
// 1. 获取当前认证对象
Authentication auth = getAuthentication();
if (auth != null && auth.isAuthenticated()) {
Object principal = auth.getPrincipal();
// 2. 判断 Principal 类型并提取用户名
if (principal instanceof CustomUserDetails user) {
return user.getUsername();
}else if (principal instanceof String username) {
return user.getUsername();
} else if (principal instanceof String username) {
return username;
}
}
@@ -33,14 +63,21 @@ public class SecurityUtils {
}
/**
* 获取用户Id
* 获取当前登录用户的用户ID
* 步骤:
* 1. 获取当前认证对象
* 2. 检查用户是否已认证
* 3. 从 CustomUserDetails 中提取用户ID
*
* @return 用户ID未登录返回 null
*/
public static Long getUserId() {
// 1. 获取当前认证对象
Authentication auth = getAuthentication();
// 2. 检查是否已认证且 Principal 类型为 CustomUserDetails
if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof CustomUserDetails user) {
return user.getId();
}
return null;
}
}