工具

连接缓存概念

qianmoQqianmoQ· 更新于 2026-10-01· 阅读 13 分钟· 0 次阅读

登录后可跨设备保存划线和私人笔记登录

在某些应用中,代码的多个部分可能都需要访问 PLC 连接。

与通常的微服务架构不同,对于 PLC,我们不能随意打开任意数量的连接。例如,S7-1200 通常只允许 3 个并发连接。

此外,建立连接的过程本身也可能是相当耗费资源的任务。例如在 ADS 协议中,连接时驱动程序会加载包含 PLC 中定义的所有数据类型描述的表,以及声明了哪些变量已定义、它们的地址是什么、引用了什么数据类型、在 PLC 内存中位于何处的符号表。

即使只有一段代码反复访问 PLC,每次都会新建连接也会给 PLC 和网络带来过大的负载。

使用连接缓存时,多段代码可以并行使用它。不过,在同一时刻只有一段代码可以访问某个连接。

因此,第一个请求新连接的线程会让缓存创建一个新连接并将其返回给客户端。之后,它可以像使用从基础的 PlcConnectionFactory 获取的任何普通连接一样使用它。不过主要区别在于,一旦客户端在这个所谓的连接租约上调用 close(),连接并不会被关闭,而是由缓存将其放回存储中,等待下一个线程来请求它。

如果一个线程请求的连接当前正被另一个线程租用,那么请求线程将等待,直到该连接被归还,然后立即继续使用该连接,直到再将其归还给缓存。

不过,如果第二个线程请求的是另一个连接(具有不同的连接字符串),那么连接缓存会创建一个新连接并立即返回它。

使用连接缓存时,不应长时间占用连接租约。因此,连接缓存会跟踪它发放出去的租约,并终止那些长时间未归还的连接租约。

下面是一个使用连接缓存的示例应用:

    public static void main(String[] args) throws Exception {
        PlcConnectionCache connectionCache = PlcConnectionCache.getBuilder()
            .withConnectionFactory(PlcDriverManager.getDefault().getConnectionFactory())
            .build();
        for (int i = 0; i < 10000; i++) {
            try(PlcConnection connection = connectionCache.getConnection("s7://192.168.1.192")) {
                if (connection.isConnected()){
                    PlcReadRequest.Builder builder = connection.readRequestBuilder();
                    builder.addTagAddress("PollingValue", "%DB1:4.0:BOOL");
                    PlcReadRequest readRequest = builder.build();
                    PlcReadResponse syncResponse = readRequest.execute().get(2000, TimeUnit.MILLISECONDS);
                    printResponse(syncResponse);
                } else {
                    logger.info("PLC is not connected, let's try again to connect");
                    connection.connect();
                }
            } catch (PlcConnectionException e){
                logger.error("Connection exception in trying to connect", e);
            } catch (CancellationException e){
                logger.error("Polling Thread canceled", e);
            } catch (IllegalStateException e){
                logger.error("Connection was in an unexpected state", e);
            } catch (ExecutionException e){
                logger.error("Interrupted Exception fired", e);
            } catch (TimeoutException e) {
                logger.error("Timeout exception fired", e);
            }
            TimeUnit.MILLISECONDS.sleep(100);
        }
        System.exit(0);
    }

要使用连接缓存,你需要向 plc4j-tools-connection-cache 模块添加一个依赖。

    <dependency>
      <groupId>org.apache.plc4x</groupId>
      <artifactId>plc4j-tools-connection-cache</artifactId>
      <version>1.0.0</version>
    </dependency>

在这段代码片段中,有一些值得强调的注意事项。

  • 在近期版本的 PLC4X 中,我们对 PlcDriverManager 进行了重构,以提供对 PlcConnectionFactory 接口的访问。该接口包含所有与创建连接相关的方法。ConnectionCache 通过 PlcConnectionManager 实现了同一个接口——它增加了缓存所需的 close() 方法——因此,你可以在任何可以使用普通 PlcConnectionFactory 的地方使用 PlcConnectionCache。
  • 一个新的 PlcConnectionCache 通常通过构建器创建,可通过 PlcConnectionCache.getBuilder() 访问。下一章将对此做更详细的说明。
  • 缓存始终需要一个连接来源,因此 withConnectionFactory() 是必需的;如果没有它,build() 会抛出 IllegalStateException。通常这是驱动管理器,但任何 PlcConnectionFactory 都可以。
  • try-with-resources 语句(即 try (PlcConnection connection = connectionCache.getConnection(connectionString)))确保租用的连接在使用完毕后会自动归还缓存。如前所述,如果应用程序持有连接的时间过长,超过可配置的一段时间后,该连接会被缓存自动关闭,线程便不能再使用它(即 maxLeaseTime 参数默认为 1 分钟,并且可以配置——详见下一章关于配置连接缓存的内容)。
  • 在发放一个空闲时间超过 idlePingThreshold 的连接租约之前,连接缓存会对该连接执行 ping() 操作,以检查它是否仍然有效。如果检查失败,缓存将终止该连接,建立一个新连接,然后返回该新连接的句柄。最近使用过的连接则不会经过这次额外的往返而直接发放。

