内置缓存系统 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 工厂类来管理缓存实例。它可以注册并返回缓存实例,允许开发者根据需要缓存数据。
核心步骤:
- 注册缓存实例:通过
CacheFactory.register()方法,创建或获取一个AbsCache实例。 - 缓存数据:调用缓存实例的
put()方法,将数据存储到缓存中。 - 获取缓存数据:使用
get()方法,从缓存中获取数据。 - 移除缓存数据:通过
remove()方法,删除缓存中的某个键值对。
2. 使用示例
CacheFactory cacheFactory = TioBootServer.me().getServerTioConfig().getCacheFactory();
AbsCache absCache = cacheFactory.register("test001",300L,3000L);
absCache.put("key", "value");
下面是一个简单的示例代码,展示如何向 AbsCache 缓存系统添加数据
解释:
- 缓存工厂获取:通过
TioBootServer获取CacheFactory实例. - 添加缓存名称:通过
register,添加缓存名称到缓存系统。 - 添加缓存数据:通过 absCache 添加数据。
3. 实现类 ConcurrentMapCache
ConcurrentMapCache 是 AbsCache 的一个具体实现,它使用 ConcurrentHashMap 来保存缓存数据,并提供 TTL 和 TTI 的过期机制。
主要功能:
- 数据存储:
put()方法用于将数据存储到缓存,并为缓存项设定过期时间。 - 数据获取:
get()方法用于从缓存中获取数据,并更新 TTI 时间。 - 数据删除:
remove()方法用于删除缓存项,并调用删除监听器。
缓存的有效截止时间取 TTL 截止时间与 TTI 截止时间中较早的一个。get() 只延长尚未过期条目的 TTI,不突破从本次 put() 开始计算的 TTL 上限;读取已经过期的条目返回 null,不会续活。
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 示例
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 数据格式示例:
{
"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:
<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 时,使用不同实例,数据不会因为自动命名相同而混用。
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,不会收到占位对象。
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。失败不写入空结果占位,后续请求可以再次尝试加载。业务可在调用边界统一处理重试、降级或错误响应。
