Skip to content

内置缓存系统 AbsCache ​

在构建高性能的网络应用时,缓存系统起着至关重要的作用。Tio-boot 框架内置了一种轻量级的缓存系统——AbsCache。本文将详细介绍如何使用 AbsCache 来缓存数据,并通过 CacheFactory 实现缓存实例的注册和管理。Tio-boot 框架内部状态都使用 AbsCache 来进行存储

简介 ​

1. 什么是 AbsCache ​

AbsCache 是 Tio-boot 框架中的抽象类,用于定义缓存的核心功能。它提供了基本的缓存操作,例如存储、获取、删除缓存条目,并支持配置缓存的生存时间(TTL, Time to Live)和空闲时间(TTI, Time to Idle)。AbsCache 支持各种实现,如基于 Map 的缓存。

AbsCache 核心功能 ​

  • TTL(存活时间):缓存条目从创建开始有效,超过设定时间后将自动过期。
  • TTI(空闲时间):缓存条目在最后一次访问后有一定的有效期,如果在此期间未再次访问,也会过期。

使用 ​

1. CacheFactory 和 AbsCache 的使用流程 ​

使用 AbsCache 时,主要通过 CacheFactory 工厂类来管理缓存实例。它可以注册并返回缓存实例,允许开发者根据需要缓存数据。

核心步骤: ​

  1. 注册缓存实例:通过 CacheFactory.register() 方法,创建或获取一个 AbsCache 实例。
  2. 缓存数据:调用缓存实例的 put() 方法,将数据存储到缓存中。
  3. 获取缓存数据:使用 get() 方法,从缓存中获取数据。
  4. 移除缓存数据:通过 remove() 方法,删除缓存中的某个键值对。

2. 使用示例 ​

java
    CacheFactory cacheFactory = TioBootServer.me().getServerTioConfig().getCacheFactory();
    AbsCache absCache = cacheFactory.register("test001",300L,3000L);
    absCache.put("key", "value");

下面是一个简单的示例代码,展示如何向 AbsCache 缓存系统添加数据

解释: ​

  1. 缓存工厂获取:通过 TioBootServer 获取 CacheFactory 实例.
  2. 添加缓存名称:通过register,添加缓存名称到缓存系统。
  3. 添加缓存数据:通过 absCache 添加数据。

3. 实现类 ConcurrentMapCache ​

ConcurrentMapCache 是 AbsCache 的一个具体实现,它使用 ConcurrentHashMap 来保存缓存数据,并提供 TTL 和 TTI 的过期机制。

主要功能: ​

  • 数据存储:put() 方法用于将数据存储到缓存,并为缓存项设定过期时间。
  • 数据获取:get() 方法用于从缓存中获取数据,并更新 TTI 时间。
  • 数据删除:remove() 方法用于删除缓存项,并调用删除监听器。

缓存的有效截止时间取 TTL 截止时间与 TTI 截止时间中较早的一个。get() 只延长尚未过期条目的 TTI,不突破从本次 put() 开始计算的 TTL 上限;读取已经过期的条目返回 null,不会续活。

java
import nexus.io.tio.utils.cache.mapcache.ConcurrentMapCache;
import nexus.io.tio.utils.cache.mapcache.ConcurrentMapCacheFactory;

public class CacheExample {
  public void cacheSession() {
    ConcurrentMapCache cache = ConcurrentMapCacheFactory.INSTANCE.register("session-example", 3600L, 600L);
    cache.put("session-id", "session-data");
    String value = cache.get("session-id", String.class);
    long remainingMillis = cache.ttl("session-id");
  }
}

以上条目最多保存一小时,连续十分钟没有读取也会过期。仅需要空闲过期时可将 TTL 传为 null;两者都为 null 表示不设置过期时间。ttl() 返回剩余毫秒数,条目不存在或没有截止时间时返回 -1。ttl()、size() 和键集合查询不会刷新 TTI。

缓存使用共享守护线程安排清理,TTI 延后时会在原任务触发后重新安排清理;覆盖、删除或清空条目会取消旧任务。同名新条目有独立的过期状态,旧任务不会删除新值。读取也会检查截止时间,保证后台清理稍有延迟时不返回过期值。永久条目不安排定时清理。

TTL 和 TTI 到期都以 RemovalCause.EXPIRED 通知移除监听器,显式删除使用 EXPLICIT。监听器在缓存锁之外执行,应保持简短,避免阻塞清理线程。写入和删除请使用缓存 API;asMap() 和键集合保留底层视图,直接修改视图不会完整维护过期元数据。

4. 使用场景 ​

AbsCache 适用于以下场景:

  • 短期缓存:需要快速访问的数据,例如用户会话信息、临时配置等。
  • 限制过期时间:当需要在一定时间内缓存数据,且希望在不使用后自动过期。
  • 轻量级缓存系统:AbsCache 的实现轻便、易于集成,非常适合内存敏感的场景。

