CacheThing
CacheThing 提供一个用于快速高效存储和检索键值数据的接口。它非常适合缓存成本高昂或非常耗时的操作,例如慢查询。
|
|
CacheThing 是内存缓存,这意味着重新启动 ThingWorx 后,需要重新构建该事物。在高可用性 (HA) 系统中,每个 ThingWorx 节点都有一个独立的缓存,因此,某个缓存条目在一个节点上设置或更新后,不会在其他节点上自动更新。
|
CacheThing 可用于缓存各种类型的数据,包括:
• 对数据库事物进行成本高昂或频繁查询的结果
• 对值流或数据表进行成本高昂或频繁查询的结果
• 慢网络调用的结果,例如,使用 ContentLoaderFunctions 进行的调用
• 成本高昂的计算值
• 来自 FileRepositories 的频繁使用且极小文件的内容
由于 CacheThing 在内存中运行,因此与访问数据库或网络资源等外部系统相比,CacheThing 可更快地提供结果,并且资源消耗更低。
使用指南
不应将 CacheThing 视为永久数据存储。缓存在 CacheThing 中的任何数据都应该能够从原始源重新创建。
CacheThing 中的数据可能
到期或
被逐出。如果缓存返回空结果,则使用 CacheThing 的服务应该能够读取源数据。有关详情,请参阅
示例。
配置
必须使用具有主键的 DataShape 来配置 CacheThing。主键用于从缓存中提取条目。
|
|
允许使用复合主键,但这会妨碍您使用下面列出的 *ByKey 便利服务。
|
到期策略
到期策略决定缓存中每个条目的生存时间 (TTL)。到期时间使用 Cache Entry Expiration Time (Seconds) 设置进行配置。条目到期后,检索值将返回一个空结果,这与已将该值从缓存中删除类似。
允许进行以下配置:
|
缓存条目到期策略
|
说明
|
|
从不
|
系统从不根据缓存条目储存在缓存中的时间来移除该缓存条目。但是,如果缓存达到缓存上限配置,仍可通过逐出的方式或使用 DeleteEntry 服务将其移除。配置 Never 到期策略后,系统将忽略 Cache Entry Expiration Time (Seconds) 设置。
|
|
自上次访问后的到期时间
|
如果未在指定的到期时间内读取条目,则系统会将条目从缓存中移除。
|
|
自创建以来的到期时间
|
在指定的到期时间之后,系统会将条目从缓存中移除。
|
|
自上次修改后的到期时间
|
如果未在给定的到期时间内修改条目,系统会将该条目移除。
|
|
自上次操作后的到期时间
|
如果未在给定的到期时间内访问或修改条目,系统会将该条目移除。
|
缓存上限 (逐出)
如果条目达到在 Cache Maximum Size (MB) 设置中配置的大小,则会自动从缓存中逐出 (删除) 条目。将逐出较旧、最不经常使用 (LFU) 的条目,以便为新条目释放空间。逐出条目后,检索值将返回一个空结果,这与已将该值从缓存中删除类似。
如果将大条目添加到缓存中,则可能会逐出多个小条目以释放空间。
为避免在出现非常大的条目时触发大量较小条目的逐出操作,您可以在将潜在条目添加到缓存之前,使用 EstimateEntrySize 服务检查其大小。
缓存“逐出”和“到期”操作将根据需要独立地从缓存中移除条目。逐出机制基于已配置的缓存大小以及条目与其他条目相比的使用频率。到期机制基于根据配置的到期策略所经过的时间。
全局缓存上限
平台子系统的 Maximum Size Shared between all CacheThings per ThingWorx Node (MB) 配置用于限制所有 CacheThing 的 Cache Maximum Size (MB) 组合大小。此护栏旨在防止因 CacheThing 配置错误而意外消耗 ThingWorx 的所有系统内存。非启用中 (已禁用) CacheThing 不计入此限制。
此配置可防止:
• 创建大小超过此限制的新 CacheThing
• 更新 Cache Maximum Size (MB) 超过此限制的 CacheThing
• 导入大小超过此上限的 CacheThing (作为实体导入或作为扩展的一部分),会导致整个导入失败
• 将大小上限减小到当前所有 CacheThing 的 Cache Maximum Size (MB) 总和以下
• 激活 (启用) 超过全局限制的 CacheThing
更改缓存配置
对缓存配置进行任何更改时,系统都会自动清除缓存以保持内部一致性。编辑下列任一属性都将清除缓存:
• DataShape - 这包括编辑当前配置的 DataShape 本身,例如添加或移除字段。
• Cache Maximum Size (MB)
• Cache Entry Expiration Policy
• Expiration Time (Seconds)
服务
针对 CacheThing 提供了以下服务。在高可用性 (HA) 环境中,它们会影响相应服务运行所在的 ThingWorx 节点上的缓存。
仅当缓存的 DataShape 具有单一主键时,下面列出的 *ByKey 便利服务才可用。如果缓存的 DataShape 配置了多个主键,则使用服务的完整信息表输入版本。
|
服务名称
|
说明
|
|
PutEntry
|
• 向缓存中添加条目。
• 输入值 - 使用为 CacheThing 配置的 DataShape 的单行信息表。
|
|
GetEntry
|
• 从缓存返回单行信息表结果。如果在缓存中未找到该条目 (缓存未命中),则会返回一个空信息表。条目从未添加至缓存、已从缓存中逐出或到期时,可能会发生缓存未命中。
• 输入值 - 使用为 CacheThing 配置的 DataShape 的单行信息表。应仅填充 Primary Key 值。
|
|
GetEntryByKey
|
• 从缓存返回单行信息表结果。如果缓存未命中,则返回空信息表。条目从未添加至缓存、已从缓存中逐出或到期时,可能会发生缓存未命中。
• 输入值 - 正在检索的条目的字符串版本主键。此服务最适合字符串主键,但也适用于大多数具有明确字符串转换的类型。仅当配置的 DataShape 具有单一主键时,才允许使用此服务。
|
|
DeleteEntry
|
• 删除缓存中的条目。
• 输入值 - 使用为 CacheThing 配置的 DataShape 的单行信息表。应仅填充 Primary Key 值。
|
|
DeleteEntryByKey
|
• 删除缓存中的条目。
• 输入值 - 正在检索的条目的字符串版本主键。此服务最适合字符串主键,但也适用于大多数具有明确字符串转换的类型。仅当配置的 DataShape 具有单一主键时,才允许使用此服务。
|
|
PurgeCache
|
移除所有缓存条目。
|
|
GetDataShape
|
返回为缓存配置的 DataShape。
|
|
SetDataShape
|
设置缓存配置表中的 DataShape 字段。更改此值将自动清除缓存。
|
|
GetEstimatedEntryCount
|
获取缓存中估计的条目数。任何正在进行的 PUT 和 DELETE 服务都可能导致计数不准确,运行 Expiration 或 Eviction 进程时也可能出现类似情况。
|
|
EstimateEntrySize
|
• 返回如果将条目添加到缓存中,该条目将使用的大小。此服务不会将条目实际放入缓存中,也不需要缓存中已有的任何条目。此服务主要用于估计适当的 Cache Maximum Size (MB),以及防止将大条目添加到缓存中。
• 输入值 - 使用为 CacheThing 配置的 DataShape 的单行信息表。
|
指标
提供了一些与缓存相关的指标。在高可用性 (HA) 环境中,每个节点的缓存都是独立的。因此,应根据 platform 和 cache_name 标签按节点处理缓存指标。
以下缓存指标可用:
|
缓存指标
|
说明
|
|
thingworx_cache_hit_rate
|
导致在缓存中找到条目的缓存请求比率
|
|
thingworx_cache_hits
|
缓存查找方法返回缓存值的次数
|
|
thingworx_cache_request_count
|
缓存查找方法返回缓存值或未缓存值的次数
|
|
thingworx_cache_miss_rate
|
缓存查找方法返回未缓存 (新加载的) 值或空值的比率
|
|
thingworx_cache_misses
|
缓存查找方法返回未缓存 (新加载的) 值或空值的次数
|
|
thingworx_cache_eviction_count
|
逐出条目的次数
|
|
thingworx_cache_average_load_penalty
|
加载新值所用的平均时间
|
|
thingworx_cache_weighted_size
|
缓存的当前近似大小 (字节)
|
|
thingworx_cache_max_weight
|
缓存在逐出之前的最大大小 (字节)
|
|
thingworx_cache_estimated_entry_count
|
缓存中的预估条目数
|
|
thingworx_cache_global_max_size
|
所有缓存的全局最大配置大小 (字节)
|
告警 (在 PTC 云托管环境中可用)
PTC 云托管环境中可使用以下告警:
• thingworxEntityCacheMissRate - 缓存未命中率持续 1 小时超过 80% 时,发出告警。
• thingworxEntityCacheEvictions - 缓存逐出率持续 30 分钟超过 80% 时,发出告警。
示例和最佳实践
要使用 CacheThing 正确实现服务,需要特别注意细节。
以下是主要考虑因素:
• ThingWorx 节点重新启动时会清除缓存。
• 缓存中的条目可能会在无任何用户输入的情况下到期或被逐出,因此永远不能假设条目可无限期地保留在缓存中。
• 在高可用性 (HA) 环境中,每个节点都有一个独立的备份缓存。在一个节点上添加、删除或清除缓存不会影响所有 ThingWorx 节点。
以下是使用缓存的标准模式:
1. 使用主键检索缓存中的条目。
2. 检查是否已填充结果 (返回的信息表中是否有一行)。
3. 如果缓存中有结果,则使用该结果。
4. 如果缓存中没有结果:
◦ 从源数据 (例如,Database Thing、DataTable、ContentLoader) 提取或生成所需条目。
◦ 将条目放入缓存中。
◦ 返回现在已缓存的条目。
示例:使用 TimesTwo 服务
TimesTwo 服务将获取一个数值,将该数值乘以 2,然后将结果存储在缓存中。要进行此设置,请按照下列步骤操作:
1. 使用以下字段创建 DataShape (TimesTwoDataShape):operand: (LONG, primaryKey) 和 timesTwoValue: (LONG)。
2. 使用配置的 TimesTwoDataShape 创建 CacheThing (MyTimesTwoCache)。
3. 将 TimesTwo 服务添加到 MyTimesTwoCache:
◦ 输入 - x (LONG 数值)
◦ 输出 - result (LONG 数值)
// Input is x as a LONG
// Output is result as LONG
// MyTimesTwoCache CacheThing is configured with a DataShape with two fields:
// operand: (LONG, primaryKey)
// timesTwoValue: (LONG)
result = -1;
// Check the cache first, to see if we have already calculated x*2
// GetEntryByKey is a convenience method that takes the String value the configured DataShape's Primary Key ("key" in this example)
// cacheResult is an InfoTable with the DataShape of the cache
let cacheResult = Things["MyTimesTwoCache"].GetEntryByKey({ operand: x.toString() });
if (cacheResult.getRowCount() === 0) {
// Cache didn't have a result, so calculate it
let timesTwo = x*2;
// Put the result in the cache for the next time we need this multiple
// operand and value are from the Cache's configured DataShape
let cacheInput = getInfoTableForPut(x, timesTwo);
Things["MyTimesTwoCache"].PutEntry({
values: cacheInput
});
// Turn ScriptLogger to debug mode to see the logger messages
logger.debug("PutEntry the following entry into the MyTimesTwoCache: {operand: " + x + ", timesTwoValue: " + timesTwo + "}");
// Set the global result Output variable
result = timesTwo;
} else {
// If results are found, they are always in the first row of the InfoTable
let row = cacheResult.getRow(0);
logger.debug("Found the following entry already in MyTimesTwoCache: {operand: " + row.operand + ", timesTwoValue: " + row.timesTwoValue + "}");
result = row.timesTwoValue;
}
function getInfoTableForPut(operand, timesTwoValue) {
let infoTable = Resources["InfoTableFunctions"].CreateInfoTableFromDataShape({
infoTableName: "InfoTable",
dataShapeName: Things["MyTimesTwoCache"].GetDataShape()
});
infoTable.AddRow({"operand": operand, "timesTwoValue ": timesTwoValue });
return infoTable;
}
示例:使用 DataTableQuery
此示例演示如何使用正在工作的 BetweenDatesDataTableQuery 服务来查找数据表中两个日期之间的最大值,并将结果存储在缓存中。唯一调用 CachingBetweenDatesDataTableQuery 最初会查询备份数据表,但后续相同调用将改为查询缓存。
要进行此设置,请按照下列步骤操作:
1. 使用以下字段创建 DataShape (TimeValueDataShape):longKey: (LONG, primaryKey) 和 dateKey: (DATETIME, primaryKey)。
2. 使用 TimeValueDataShape DataShape 创建 DataTable (DemoDataTable)。
3. 使用以下字段创建 DataShape (TimeSpanValueCacheDataShape):startDate: (DATETIME, primaryKey)、endDate: (DATETIME, primaryKey) 和 longValue: (LONG)。
4. 使用配置的 TimeSpanValueCacheDataShape DataShape 创建 CacheThing (CacheThingDemo)。
5. 针对 CacheThingDemo 创建以下服务:
◦ CachingBetweenDatesDataTableQuery - 输入:StartDate 和 EndDate,使用 DATETIME 格式输出:result,使用 LONG 格式
◦ BetweenDatesDataTableQuery - 输入:StartDate 和 EndDate,使用 DATETIME 格式。输出:result,使用 LONG 格式
| 此服务会自动将演示数据填充到查询日期范围内的数据表中。 |
6. 可选。在 CacheThingDemo 上使用除 Never 以外的 ExpirationPolicy 配置短整型 ExpirationTime,以观察缓存中到期的条目。
CachingBetweenDatesDataTableQuery
/* Reads the Cached results of BetweenDatesDataTableQuery Service.
If Cache doesn't have the result, call BetweenDatesDataTableQuery and load results into Cache.
Service Name: CachingBetweenDatesDataTableQuery
Inputs: StartDate[DATETIME, required], EndDate[DATETIME, required]
Output: LONG
*/
// This example uses CacheThing.GetEntry, which requires a one-row infoTable populated with the CacheThing's DataShapes primaryKey getEntryByKey
let cacheEntryInfoTable = getCacheEntryInfoTable();
cacheEntryInfoTable.AddRow({"startDate": StartDate, "endDate": EndDate});
let getEntryResult = Things["CacheThingDemo"].GetEntry({
values: cacheEntryInfoTable
});
if (getEntryResult.getRowCount() === 0) {
// Cache didn't have results, so go to backing DataTable, by calling the BetweenDatesDataTableQuery Service
let queryResult = Things["CacheThingDemo"].BetweenDatesDataTableQuery({
StartDate: StartDate,
EndDate: EndDate
});
// Turn ScriptLogger to debug mode to see the logger messages
logger.debug("PutEntry called for BetweenDatesQueryCache: {StartDate: " + StartDate + ", EndDate: " + EndDate + " value: " + queryResult + "}");
if (!queryResult || queryResult < 0) {
// BetweenDatesDataTableQuery should always populate with demo data if no data is in the StartDate - EndDate range
throw new Error("BetweenDatesDataTableQuery did not return results");
}
let newCacheEntryInfoTable = getCacheEntryInfoTable();
newCacheEntryInfoTable.AddRow({"startDate": StartDate, "endDate": EndDate, "longValue": queryResult});
Things["CacheThingDemo"].PutEntry({
values: newCacheEntryInfoTable
});
result = queryResult;
} else {
let row = getEntryResult.getRow(0);
logger.debug("Found the following entry already in Cache : {StartDate: " + row.startDate + ", EndDate: " + row.endDate + ", LongValue: " + row.longValue + "}");
result = row.longValue;
}
function getCacheEntryInfoTable() {
return Resources["InfoTableFunctions"].CreateInfoTableFromDataShape({
infoTableName: "InfoTable",
dataShapeName: "TimeSpanValueCacheDataShape"
});
}
BetweenDatesDataTableQuery
/* This service queries a range of dates, based on the dateKey column, and returns the greatest longKey value found.
If there are no entries between StartDate and EndDate this Service dynamically populates demo data into demoDataTable, and returns that result.
Service Name: BetweenDatesDataTableQuery
Inputs: StartDate[DATETIME, required], EndDate[DATETIME, required]
Output: LONG
*/
if (StartDate >= EndDate) {
throw Error("StartDate must be before EndDate");
}
let dataTable = Things["DemoDataTable"];
let queryResult = queryGreatestLongKeyBetweenDates(dataTable, StartDate, EndDate);
if (queryResult.getRowCount() === 0) {
// If we don't get any results, first populate the date range with random values, then retry
// Turn ScriptLogger to debug mode to see the logger messages
logger.debug("No values found between [" + StartDate + "] and [" + EndDate + "], populating 100 demo values and re-querying");
populateDemoDataTable(dataTable, StartDate, EndDate);
// Re-query the now populated DateTable
queryResult = queryGreatestLongKeyBetweenDates(dataTable, StartDate, EndDate);
result = queryResult.getRow(0).get("longKey");
} else {
result = queryResult.getRow(0).get("longKey");
}
// Queries the DataTable for the largest longKey column value between the given dates
// Returns: One-row InfoTable with the found row.
function queryGreatestLongKeyBetweenDates(dateTable, startDate, endDate) {
let greatestBetweenDatesQuery = {
"filters": {
"type": "Between",
"fieldName": "dateKey",
"from": startDate,
"to": endDate
},
"sorts": [{
"fieldName": "longKey",
"isAscending": false
}]
};
// Run the Query to find the greatest Value Between Two Dates
let queryResult = dataTable.QueryDataTableEntries({
maxItems: 1,
query: greatestBetweenDatesQuery,
});
logger.debug("Queried between [" + StartDate + "] and [" + EndDate + "], found:" + queryResult.toJSON());
return queryResult;
}
function populateDemoDataTable(dataTable, startDate, endDate) {
// add 100 random entries between startDate and endDate
for (let entry = 0; entry < 100; entry++) {
let randomTimeBetween = randomDate(startDate, endDate);
let entry = Resources["InfoTableFunctions"].CreateInfoTableFromDataShape({
infoTableName: "InfoTable",
dataShapeName: "TimeValueDataShape"
});
entry.AddRow({
"longKey": Math.floor(Math.random() * 1000),
"dateKey": randomTimeBetween
});
Things["DemoDataTable"].AddDataTableEntry({
values: entry /* INFOTABLE */ ,
});
}
}
function randomDate(startDate, endDate) {
let randomTime = Math.random() * (endDate.getTime() - startDate.getTime()) + startDate.getTime();
return new Date(randomTime);
}