配置 PlcConnectionCache

如前所述,PlcConnectionCache 是可配置的。这主要涉及配置超时时间。

所有超时时间都以 long 值的形式设置,并配以 java.util.concurrent.TimeUnit。

| 名称 | 类型 | 默认值 | 说明 |

|
|-------------------|--------------------------|------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
| connectionFactory | PlcConnectionFactory | (必填) | 缓存用于创建连接的来源——通常是 PlcDriverManager.getDefault().getConnectionFactory()。缺少它时 build() 会抛出一个 IllegalStateException。 |
| maxIdleTime | long + TimeUnit | 5 minutes | 缓存连接在被关闭并从池中移除之前允许闲置的时间。下一次对该连接串的请求将建立一个全新连接。 |
| maxLeaseTime | long + TimeUnit | 1 minute | 线程可以持有一个连接租约的时间,超过后连接缓存将终止该租约。此设置用于防止忘记释放租约的客户端。设置为 0 可禁用。 |
| maxWaitTime | long + TimeUnit | 30 seconds | 请求连接的线程在连接缓存放弃并抛出 PlcConnectionException 之前允许等待的时间。 |
| idlePingThreshold | long + TimeUnit | 30 seconds | 连接在被交付使用前需要闲置多久,缓存才会通过一次 ping() 对其进行验证。最近使用过的连接无需经过该往返验证即可直接交付。 |
| pingTimeout | long + TimeUnit | 5 seconds | 等待此类验证 ping() 响应的时间,超过后即判定该连接已失效。 |
| closeTimeout | long + TimeUnit | 5 seconds | 关闭底层连接的时间上限。否则一个卡死的套接字可能会阻塞该连接上的所有操作;超过此时间后,关闭操作将被交由一个后台守护线程执行,缓存随之恢复。设置为 0 可无限等待。 |
| scheduler | ScheduledExecutorService | 2 个守护线程 | 运行闲置 / 租约 / 等待 / 验证超时任务的调度器。可提供你自己的调度器,以便在多个缓存之间共享。请注意,缓存在关闭时会关闭所得到的调度器。 |

一个 PlcConnectionCache 的配置在创建实例时完成,使用与上述名称对应的构建器方法。

下面是一个示例:

    public static void main(String[] args) throws Exception {
        PlcConnectionCache connectionCache = PlcConnectionCache.getBuilder()
            .withConnectionFactory(PlcDriverManager.getDefault().getConnectionFactory())
            .withMaxLeaseTime(10, TimeUnit.SECONDS)
            .withMaxWaitTime(1, TimeUnit.MINUTES)
            .build();

        ...

    }

关闭缓存

缓存会保留它发放出去的连接,因此总得有人把它们再释放回来。close() 正是为此而设:它会关闭调度器,关闭每一条被缓存的连接,并清空连接池。之后缓存便不可再用——任何后续的 getConnection() 都会以 PlcConnectionCacheClosedException 失败——而对一个已经关闭的缓存再次执行关闭则不会有任何效果。

PlcConnectionCache 实现了 PlcConnectionManager,后者又继承自 AutoCloseable,因此它可以作为 try-with-resources 的资源使用,也可以接入你的框架所提供的任意销毁回调中:

    try (PlcConnectionCache connectionCache = PlcConnectionCache.getBuilder()
            .withConnectionFactory(PlcDriverManager.getDefault().getConnectionFactory())
            .build()) {
        ...
    } // all cached connections are closed here

单个连接也可以从池中移除,而不必关闭整个缓存,这可以通过 removeCachedConnection(connectionString) 实现。用于监控时,getCachedConnectionCount() 和 getActiveLeaseCount() 会报告缓存中当前持有的连接数量,以及其中已被租借出去的连接数量。

订阅与缓存连接

订阅是与连接绑定的,如果缓存在已订阅的客户端下方悄悄更换了连接,就会丢失这些订阅。因此,连接缓存会跟踪通过租借连接注册的订阅和事件监听器,并在不得不重新创建连接时——例如在验证 ping 失败之后——在新连接上重新建立它们。从客户端的角度来看,它的订阅只是继续接收事件而已。

评论

登录后参与评论

正在加载评论…