|
@@ -1,13 +1,12 @@
|
|
package com.txz.cif.core;
|
|
package com.txz.cif.core;
|
|
|
|
|
|
import org.springframework.data.redis.core.RedisTemplate;
|
|
import org.springframework.data.redis.core.RedisTemplate;
|
|
|
|
+import org.springframework.data.redis.core.ZSetOperations;
|
|
import org.springframework.stereotype.Component;
|
|
import org.springframework.stereotype.Component;
|
|
import org.springframework.util.CollectionUtils;
|
|
import org.springframework.util.CollectionUtils;
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
import javax.annotation.Resource;
|
|
-import java.util.List;
|
|
|
|
-import java.util.Map;
|
|
|
|
-import java.util.Set;
|
|
|
|
|
|
+import java.util.*;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
|
|
|
|
@@ -592,4 +591,302 @@ public class RedisUtil {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+
|
|
|
|
+ //===============================ZSet(排行榜)=============================
|
|
|
|
+ /**
|
|
|
|
+ * 向有序集合添加元素(用于初始化或更新用户分数)
|
|
|
|
+ * @param key 有序集合的键(排行榜名称)
|
|
|
|
+ * @param value 元素值(如用户ID)
|
|
|
|
+ * @param score 分数(排序依据)
|
|
|
|
+ * @return true=新增元素,false=更新已有元素
|
|
|
|
+ */
|
|
|
|
+ public Boolean zAdd(String key, Object value, double score) {
|
|
|
|
+ try {
|
|
|
|
+ return redisTemplate.opsForZSet().add(key, value, score);
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ e.printStackTrace();
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 向有序集合添加元素并设置过期时间
|
|
|
|
+ * @param key 有序集合的键
|
|
|
|
+ * @param value 元素值
|
|
|
|
+ * @param score 分数
|
|
|
|
+ * @param time 过期时间
|
|
|
|
+ * @param timeUnit 时间单位
|
|
|
|
+ * @return true=操作成功
|
|
|
|
+ */
|
|
|
|
+ public Boolean zAdd(String key, Object value, double score, long time, TimeUnit timeUnit) {
|
|
|
|
+ try {
|
|
|
|
+ Boolean result = redisTemplate.opsForZSet().add(key, value, score);
|
|
|
|
+ if (time > 0) {
|
|
|
|
+ expire(key, time, timeUnit);
|
|
|
|
+ }
|
|
|
|
+ return result;
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ e.printStackTrace();
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 增加有序集合中元素的分数(用于排行榜分数累加)
|
|
|
|
+ * @param key 有序集合的键
|
|
|
|
+ * @param value 元素值
|
|
|
|
+ * @param delta 增加的分数(正数增加,负数减少)
|
|
|
|
+ * @return 增加后的分数
|
|
|
|
+ */
|
|
|
|
+ public Double zIncrBy(String key, Object value, double delta) {
|
|
|
|
+ try {
|
|
|
|
+ return redisTemplate.opsForZSet().incrementScore(key, value, delta);
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ e.printStackTrace();
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 获取元素在有序集合中的排名(从高到低)
|
|
|
|
+ * @param key 有序集合的键
|
|
|
|
+ * @param value 元素值
|
|
|
|
+ * @return 排名(0表示第一名,null表示元素不存在)
|
|
|
|
+ */
|
|
|
|
+ public Long zRevRank(String key, Object value) {
|
|
|
|
+ try {
|
|
|
|
+ return redisTemplate.opsForZSet().reverseRank(key, value);
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ e.printStackTrace();
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 获取元素在有序集合中的分数
|
|
|
|
+ * @param key 有序集合的键
|
|
|
|
+ * @param value 元素值
|
|
|
|
+ * @return 分数(null表示元素不存在)
|
|
|
|
+ */
|
|
|
|
+ public Double zScore(String key, Object value) {
|
|
|
|
+ try {
|
|
|
|
+ return redisTemplate.opsForZSet().score(key, value);
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ e.printStackTrace();
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 获取有序集合中指定排名范围的元素(从高到低)
|
|
|
|
+ * @param key 有序集合的键
|
|
|
|
+ * @param start 起始排名(0表示第一名)
|
|
|
|
+ * @param end 结束排名(如9表示前10名)
|
|
|
|
+ * @return 元素列表(仅包含值,不包含分数)
|
|
|
|
+ */
|
|
|
|
+ public Set<Object> zRevRange(String key, long start, long end) {
|
|
|
|
+ try {
|
|
|
|
+ return redisTemplate.opsForZSet().reverseRange(key, start, end);
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ e.printStackTrace();
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 获取有序集合中指定排名范围的元素(包含分数,从高到低)
|
|
|
|
+ * @param key 有序集合的键
|
|
|
|
+ * @param start 起始排名(0表示第一名)
|
|
|
|
+ * @param end 结束排名(如9表示前10名)
|
|
|
|
+ * @return 元素-分数映射集合(ZSetOperations.TypedTuple包含value和score)
|
|
|
|
+ */
|
|
|
|
+ public Set<ZSetOperations.TypedTuple<Object>> zRevRangeWithScores(String key, long start, long end) {
|
|
|
|
+ try {
|
|
|
|
+ return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ e.printStackTrace();
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 获取有序集合中分数在指定范围内的元素数量
|
|
|
|
+ * @param key 有序集合的键
|
|
|
|
+ * @param min 最小分数
|
|
|
|
+ * @param max 最大分数
|
|
|
|
+ * @return 元素数量
|
|
|
|
+ */
|
|
|
|
+ public Long zCount(String key, double min, double max) {
|
|
|
|
+ try {
|
|
|
|
+ return redisTemplate.opsForZSet().count(key, min, max);
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ e.printStackTrace();
|
|
|
|
+ return 0L;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 获取有序集合的总元素数量(排行榜总人数)
|
|
|
|
+ * @param key 有序集合的键
|
|
|
|
+ * @return 总数量
|
|
|
|
+ */
|
|
|
|
+ public Long zSize(String key) {
|
|
|
|
+ try {
|
|
|
|
+ return redisTemplate.opsForZSet().size(key);
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ e.printStackTrace();
|
|
|
|
+ return 0L;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 从有序集合中删除元素
|
|
|
|
+ * @param key 有序集合的键
|
|
|
|
+ * @param values 要删除的元素(可多个)
|
|
|
|
+ * @return 成功删除的数量
|
|
|
|
+ */
|
|
|
|
+ public Long zRemove(String key, Object... values) {
|
|
|
|
+ try {
|
|
|
|
+ return redisTemplate.opsForZSet().remove(key, values);
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ e.printStackTrace();
|
|
|
|
+ return 0L;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+// ========================= ZSet 分数编码工具方法 =========================
|
|
|
|
+ /**
|
|
|
|
+ * 生成复合分数(原始分数 + 时间戳)
|
|
|
|
+ * 同原始分数时,较早录入的元素分数更高(排前面)
|
|
|
|
+ * @param originalScore 原始分数(业务分数)
|
|
|
|
+ * @param timestamp 录入时间戳(毫秒),建议使用System.currentTimeMillis()
|
|
|
|
+ * @return 复合分数(可直接用于Redis的ZSet)
|
|
|
|
+ */
|
|
|
|
+ private double encodeScore(double originalScore, long timestamp) {
|
|
|
|
+ // 权重:确保原始分数为第一排序维度(1e18需大于最大可能的时间戳,避免冲突)
|
|
|
|
+ long weight = 1000_000_000_000_000_000L; // 1e18
|
|
|
|
+ // 反向时间戳:将时间戳反转,使较早的时间戳对应更大的值
|
|
|
|
+ long reversedTimestamp = Long.MAX_VALUE - timestamp;
|
|
|
|
+ // 复合分数 = 原始分数 * 权重 + 反向时间戳(保证原始分数优先,同分时时间早的排前面)
|
|
|
|
+ return originalScore * weight + reversedTimestamp;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 从复合分数中解析原始分数
|
|
|
|
+ * @param encodedScore 复合分数
|
|
|
|
+ * @return 原始业务分数
|
|
|
|
+ */
|
|
|
|
+ public double decodeOriginalScore(double encodedScore) {
|
|
|
|
+ long weight = 1000_000_000_000_000_000L;
|
|
|
|
+ return Math.floor(encodedScore / weight);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 从复合分数中解析录入时间戳
|
|
|
|
+ * @param encodedScore 复合分数
|
|
|
|
+ * @return 录入时的时间戳(毫秒)
|
|
|
|
+ */
|
|
|
|
+ public long decodeTimestamp(double encodedScore) {
|
|
|
|
+ long weight = 1000_000_000_000_000_000L;
|
|
|
|
+ long reversedTimestamp = (long) (encodedScore % weight);
|
|
|
|
+ return Long.MAX_VALUE - reversedTimestamp;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+// ========================= 支持时间排序的排行榜方法 =========================
|
|
|
|
+ /**
|
|
|
|
+ * 新增/更新用户分数(同分时,先录入的排前面)
|
|
|
|
+ * @param key 排行榜键名
|
|
|
|
+ * @param userId 用户ID(元素值)
|
|
|
|
+ * @param originalScore 原始业务分数
|
|
|
|
+ * @param timestamp 录入时间戳(毫秒,建议使用System.currentTimeMillis())
|
|
|
|
+ * @return true=新增元素,false=更新已有元素
|
|
|
|
+ */
|
|
|
|
+ public Boolean zAddWithTimeOrder(String key, Object userId, double originalScore, long timestamp) {
|
|
|
|
+ try {
|
|
|
|
+ double encodedScore = encodeScore(originalScore, timestamp);
|
|
|
|
+ return redisTemplate.opsForZSet().add(key, userId, encodedScore);
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ e.printStackTrace();
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 新增/更新用户分数并设置过期时间
|
|
|
|
+ * @param key 排行榜键名
|
|
|
|
+ * @param userId 用户ID
|
|
|
|
+ * @param originalScore 原始业务分数
|
|
|
|
+ * @param timestamp 录入时间戳
|
|
|
|
+ * @param time 过期时间
|
|
|
|
+ * @param timeUnit 时间单位
|
|
|
|
+ * @return true=操作成功
|
|
|
|
+ */
|
|
|
|
+ public Boolean zAddWithTimeOrder(String key, Object userId, double originalScore, long timestamp,
|
|
|
|
+ long time, TimeUnit timeUnit) {
|
|
|
|
+ try {
|
|
|
|
+ double encodedScore = encodeScore(originalScore, timestamp);
|
|
|
|
+ Boolean result = redisTemplate.opsForZSet().add(key, userId, encodedScore);
|
|
|
|
+ if (time > 0) {
|
|
|
|
+ expire(key, time, timeUnit);
|
|
|
|
+ }
|
|
|
|
+ return result;
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ e.printStackTrace();
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 获取用户的原始分数(业务分数)
|
|
|
|
+ * @param key 排行榜键名
|
|
|
|
+ * @param userId 用户ID
|
|
|
|
+ * @return 原始分数(null表示用户不存在)
|
|
|
|
+ */
|
|
|
|
+ public Double zGetOriginalScore(String key, Object userId) {
|
|
|
|
+ Double encodedScore = zScore(key, userId); // 复用之前的zScore方法
|
|
|
|
+ return encodedScore != null ? decodeOriginalScore(encodedScore) : null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 获取用户的录入时间戳
|
|
|
|
+ * @param key 排行榜键名
|
|
|
|
+ * @param userId 用户ID
|
|
|
|
+ * @return 时间戳(毫秒,null表示用户不存在)
|
|
|
|
+ */
|
|
|
|
+ public Long zGetTimestamp(String key, Object userId) {
|
|
|
|
+ Double encodedScore = zScore(key, userId);
|
|
|
|
+ return encodedScore != null ? decodeTimestamp(encodedScore) : null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * 获取排行榜指定范围(从高到低),包含原始分数和时间戳
|
|
|
|
+ * @param key 排行榜键名
|
|
|
|
+ * @param start 起始排名(0=第一名)
|
|
|
|
+ * @param end 结束排名(如9=前10名)
|
|
|
|
+ * @return 列表元素包含:用户ID、原始分数、时间戳
|
|
|
|
+ */
|
|
|
|
+ public List<Map<String, Object>> zRevRangeWithDetails(String key, long start, long end) {
|
|
|
|
+ try {
|
|
|
|
+ Set<ZSetOperations.TypedTuple<Object>> tuples = redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
|
|
|
|
+ if (tuples == null) {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+ List<Map<String, Object>> result = new ArrayList<>();
|
|
|
|
+ for (ZSetOperations.TypedTuple<Object> tuple : tuples) {
|
|
|
|
+ Map<String, Object> detail = new HashMap<>();
|
|
|
|
+ detail.put("userId", tuple.getValue());
|
|
|
|
+ detail.put("originalScore", decodeOriginalScore(tuple.getScore()));
|
|
|
|
+ detail.put("timestamp", decodeTimestamp(tuple.getScore()));
|
|
|
|
+ result.add(detail);
|
|
|
|
+ }
|
|
|
|
+ return result;
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ e.printStackTrace();
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
}
|
|
}
|