缓存允许应用程序将数据库查询和 API 调用返回的数据副本存储在客户端内存中。这可以提高应用程序的性能,因为应用程序无需在将来的请求中重新查询数据库以获取相同的数据。
客户端请求数据时,会向应用服务器上运行的方法发送请求。应用服务器处理该请求,从数据库获取数据,然后将其转发给客户端。
禁用缓存后,每个对象请求都需要调用数据库。启用缓存后,客户端会检查对象是否在其内存中。如果在,则客户端会重用内存中的对象。否则,客户端会从数据库获取对象,然后将其存储在内存中。
使用过滤器检索对象时,过滤器始终会访问数据库以获取对象。这与通过调用 get<Object>ByName(...) API 检索对象不同,后者会从缓存中返回对象,而缓存不一定使用数据库中的最新数据进行刷新。但是,运行过滤器会导致缓存使用过滤器结果刷新已缓存的对象。
所有构建时对象(即任何可在 Process Designer 中修改的对象)和可导出的 ATRow 对象(dataManagementType 为 EXPORTABLE 的 AT 定义)都会被缓存。如果对象有子对象,则在检索父对象时,子对象会与父对象一起缓存。默认情况下,应用程序不会缓存以下运行时对象,但可以通过编程方式为它们启用缓存。
- Box
- Lot
- Unit
- WorkOrder(及其关联的 OrderItem 对象)
在大多数情况下,使用缓存对象与使用未缓存对象相同。当执行需要数据的操作时,应用程序首先检查数据是否在内存中,如果不在,则执行数据库查询。当应用程序执行事务时,如果该对象启用了缓存,它会始终刷新执行事务的客户端上的缓存数据。
一、缓存时长
Plant Operations仅在缓存中保留每种对象类型的 100 个最近使用的对象。此限制不适用于forms和subroutines。可缓存的forms和subroutines数量没有限制。例如,如果在应用程序中获取 Account001、Account002、... 和 Account100,那么它们都将在缓存中。当获取 Account101 时,应用程序会从缓存中移除 Account001,并将 Account101 置于缓存顶部。如果随后获取 Equipment001,应用程序会将该对象添加到单独的缓存中。这不会影响缓存的帐户,因为每种对象类型都是单独缓存的。
Plant Operations会在整个会话期间保留缓存数据。这允许多个用户在同一会话期间在多个表单中重复使用缓存的数据。每个 Plant Operations 实例都有单独的缓存。
二、应用服务器缓存
应用服务器还会缓存影响 Plant Operations 客户端的信息,具体方式如下: 数据库配置信息:应用服务器缓存数据库配置设置,例如连接信息和站点 ID。如果这些信息发生变化,必须重启应用服务器。这将清除缓存,并确保您的应用程序使用正确的配置设置。
对象:应用服务器缓存对象(例如Route和Production Line)及其关联对象(例如Route Step和Work Center),以提高性能。当客户端使用这些对象执行事务时,应用服务器会在处理之前确保缓存的对象与数据库中的对象匹配。应用程序设计人员不受此缓存的影响,无需执行任何任务来确保对象未过时。
UDA 定义:每个应用服务器都会缓存 UDA 定义,并每 30 分钟运行一次计划任务来刷新缓存的定义。如果有多个应用服务器,并且想要强制其中一个刷新其定义,则必须重新启动该应用服务器。如果只有一台应用服务器访问生产数据库,则此缓存不会对您造成影响。
三、缓存注意事项
启用缓存可以提高性能,因为它可以减少对数据库的查询次数。但是,缓存可能会导致数据新鲜度问题,因为数据库无法将更改传播到包含过时数据的客户端。在决定是否在应用程序中使用缓存时,请考虑以下因素:
客户端多久会重用一个对象?如果客户端不重用对象,那么缓存对象数据就没有任何好处。例如,装配站可能会取回一个单元并对其执行事务。如果该单元不会返回装配站,那么就没有理由缓存该单元。
应用程序的事务负载有多大?如果应用程序向数据库发出大量信息请求,那么启用缓存可以减少应用程序对数据库的负载。
数据更改的频率是多少?缓存在数据不频繁更改的应用程序中效果最佳。
多个客户端访问同一个对象的频率是多少?如果多个客户端同时访问一个对象,并且至少有一个客户端正在更改该对象,那么缓存可能会导致客户端使用过时的数据。
客户端系统上有多少内存可用于缓存?应用程序将数据缓存到客户端系统的内存中。如果客户端系统没有足够的内存来缓存数据,那么应用程序将需要更频繁地重新查询数据库。
四、使用分布式事件的 Web 应用程序缓存
Process Designer, Shop Operations, Shop Operations Server (SOS)和 Web 应用程序(例如Quality Management)会缓存构建时对象,以提高性能并最大程度地减少同一对象的两个实例同时存在于应用程序中的可能性。Shop Operations和 SOS 的每个实例都有各自的对象缓存,因此每个用户都有各自的缓存(车间操作和 SOS 本质上是单用户应用程序)。Web 应用程序在每个 servlet 实例中维护一个缓存。因此,非集群解决方案只有一个缓存实例。
请注意 FTPC 应用程序执行的以下缓存行为:
- 当应用程序按名称或键检索对象时,会先搜索缓存,然后再访问数据库。如果对象不在缓存中,则会从数据库中检索并放入缓存中。
- 当使用过滤器检索对象时,会从数据库中检索对象并放入缓存中。
- 无论通过何种机制从数据库检索对象,如果对象尚未在缓存中,都会将其放入缓存中。如果对象实例当前在缓存中,则会刷新缓存版本。
- 对象一旦进入缓存,通常会一直保留,直到应用程序明确移除或对象老化并被缓存自动移除。
默认情况下,Web 应用程序 API 不支持应用程序显式移除对象。因此,当构建时对象在执行更改的 FTPC 应用程序的数据库和缓存中更新时,它不会在其他应用程序实例(例如 Web 应用程序实例)的缓存中更新。
为了确保构建时对象在 Web 应用程序缓存中得到更新,可以将中间层配置为在每次保存或删除顶级构建时对象时,使用分布式事件基础架构向所有客户端广播事件。为此,必须在 FTPC 管理器中进行以下配置:
- 如果当前未选中“启用键控对象更改事件”属性,请选中该属性。“启用键控对象更改事件”属性默认处于启用状态。
- 通过定义 JMS 服务器 URL 属性来定义 ActiveMQ URL。请确保 ActiveMQ 已安装并正在运行。
启用并定义这些属性后,对象缓存基础架构会监听这些新事件,并自动从缓存中删除已更改的对象。下次通过缓存访问该对象(通过名称或键)时,将找不到该对象,而是访问数据库(已更新或已删除)的版本。
由于应用程序表可能包含构建时数据或运行时数据,因此 FTPC 事件系统需要识别哪些应用程序表包含构建时数据。要将表定义为包含构建时数据,请将 dataManagementType 设置为 TYPE_EXPORTABLE。Exportable 标志用于确定应用程序表是否应被视为构建时对象。请确保正确设置此标志,以确保事件和缓存管理系统正常工作。
请注意以下限制:
虽然应用程序可以在查看Form或页面时缓存对象(例如,使用过滤器填充网格或表格),并在关闭Form或页面时清除缓存,但在某些情况下,应用程序需要采取一些明确的操作来清除自身的缓存。
在某些情况下,构建时对象会作为临时对象存储在其他系统对象中。本地存储的系统对象不会使用上述机制进行清除。
五、启用运行时对象(Runtime Object)缓存
要在应用程序中缓存Box、Lot、Unit和WorkOrder运行时对象,请在应用程序开头或表单的 initForm 事件中添加以下方法:
- enableRuntimeObjectCaching()
要禁用缓存,请使用以下方法:
- disableRuntimeObjectCaching()
默认情况下,应用程序在运行时检索数据后不会缓存数据。只需在启用缓存后使用此方法禁用缓存或清除当前应用程序的缓存即可。
六、管理缓存对象
可以使用以下方法强制应用程序查询数据库而不是使用缓存数据:
- clear<object>Cache():从缓存中移除所有 <object> 实例。
- <object>.unload():从缓存中移除一个 <object> 实例。对于父对象,unload() 方法带有一个布尔参数。如果将 true 传递给该方法,则应用将卸载所有子对象。
- <object>.refresh():查询数据库,并用数据库中的数据替换当前缓存的数据。
- RuntimeDCS.clearCache():从缓存中移除 RuntimeDCS 维护的所有 DCInstance 对象。
以下示例演示了对 Order 对象的方法调用。可以将这些示例推广到其他运行时对象。
获取缓存数据
获取缓存数据允许访问缓存中的对象或对象信息,但不允许应用程序查询数据库。
Functions 类中提供了以下方法,它们返回当前已缓存的某种类型的对象的向量:
getLoadedBoxes() getLoadedCarriers() getLoadedLocations() | getLoadedLots() getLoadedOperations() getLoadedRoutes() | getLoadedUnits() getLoadedUsers() getLoadedWorkOrders() |
要从缓存中获取某个对象类型的所有实例,请调用 getLoaded<object>() 方法,其中 <object> 是可用的对象类型。例如:
vecUsers = getLoadedUsers()
Functions 类中有以下方法可用,并返回字符串数组:
getLoadedBoxNames() getLoadedCarrierNames() getLoadedLocationNames() | getLoadedLotNames() getLoadedRouteNames() | getLoadedUnitSerialNumbers() getLoadedWorkOrderNumbers() |
以下语句将从缓存中的Unit中获取所有序列号:
arrSNs = getLoadedUnitSerialNumbers()
Order 类中有以下方法可用,并返回手数或单位的向量:
getLoadedLots() | getLoadedUnits() | getLotsIfLoaded() |
Lot 类中有以下方法可用,并返回一个单位向量:
getLoadedUnits() | getUnitsIfLoaded() |
删除缓存数据
删除缓存数据会强制应用程序在下次需要数据时查询数据库。
在应用程序将数据保存到数据库之前,更改会保留在缓存中。从缓存中删除的对象中所有未保存的数据都将丢失。
要从缓存中移除某个对象类型的实例,请调用 unload() 方法。例如:
ovenEquipment = getEquipmentByName("Oven1")
// perform tasks on ovenEquipment
ovenEquipment.unload()
以下语句将从缓存中卸载 OrderU 以及订单中的任何Lot和Unit:
customerOrder = getWorkOrder("OrderU")
// perform tasks on customerOrder
customerOrder.unload(true)
要从缓存中移除某个对象类型的所有实例,请调用 clear<object>Cache() 方法,其中 <object> 是运行时对象类型。例如,
clearWorkOrderCache()
要从应用程序中移除所有缓存的Box、Lot、Unit和Work Order数据,请禁用缓存。如果想在移除缓存后继续缓存数据,请启用缓存。例如,
disableRuntimeObjectCaching() // removes all cached data
enableRuntimeObjectCaching() // application will cache new data
调用
disableRuntimeObjectCaching 只会删除boxes、lots、units和work orders的缓存数据。
刷新缓存数据
刷新缓存数据允许使用数据库中最新的数据副本更新缓存。
在应用程序将数据保存到数据库之前,更改会保留在缓存中。刷新对象后,所有未保存的数据都会丢失。
要刷新对象类型的实例,请调用 refresh() 方法。例如,
customerOrder = getWorkOrder("OrderU")
// perform tasks on customerOrder
customerOrder.refresh()
Order和Lot对象各自都有一个刷新方法,用于刷新其关联的缓存数据:
- Order.refreshLotsIfLoaded():如果缓存了与Order关联的Lot,则可以调用此方法刷新已加载的Lot。
- Lot.refreshLoadedUnits():如果缓存了与Lot关联的Unit,则可以调用此方法刷新已加载的Unit。
七、缓存数据策略
尽管缓存在数据不频繁更改的环境中效果最佳,但可以使用以下技术创建有效缓存数据的应用程序。由于并非所有应用程序都适合缓存,因此应用程序设计人员必须决定应用程序是否应该缓存数据。
保存前确保数据是最新的
如果两个或多个用户在其应用程序中访问同一个对象,则存在这样的风险:他们都会在不知道其对象是否为最新版本的情况下尝试更改对象。为了避免此问题,可以在检索对象时保存其时间戳,然后在保存更改之前将该时间戳与当前时间戳进行比较。
考虑以下子例程,该子例程使用订单 Order1 中的商品填充列表框 lbItems。
objOrder = getWorkOrder("Order1")
setProperty("lastRev", objOrder.getLastModifiedTime())
lbItems.removeAll()
orderItemList = objOrder.getOrderItems()
foreach item ( orderItemList.elements() ) {
index = lbItems.addItem(item)
}
这会将订单的上次修改时间保存到 lastRev 属性中。保存更改之前,请测试缓存数据是否已过期。例如:
objOrder = getWorkOrder("Order1")
// Refresh the order to get its timestamp.
objOrder.refresh()
// Get cached and database timestamps.
orderCurrentTimeStamp = objOrder.getLastModifiedTime()
orderCachedTimeStamp = getProperty("lastRev")
// Test if data is currently cached.
isOrderCached = orderCachedTimeStamp != null
// Compare cached timestamp to database time stamp.
isOrderStale = orderCurrentTimeStamp.compareTo(orderCachedTimeStamp) == 1
if ( isOrderCached && isOrderStale ) {
// The cached data is stale. Prompt the user for action.
} else {
// The cached data is current. Save changes.
}
锁定对象
除了在用户保存对象之前比较时间戳之外,您还可以将数据保存到对象的 UDA 中,并将其用作锁定机制。以下示例展示了此技术,允许操作员将其用户名保存为第一个 UDA 属性来锁定对象。
为了锁定对象,应用程序运行以下子例程:
objOrder = getWorkOrder("Order1")
objOrder.refresh() // refresh the order before saving
lockingUser = objOrder.getUDA(0) // get the locking user from the UDA
cUser = getCurrentUser().getName() // get the current user
if ( lockingUser == null || lockingUser.equals(cUser) ) {
// safe to lock object
objOrder.setUDA(cUser, 0) // set UDA[0] to the current user name
objOrder.save()
} else {
// The object is locked. Prompt the user for action.
}
此子例程将当前用户的名称保存为第一个 UDA 属性。在更新对象之前,它会进行测试以确保其他用户未锁定该对象。同样,以下子例程会清除第一个 UDA 属性以解锁对象:
objOrder = getWorkOrder("Order1")
objOrder.refresh() // refresh the order before saving
lockingUser = objOrder.getUDA(0) // get the locking user from the UDA
cUser = getCurrentUser().getName() // get the current user
if ( lockingUser == null || lockingUser.equals(cUser) ) {
// safe to unlock object
objOrder.setUDA(null, 0) // clear UDA[0]
objOrder.save()
} else {
// The object is locked. Prompt the user for action.
}
当用户尝试更新对象时,以下子例程将运行。它确保在保存更改之前,对象已被当前用户解锁或锁定。
objOrder = getWorkOrder("Order1")
objOrder.refresh() // refresh the order before saving
lockingUser = objOrder.getUDA(0) // get the locking user from the UDA
cUser = getCurrentUser().getName() // get the current user for comparison
if ( lockingUser == null || lockingUser.equals(cUser) ) {
// The object is unlocked. Save changes.
} else {
// The object is locked. Prompt the user for action.
}