获取所有缓存数据 ​

IndexController 示例

java
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import com.jfinal.kit.Kv;
import nexus.io.annotation.RequestPath;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.utils.cache.AbsCache;
import nexus.io.tio.utils.cache.CacheFactory;

@RequestPath("/internal")
public class InternalController {

  @RequestPath()
  public RespBodyVo cache() {
    CacheFactory cacheFactory = TioBootServer.me().getServerTioConfig().getCacheFactory();
    Map<String, ? extends AbsCache> map = cacheFactory.getMap();

    Kv kv = Kv.by("cacheFactory", cacheFactory);
    kv.set("map", map);

    for (Map.Entry<String, ? extends AbsCache> entry : map.entrySet()) {
      String cacheName = entry.getKey();
      AbsCache cache = entry.getValue();
      Collection<String> keysCollection = cache.keysCollection();
      Map<String, Object> cacheMap = new HashMap<>();
      for (String key : keysCollection) {
        cacheMap.put(key, cache.get(key));
      }
      kv.set("cache_" + cacheName, cacheMap);
    }

    return RespBodyVo.ok(kv);
  }
}

返回数据格式示例

下面是一个返回的 JSON 数据格式示例:

json
{
  "data": {
    "cache_TIO_HTTP_STATIC_RES_CONTENT": {},
    "cacheFactory": "INSTANCE",
    "cache_nexus.io.tio.utils.lock.LockUtilsOBJ": {},
    "cache_nexus.io.tio.utils.lock.LockUtilsRW": {
      "StrCache:getLowercase-606089688": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes653303108": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getLowercase-1844712829": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes3151881": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getLowercase1217813246": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes3178825": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getLowercase1577755768": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes1520117783": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "_tio_ips__0:0:0:0:0:0:0:1": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes78": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes-579138113": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes1381242263": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes-227623528": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getLowercase12634714": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getLowercase12565974": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getLowercase-1399754748": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getLowercase-129587587": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes100245": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes-1452090263": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes1519667058": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getLowercase12115249": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getLowercase-2040128046": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes-1610277289": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getLowercase2024076932": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getLowercase-1580933225": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes78355": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes-1450561559": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getLowercase2255304": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes-1383386683": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes-640121764": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getLowercase-1099743112": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes1749280625": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes1519944307": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes-1368734941": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getLowercase1955373352": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getLowercase12392498": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      },
      "StrCache:getBytes1520186523": {
        "queueLength": 0,
        "fair": false,
        "readHoldCount": 0,
        "readLockCount": 0,
        "writeHoldCount": 0,
        "writeLocked": false,
        "writeLockedByCurrentThread": false
      }
    },
    "cache_TIO_HTTP_SESSIONRATELIMITER_CACHENAME": {
      "/?d87d6737327c4ee680f1213c67465403": {
        "path": "/",
        "lastAccessTime": "0",
        "accessCount": 0
      }
    },
    "cache_TIO_IP_BLACK_LIST__global__": {},
    "cache_TIO_IP_STAT_null_300": {
      "0:0:0:0:0:0:0:1": {
        "durationType": "300",
        "duration": "203",
        "ip": "0:0:0:0:0:0:0:1",
        "handledPackets": 0,
        "handledPacketCosts": 0,
        "receivedPackets": 1,
        "handledCostsPerPacket": 0,
        "packetsPerTcpReceive": 1,
        "bytesPerTcpReceive": 1469,
        "handledBytes": 0,
        "sentPackets": 0,
        "receivedTcps": 1,
        "receivedBytes": 1469,
        "sentBytes": 0,
        "requestCount": 2,
        "start": "2024-09-08 22:16:41",
        "decodeErrorCount": 0,
        "formatedDuration": "203毫秒"
      }
    },
    "map": {
      "nexus.io.tio.utils.lock.LockUtilsOBJ": {
        "cacheName": "nexus.io.tio.utils.lock.LockUtilsOBJ",
        "timeToIdleSeconds": "3600",
        "timeToLiveSeconds": null
      },
      "TIO_HTTP_SESSIONRATELIMITER_CACHENAME": {
        "cacheName": "TIO_HTTP_SESSIONRATELIMITER_CACHENAME",
        "timeToIdleSeconds": null,
        "timeToLiveSeconds": "60"
      },
      "tio-h-s": {
        "cacheName": "tio-h-s",
        "timeToIdleSeconds": "1800",
        "timeToLiveSeconds": null
      },
      "TIO_IP_STAT_null_300": {
        "cacheName": "TIO_IP_STAT_null_300",
        "timeToIdleSeconds": null,
        "timeToLiveSeconds": "300"
      },
      "TIO_HTTP_STATIC_RES_CONTENT": {
        "cacheName": "TIO_HTTP_STATIC_RES_CONTENT",
        "timeToIdleSeconds": null,
        "timeToLiveSeconds": "600"
      },
      "nexus.io.tio.utils.lock.LockUtilsRW": {
        "cacheName": "nexus.io.tio.utils.lock.LockUtilsRW",
        "timeToIdleSeconds": "3600",
        "timeToLiveSeconds": null
      },
      "TIO_IP_BLACK_LIST__global__": {
        "cacheName": "TIO_IP_BLACK_LIST__global__",
        "timeToIdleSeconds": null,
        "timeToLiveSeconds": "10368000"
      }
    },
    "cache_tio-h-s": {}
  },
  "ok": true,
  "code": 1,
  "msg": null
}

使用 Caffeine 作为内置存储 ​

当配置开启 session 时,tio-boot 会将浏览器的 session 信息存储到默认的 ConcurrentMapCache 中。需要更丰富的容量控制和缓存策略时,可以选择 Caffeine。

只需添加以下依赖,tio-boot 在启动时如果检测到 com.github.benmanes.caffeine.cache.LoadingCache 存在,会自动启用 CaffeineCacheFactory:

xml
<dependency>
  <groupId>com.github.ben-manes.caffeine</groupId>
  <artifactId>caffeine</artifactId>
  <version>2.9.3</version>
</dependency>

开启 session 功能

server.session.enable=true

总结 ​

Tio-boot 内置的缓存系统 AbsCache 提供了灵活且易用的缓存管理方式。通过 CacheFactory 注册并管理缓存实例,我们可以轻松实现高效的缓存机制,减少频繁数据重复计算。对于有 TTL 和 TTI 要求的场景,AbsCache 能帮助我们自动管理缓存条目的过期,提升系统性能。

按过期策略获取缓存 ​

CacheUtils.getCaffeineCache(ttl, tti) 根据完整的 TTL 和 TTI 组合复用缓存。参数完全相同时返回同一实例;TTL 相同但 TTI 不同,或一组未设置 TTI 时,使用不同实例,数据不会因为自动命名相同而混用。

java
import nexus.io.tio.utils.cache.CacheUtils;
import nexus.io.tio.utils.cache.caffeine.CaffeineCache;

public class CachePolicyExample {
  public void store() {
    CaffeineCache shortIdle = CacheUtils.getCaffeineCache(3600L, 60L);
    CaffeineCache longIdle = CacheUtils.getCaffeineCache(3600L, 600L);
    shortIdle.put("status", "short");
    longIdle.put("status", "long");
  }
}

TTL 与 TTI 都为空时,该自动获取接口仍会拒绝调用。单独指定 TTL 或 TTI 的自动命名规则保持兼容;显式使用缓存工厂按名称注册时,同名注册继续复用已有实例,不会用后续参数重新配置它。Caffeine 工厂支持并发注册和查询。

缓存空结果 ​

对于查询不到的数据,可以使用 CacheUtils.get(cache, key, true, creator) 暂存空结果,减少同一个不存在的键反复访问数据源。内置 AbsCache 实现通过临时占位对象区分“未缓存”和“已经查询过但没有结果”;业务调用仍然得到 null,不会收到占位对象。

java
import nexus.io.tio.utils.cache.CacheUtils;
import nexus.io.tio.utils.cache.caffeine.CaffeineCache;
import nexus.io.tio.utils.cache.caffeine.CaffeineCacheFactory;

public class MissingValueCacheExample {
  public String demonstrate() {
    CaffeineCache cache = CaffeineCacheFactory.INSTANCE.register("lookup-results", 300L, null);
    String missing = CacheUtils.get(cache, "item:42", true, () -> null);
    // The loader is skipped while the temporary null entry is present.
    String stillMissing = CacheUtils.get(cache, "item:42", true, () -> "created");
    cache.remove("item:42");
    return CacheUtils.get(cache, "item:42", true, () -> "created");
  }
}

临时条目的有效期由缓存实现的 putTemporary 策略决定,不等同于普通条目的 TTL。临时条目失效或被删除后,可以再次加载;明确知道数据已创建时可主动删除对应键。传入 false 时,不保存新的空结果。自定义 ICache 如果在读取时隐藏空占位对象,需要自行提供相应的空结果命中能力。

缓存加载的隔离与异常 ​

缓存加载协调键使用带长度边界的缓存名称与条目键,分别命名的缓存及其条目保持独立。相同缓存、相同键的请求仍共享加载协调机制。

加载函数返回 null 表示无结果;加载失败则向执行加载的调用方传播异常。RuntimeException 保持原异常对象,受检异常通过 IllegalStateException 保留为 cause。失败不写入空结果占位,后续请求可以再次尝试加载。业务可在调用边界统一处理重试、降级或错误响应。

MIT Licensed