Merge pull request #4157 from Lettuceleaves/feat/sieve-cache

Feat/sieve cache
This commit is contained in:
Golden Looly
2025-11-26 17:39:33 +08:00
committed by GitHub
2 changed files with 442 additions and 0 deletions

View File

@@ -0,0 +1,287 @@
/*
* Copyright (c) 2013-2025 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.hutool.v7.core.cache.impl;
import cn.hutool.v7.core.lang.mutable.Mutable;
import cn.hutool.v7.core.lang.mutable.MutableObj;
import java.io.Serial;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.locks.ReentrantLock;
/**
* SIEVE 缓存算法实现<br>
* <p>
* SIEVE 是一种比 LRU 更简单且通常更高效的缓存算法。<br>
* 核心特性:<br>
* 缓存命中时,仅将节点的 {@code visited} 标记设为 true不移动节点位置。<br>
* 淘汰时,使用 {@code hand} 指针从尾部扫描,淘汰 {@code visited=false} 的节点。<br>
* 新加入节点 {@code visited = false} 且置于头部Hand 指针扫描时会优先淘汰它,提供抗扫描能力。<br>
* </p>
*
* @param <K> 键类型
* @param <V> 值类型
* @author Lettuceleaves
*/
public class SieveCache<K, V> extends LockedCache<K, V> {
@Serial
private static final long serialVersionUID = 1L;
/**
* 双向链表头节点
*/
private SieveCacheObj<K, V> head;
/**
* 双向链表尾节点
*/
private SieveCacheObj<K, V> tail;
/**
* 下一次扫描的起始位置
*/
private SieveCacheObj<K, V> hand;
/**
* 构造<br>
* 默认无超时
*
* @param capacity 容量
*/
public SieveCache(int capacity) {
this(capacity, 0);
}
/**
* 构造
*
* @param capacity 容量
* @param timeout 默认超时时间,单位:毫秒
*/
public SieveCache(int capacity, long timeout) {
if (Integer.MAX_VALUE == capacity) {
capacity -= 1;
}
this.capacity = capacity;
this.timeout = timeout;
// 这里的设置 capacity + 1, 1.0f 避免触发扩容
this.cacheMap = new HashMap<>(capacity + 1, 1.0f);
this.lock = new ReentrantLock();
}
@Override
protected void putWithoutLock(K key, V object, long timeout) {
final Mutable<K> keyObj = MutableObj.of(key);
SieveCacheObj<K, V> co = (SieveCacheObj<K, V>) cacheMap.get(keyObj);
if (co != null) {
final SieveCacheObj<K, V> newCo = new SieveCacheObj<>(key, object, timeout);
// 新加入的节点,默认刚访问过,防止立刻被淘汰
newCo.visited = true;
// 替换旧节点
replaceNode(co, newCo);
cacheMap.put(keyObj, newCo);
} else {
co = new SieveCacheObj<>(key, object, timeout);
cacheMap.put(keyObj, co);
addToHead(co);
co.visited = false;
if (cacheMap.size() > capacity) {
pruneCache();
}
}
}
/**
* 在双向链表中用 newNode 替换 oldNode保持链表结构不变
*
* @param oldNode 旧节点
* @param newNode 新节点
*/
private void replaceNode(SieveCacheObj<K, V> oldNode, SieveCacheObj<K, V> newNode) {
newNode.prev = oldNode.prev;
newNode.next = oldNode.next;
// 更新前向指针
if (oldNode.prev != null) {
oldNode.prev.next = newNode;
} else {
head = newNode;
}
// 更新后向指针
if (oldNode.next != null) {
oldNode.next.prev = newNode;
} else {
tail = newNode;
}
// 将hand转移至新节点防止扫描时淘汰热点数据
if (hand == oldNode) {
hand = newNode;
}
oldNode.prev = null;
oldNode.next = null;
}
@Override
protected CacheObj<K, V> getOrRemoveExpiredWithoutLock(K key) {
final Mutable<K> keyObj = MutableObj.of(key);
final SieveCacheObj<K, V> co = (SieveCacheObj<K, V>) cacheMap.get(keyObj);
if (null != co) {
if (co.isExpired()) {
removeWithoutLock(key);
return null;
}
co.visited = true;
co.lastAccess = System.currentTimeMillis();
}
return co;
}
@Override
protected CacheObj<K, V> removeWithoutLock(K key) {
final Mutable<K> keyObj = MutableObj.of(key);
final SieveCacheObj<K, V> co = (SieveCacheObj<K, V>) cacheMap.remove(keyObj);
if (co != null) {
removeNode(co);
}
return co;
}
/**
* 优先清理过期对象如果容量仍溢出反向扫描visited为false的节点设置true节点为false
*/
@Override
protected int pruneCache() {
int count = 0;
if (isPruneExpiredActive()) {
final Iterator<CacheObj<K, V>> values = cacheObjIter();
CacheObj<K, V> co;
while (values.hasNext()) {
co = values.next();
if (co.isExpired()) {
values.remove();
removeNode((SieveCacheObj<K, V>) co);
onRemove(co.key, co.obj);
count++;
}
}
}
if (cacheMap.size() > capacity) {
if (hand == null) {
hand = tail;
}
while (cacheMap.size() > capacity) {
if (hand == null) {
hand = tail;
}
if (!hand.visited) {
final SieveCacheObj<K, V> victim = hand;
hand = hand.prev;
final Mutable<K> keyObj = MutableObj.of(victim.key);
cacheMap.remove(keyObj);
removeNode(victim);
onRemove(victim.key, victim.obj);
count++;
} else {
hand.visited = false;
hand = hand.prev;
}
}
}
return count;
}
/**
* 将节点加入链表头部
*
* @param node 节点
*/
private void addToHead(SieveCacheObj<K, V> node) {
node.next = head;
node.prev = null;
if (head != null) {
head.prev = node;
}
head = node;
if (tail == null) {
tail = node;
}
}
/**
* 从链表中移除节点
*
* @param node 节点
*/
private void removeNode(SieveCacheObj<K, V> node) {
if (node == hand) {
hand = node.prev;
}
if (node.prev != null) {
node.prev.next = node.next;
} else {
head = node.next;
}
if (node.next != null) {
node.next.prev = node.prev;
} else {
tail = node.prev;
}
node.next = null;
node.prev = null;
}
/**
* 给节点添加visited属性用于Sieve缓存淘汰策略
*/
private static class SieveCacheObj<K, V> extends CacheObj<K, V> {
@Serial
private static final long serialVersionUID = 1L;
/**
* 是否被访问过
*/
boolean visited = false;
/**
* 前向节点
*/
SieveCacheObj<K, V> prev;
/**
* 后向节点
*/
SieveCacheObj<K, V> next;
protected SieveCacheObj(final K key, final V obj, final long ttl) {
super(key, obj, ttl);
}
}
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright (c) 2013-2025 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.hutool.v7.core.cache;
import cn.hutool.v7.core.cache.impl.SieveCache;
import cn.hutool.v7.core.thread.ThreadUtil;
import cn.hutool.v7.core.util.RandomUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* SIEVE 缓存算法单元测试
*/
public class SieveCacheTest {
@Test
public void evictionLogicTest() {
SieveCache<String, String> cache = new SieveCache<>(3);
cache.put("A", "A");
cache.put("B", "B");
cache.put("C", "C");
cache.get("A");
cache.put("D", "D");
Assertions.assertEquals("A", cache.get("A"));
Assertions.assertEquals("C", cache.get("C"));
Assertions.assertEquals("D", cache.get("D"));
Assertions.assertNull(cache.get("B"), "B 应该被淘汰,因为它是未访问过的节点");
}
@Test
public void expiryTest() {
SieveCache<String, String> cache = new SieveCache<>(3);
cache.put("k1", "v1", 100);
cache.put("k2", "v2", 10000);
ThreadUtil.sleep(200);
Assertions.assertNull(cache.get("k1"), "k1 应该过期");
Assertions.assertEquals("v2", cache.get("k2"), "k2 应该存在");
Assertions.assertEquals(1, cache.size(), "size 应该为 1");
}
@Test
public void listenerTest() {
final AtomicInteger removeCount = new AtomicInteger();
SieveCache<Integer, Integer> cache = new SieveCache<>(2);
cache.setListener((key, value) -> {
removeCount.incrementAndGet();
});
cache.put(1, 1);
cache.put(2, 2);
cache.put(3, 3);
Assertions.assertEquals(1, removeCount.get());
}
@Test
public void concurrencyPressureTest() throws InterruptedException {
int threadCount = 20;
int loopCount = 2000;
int capacity = 100;
final SieveCache<String, String> cache = new SieveCache<>(capacity);
final CountDownLatch latch = new CountDownLatch(threadCount);
final AtomicInteger errorCount = new AtomicInteger(0);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
for (int j = 0; j < loopCount; j++) {
String key = String.valueOf(RandomUtil.randomInt(0, 1000));
if (RandomUtil.randomBoolean()) {
cache.put(key, "val-" + key);
} else {
cache.get(key);
}
}
} catch (Exception e) {
errorCount.incrementAndGet();
} finally {
latch.countDown();
}
}).start();
}
latch.await();
Assertions.assertEquals(0, errorCount.get(), "并发执行不应出现异常");
Assertions.assertTrue(cache.size() <= capacity, "缓存大小不应超过容量");
int iteratorCount = 0;
for (String ignored : cache) {
iteratorCount++;
}
Assertions.assertEquals(cache.size(), iteratorCount, "迭代器数量与 size() 应一致");
}
/**
* 抗扫描能力测试
* 如果扫描数据量过大(如 50% 容量)且热点数据无访问,热点数据的保护位会被耗尽,因此这里仅模拟少量数据的扫描攻击。
*/
@Test
public void scanResistanceTest() {
int capacity = 10;
SieveCache<Integer, Integer> cache = new SieveCache<>(capacity);
// 填满热点数据
for (int i = 0; i < capacity; i++) {
cache.put(i, i);
}
// 模拟热点访问
for (int i = 0; i < capacity; i++) {
cache.get(i);
}
// 插入 1 个冷数据
cache.put(10, 10);
int retainedHotItems = 0;
for (int i = 0; i < capacity; i++) {
if (cache.get(i) != null) {
retainedHotItems++;
}
}
Assertions.assertNull(cache.get(10), "冷数据 (10) 应该被淘汰");
Assertions.assertEquals(capacity, retainedHotItems, "所有热点数据 (0-9) 应该被保留");
}
}