热点数据计算整体来讲就是基于访问频率,可以是整体的访问次数,可以是一定时间内的频率,可以是部分请求的采样,可以借助成熟工具等,要根据业务需求来定
INCR
命令或监控工具(如 Redis Monitor)统计键的访问频率。统计访问频率要确保并发场景下数据操作的一致性
import redis.clients.jedis.Jedis;
public class HotKeyDetector {
private Jedis jedis;
public HotKeyDetector(Jedis jedis) {
this.jedis = jedis;
}
public void trackAccess(String key) {
// 使用 Redis 的计数器记录每个键的访问次数
jedis.incr("access_count:" + key);
}
public String getMostFrequentKey() {
// 获取所有键的访问计数
Set<String> keys = jedis.keys("access_count:*");
String hotKey = null;
long maxCount = 0;
for (String key : keys) {
long count = Long.parseLong(jedis.get(key));
if (count > maxCount) {
maxCount = count;
hotKey = key.replace("access_count:", "");
}
}
return hotKey;
}
}
ZSET
(有序集合)记录每个键的访问时间戳。import redis.clients.jedis.Jedis;
public class TimeWindowHotKeyDetector {
private Jedis jedis;
private static final long WINDOW_SIZE = 60000; // 时间窗口大小(1 分钟)
public TimeWindowHotKeyDetector(Jedis jedis) {
this.jedis = jedis;
}
public void trackAccess(String key) {
long currentTime = System.currentTimeMillis();
// 使用 ZSET 记录访问时间戳
jedis.zadd("access_times:" + key, currentTime, String.valueOf(currentTime));
// 清理时间窗口之外的数据
jedis.zremrangeByScore("access_times:" + key, 0, currentTime - WINDOW_SIZE);
}
public String getMostFrequentKey() {
Set<String> keys = jedis.keys("access_times:*");
String hotKey = null;
long maxCount = 0;
for (String key : keys) {
long count = jedis.zcard(key);
if (count > maxCount) {
maxCount = count;
hotKey = key.replace("access_times:", "");
}
}
return hotKey;
}
}
MONITOR
命令或客户端代码采样请求。判断 Redis 分布式缓存中的热点数据可以通过以下方法:
数据预热是指在系统启动或流量高峰到来之前,提前将热点数据加载到缓存中,以避免大量请求直接访问数据库,从而提升系统性能和稳定性。
人为指定热key,将数据加载到缓存中
import redis.clients.jedis.Jedis;
public class DataPreheating {
private Jedis jedis;
public DataPreheating(Jedis jedis) {
this.jedis = jedis;
}
public void preheatData() {
// 模拟从数据库加载热点数据
String[] hotKeys = {"key1", "key2", "key3"};
for (String key : hotKeys) {
String value = loadFromDatabase(key);
jedis.set(key, value);
}
}
private String loadFromDatabase(String key) {
// 模拟数据库查询
return "value_for_" + key;
}
}
系统自动读取热key,不需要人为指定
import redis.clients.jedis.Jedis;
import java.util.List;
public class HistoricalDataPreheating {
private Jedis jedis;
public HistoricalDataPreheating(Jedis jedis) {
this.jedis = jedis;
}
public void preheatData() {
// 从历史访问记录中获取热点数据
List<String> hotKeys = getHotKeysFromLogs();
for (String key : hotKeys) {
String value = loadFromDatabase(key);
jedis.set(key, value);
}
}
private List<String> getHotKeysFromLogs() {
// 模拟从日志中分析热点数据
return List.of("key1", "key2", "key3");
}
private String loadFromDatabase(String key) {
// 模拟数据库查询
return "value_for_" + key;
}
}
定期自动加载热点数据,热点数据可通过访问频率,时间范围等自动计算
import redis.clients.jedis.Jedis;
import java.util.Timer;
import java.util.TimerTask;
public class ScheduledDataPreheating {
private Jedis jedis;
public ScheduledDataPreheating(Jedis jedis) {
this.jedis = jedis;
}
public void startPreheating() {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
preheatData();
}
}, 0, 60 * 60 * 1000); // 每小时执行一次
}
private void preheatData() {
// 模拟从数据库加载热点数据
String[] hotKeys = {"key1", "key2", "key3"};
for (String key : hotKeys) {
String value = loadFromDatabase(key);
jedis.set(key, value);
}
}
private String loadFromDatabase(String key) {
// 模拟数据库查询
return "value_for_" + key;
}
}
详细方案可参考此篇:
数据库与缓存一致性方案
import redis.clients.jedis.Jedis;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class MessageQueuePreheating {
private Jedis jedis;
private BlockingQueue<String> messageQueue;
public MessageQueuePreheating(Jedis jedis) {
this.jedis = jedis;
this.messageQueue = new LinkedBlockingQueue<>();
startConsumer();
}
public void onDataChange(String key) {
// 当数据库数据变化时,将 key 放入消息队列
messageQueue.offer(key);
}
private void startConsumer() {
new Thread(() -> {
while (true) {
try {
String key = messageQueue.take();
String value = loadFromDatabase(key);
jedis.set(key, value);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}).start();
}
private String loadFromDatabase(String key) {
// 模拟数据库查询
return "value_for_" + key;
}
}
没淘汰的key默认为热点数据
LRU:利用双向链表,最近访问的放链表头部,最久未访问的放链表尾部,空间不足时删除尾部数据
LFP:利用最小堆,访问频率最低的放在堆顶,空间不足时移除堆顶数据
在 Redis 配置文件中设置:
maxmemory-policy allkeys-lfu
allkeys-lfu
:淘汰访问频率最低的键。allkeys-lru
:淘汰最近最少使用的键。Redis 分布式缓存的数据预热可以通过以下方法实现: