輕風解語 發表於 2026-3-23 21:04:00

Hutool 的 `TimedCache` 到期会自动清理吗? ——————hutool cache的"惰性清理"和"定期清理"

<p><strong>是的,Hutool 的 <code>TimedCache</code> 会在到期时自动清理缓存项,<em>但</em>它的清理机制是基于"惰性清理"和"定期清理"两种策略结合,而不是立即清理。</strong></p>
<h2 id="-核心清理机制">🎯 核心清理机制</h2>
<h3 id="1-惰性清理lazy-eviction">1. <strong>惰性清理(Lazy Eviction)</strong></h3>
<pre><code class="language-java">TimedCache&lt;String, Object&gt; cache = new TimedCache&lt;&gt;(1000); // 默认过期时间1秒
cache.put("key1", "value1");

// 在get时检查是否过期
Object value = cache.get("key1"); // 如果已过期,返回null并从map中移除
</code></pre>
<p>每次调用 <code>get()</code> 或 <code>containsKey()</code> 时,会检查该键是否过期,如果过期则移除。</p>
<h3 id="2-定期清理schedule-prune">2. <strong>定期清理(Schedule Prune)</strong></h3>
<pre><code class="language-java">TimedCache&lt;String, Object&gt; cache = new TimedCache&lt;&gt;(1000);
cache.schedulePrune(1000); // 启动定时清理,每1000ms执行一次
</code></pre>
<p>通过调用 <code>schedulePrune()</code> 方法启动定时清理任务,定期扫描并移除过期项。</p>
<h2 id="-使用示例">🔧 使用示例</h2>
<h3 id="示例1基本使用不自动清理">示例1:基本使用(不自动清理)</h3>
<pre><code class="language-java">import cn.hutool.cache.impl.TimedCache;

public class TimedCacheExample1 {
    public static void main(String[] args) throws InterruptedException {
      // 创建缓存,默认过期时间1000ms
      TimedCache&lt;String, String&gt; cache = new TimedCache&lt;&gt;(1000);
      
      // 放入数据
      cache.put("key1", "value1");
      
      // 立即获取
      System.out.println("立即获取:" + cache.get("key1")); // 输出: value1
      
      // 等待1.1秒
      Thread.sleep(1100);
      
      // 过期后获取
      System.out.println("1.1秒后获取:" + cache.get("key1")); // 输出: null
      // 注意:此时key1会从map中移除
      
      // 检查大小
      System.out.println("缓存大小:" + cache.size()); // 输出: 0
    }
}
</code></pre>
<h3 id="示例2启动定时清理">示例2:启动定时清理</h3>
<pre><code class="language-java">import cn.hutool.cache.impl.TimedCache;

public class TimedCacheExample2 {
    public static void main(String[] args) throws InterruptedException {
      // 创建缓存,默认过期时间1000ms
      TimedCache&lt;String, String&gt; cache = new TimedCache&lt;&gt;(1000);
      
      // 启动定时清理,每500ms执行一次
      cache.schedulePrune(500);
      
      cache.put("key1", "value1");
      cache.put("key2", "value2", 2000); // 单独设置过期时间2000ms
      
      System.out.println("初始大小:" + cache.size()); // 输出: 2
      
      Thread.sleep(1100);
      System.out.println("1.1秒后大小:" + cache.size()); // 输出: 1(key1被清理)
      
      Thread.sleep(1000);
      System.out.println("2.1秒后大小:" + cache.size()); // 输出: 0(key2被清理)
      
      // 关闭定时清理
      cache.cancelPruneSchedule();
    }
}
</code></pre>
<h2 id="-清理时机总结">⚡ 清理时机总结</h2>
<table>
<thead>
<tr>
<th>清理类型</th>
<th>触发条件</th>
<th>清理范围</th>
<th>是否立即生效</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>惰性清理</strong></td>
<td>调用 <code>get()</code>、<code>containsKey()</code> 等方法</td>
<td>只检查被访问的key</td>
<td>是</td>
</tr>
<tr>
<td><strong>定时清理</strong></td>
<td>定时任务触发(需手动调用 <code>schedulePrune()</code>)</td>
<td>所有过期key</td>
<td>是</td>
</tr>
<tr>
<td><strong>容量清理</strong></td>
<td>达到缓存容量上限</td>
<td>最近最少使用的过期项</td>
<td>是</td>
</tr>
</tbody>
</table>
<h2 id="-源码解析">📊 源码解析</h2>
<p>查看 <code>TimedCache</code> 源码的关键方法:</p>
<h3 id="1-检查过期并移除的方法">1. <strong>检查过期并移除的方法</strong></h3>
<pre><code class="language-java">// 在get操作时会调用
public V get(K key, boolean isUpdateLastAccess) {
    CacheObj&lt;K, V&gt; co = getWithoutLock(key);
    if (co == null) {
      missCount.increment();
      return null;
    }
   
    // 检查是否过期
    if (co.isExpired()) {
      // 过期则移除
      remove(key, true);
      missCount.increment();
      return null;
    }
   
    // ... 返回缓存值
}
</code></pre>
<h3 id="2-定时清理任务">2. <strong>定时清理任务</strong></h3>
<pre><code class="language-java">public void schedulePrune(long delay) {
    this.pruneTimer.schedule(new TimerTask() {
      @Override
      public void run() {
            pruneCache(); // 执行清理
      }
    }, delay, delay);
}
</code></pre>
<h2 id="-使用建议">💡 使用建议</h2>
<h3 id="1-根据场景选择清理策略">1. <strong>根据场景选择清理策略</strong></h3>
<pre><code class="language-java">// 场景1:低频率访问,使用惰性清理即可
TimedCache&lt;String, Object&gt; cache1 = new TimedCache&lt;&gt;(5000);

// 场景2:高频率访问,需要定时清理避免内存泄漏
TimedCache&lt;String, Object&gt; cache2 = new TimedCache&lt;&gt;(5000);
cache2.schedulePrune(1000); // 每1秒清理一次

// 场景3:需要严格内存控制
TimedCache&lt;String, Object&gt; cache3 = new TimedCache&lt;&gt;(5000, 1000); // 容量限制1000
cache3.schedulePrune(500);
</code></pre>
<h3 id="2-合理设置过期时间">2. <strong>合理设置过期时间</strong></h3>
<pre><code class="language-java">// 为不同数据设置不同过期时间
TimedCache&lt;String, Object&gt; cache = new TimedCache&lt;&gt;();

// 默认过期时间5秒
cache.setDefaultTimeout(5000);

// 特定key设置特定过期时间
cache.put("session_token", "abc123", 30 * 60 * 1000); // 30分钟
cache.put("captcha", "1234", 5 * 60 * 1000);         // 5分钟
cache.put("page_cache", "&lt;html&gt;...&lt;/html&gt;", 1000);   // 1秒
</code></pre>
<h3 id="3-监听器支持">3. <strong>监听器支持</strong></h3>
<pre><code class="language-java">TimedCache&lt;String, Object&gt; cache = new TimedCache&lt;&gt;(1000);
cache.setListener(new CacheListener&lt;String, Object&gt;() {
    @Override
    public void onRemove(String key, Object value) {
      System.out.println("Key被移除:" + key + ",值:" + value);
    }
});
</code></pre>
<h2 id="️-注意事项">⚠️ 注意事项</h2>
<ol>
<li><strong>定时清理需手动启动</strong>:<code>schedulePrune()</code> 必须显式调用才会启动定时清理</li>
<li><strong>内存泄漏风险</strong>:如果不调用 <code>schedulePrune()</code> 且从不访问过期key,这些key会一直占用内存</li>
<li><strong>Timer的内存泄漏</strong>:定时清理使用 <code>Timer</code>,如果缓存对象被GC,定时任务不会自动取消</li>
<li><strong>性能考虑</strong>:定时清理间隔不宜过短,特别是缓存项很多时</li>
</ol>
<h2 id="-完整生命周期示例">🔄 完整生命周期示例</h2>
<pre><code class="language-java">import cn.hutool.cache.impl.TimedCache;

public class TimedCacheLifecycle {
    public static void main(String[] args) throws InterruptedException {
      // 1. 创建缓存,默认过期时间2秒
      TimedCache&lt;String, String&gt; cache = new TimedCache&lt;&gt;(2000);
      
      // 2. 启动定时清理,每1秒一次
      cache.schedulePrune(1000);
      
      // 3. 添加数据
      cache.put("short", "短期数据", 1000);    // 1秒过期
      cache.put("medium", "中期数据", 3000);   // 3秒过期
      cache.put("long", "长期数据");         // 默认2秒过期
      
      // 4. 监控状态
      for (int i = 0; i &lt; 5; i++) {
            System.out.println("\n=== 第 " + (i+1) + " 秒 ===");
            System.out.println("缓存大小: " + cache.size());
            System.out.println("short: " + cache.get("short"));
            System.out.println("medium: " + cache.get("medium"));
            System.out.println("long: " + cache.get("long"));
            
            Thread.sleep(1000);
      }
      
      // 5. 清理资源
      cache.clear();
      cache.cancelPruneSchedule();
    }
}
</code></pre>
<h2 id="-总结">🎯 总结</h2>
<p><strong><code>TimedCache</code> 的自动清理机制</strong>:</p>
<ol>
<li><strong>不会立即自动清理</strong>:默认只有惰性清理</li>
<li><strong>需要手动启动定时清理</strong>:通过 <code>schedulePrune()</code> 方法</li>
<li><strong>清理策略可配置</strong>:可以根据业务需求选择清理频率</li>
</ol>
<p><strong>最佳实践</strong>:在创建 <code>TimedCache</code> 后立即调用 <code>schedulePrune()</code>,以确保过期数据会被及时清理,避免内存泄漏问题。</p>
<pre><code class="language-java">public final class LocalCacheUtil {
    private static TimedCache&lt;String, Object&gt; timedCache = new TimedCache&lt;&gt;(DAYS.toMillis(1));

    static {
      // init prune schedule at fix rate
      timedCache.schedulePrune(MINUTES.toMillis(3));
    }

    ...
}
</code></pre>
<p><br><br><br><br></p><hr><p></p>
<p><em>ref:缓存过期的惰性删除(Lazy Deletion)策略</em><br>
<em>ref:hutool cache的"惰性清理"和"定期清理"</em><br>
<em>ref:如何精准控制业务合同到期后不再发生业务?</em></p>


</div>
<div id="MySignature" role="contentinfo">
    <hr class="signhr"><p style="text-indent:2em;font-size:12px;text-align:center">当看到一些不好的代码时,会发现我还算优秀;当看到优秀的代码时,也才意识到持续学习的重要!--buguge<br>本文来自博客园,转载请注明原文链接:https://www.cnblogs.com/buguge/p/19759976</p><hr class="signhr">
<style>hr.signhr{width:80%;margin:0 auto;border: 0;height: 4px;background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0))}</style><br><br>
来源:https://www.cnblogs.com/buguge/p/19759976
頁: [1]
查看完整版本: Hutool 的 `TimedCache` 到期会自动清理吗? ——————hutool cache的"惰性清理"和"定期清理"