结合Hazelcast和Spring的分布式缓存 - reflectoring


在某些应用程序中,我们需要保护数据库或避免进行成本高昂的计算。我们可以为此目的使用缓存。本文展示了如何在分布式可伸缩应用程序中将Hazelcast用作Spring的缓存。
本文随附GitHub上的工作代码示例。

假设我们有一个Spring Boot应用程序,我们想在该应用程序中使用缓存。但我们也希望能够扩展此应用程序。这意味着,例如,当我们启动应用程序的三个实例时,它们必须共享缓存以保持数据一致。
Hazelcast是一个分布式的内存对象存储,并提供许多功能,包括TTL,直写和可伸缩性。我们可以通过启动网络中的多个Hazelcast节点来构建Hazelcast集群。每个节点称为成员。
我们可以使用Hazelcast实现两种拓扑:

  • 嵌入式缓存拓扑,以及
  • 客户端-服务器拓扑。

让我们看一下如何使用Spring实现每个拓扑。

嵌入式缓存
这种拓扑意味着应用程序的每个实例都有一个集成的成员:

在这种情况下,应用程序和缓存数据在同一节点上运行。当在缓存中写入新的缓存条目时,Hazelcast会负责将其分发给其他成员。从缓存中读取数据时,可以在运行应用程序的同一节点上找到数据。
让我们看一下如何使用嵌入式Hazelcast缓存拓扑和Spring应用程序构建集群。Hazelcast支持许多用于缓存的分布式数据结构。我们将使用Map,因为它提供了众所周知的get 和put操作。
首先,我们必须添加Hazelcast依赖项。Hazelcast只是一个Java库,因此可以很容易地完成(Gradle表示法):

compile group: 'com.hazelcast', name: 'hazelcast', version: '4.0.1'

现在让我们为应用程序创建一个缓存客户端。

@Component
public class CacheClient {

    public static final String CARS = "cars";
    private final HazelcastInstance hazelcastInstance
                            = Hazelcast.newHazelcastInstance();

    public Car put(String number, Car car){
        IMap<String, Car> map = hazelcastInstance.getMap(CARS);
        return map.putIfAbsent(number, car);
    }

    public Car get(String key){
        IMap<String, Car> map = hazelcastInstance.getMap(CARS);
        return map.get(key);
    }
   
   
// other methods omitted

}

现在,应用程序具有分布式缓存。该代码最重要的部分是创建集群成员。它通过调用Hazelcast.newHazelcastInstance()方法来创建。
当我们要扩展应用程序时,每个新实例都将创建一个新成员,并且该成员将自动加入集群。
Hazelcast提供了多种发现成员的机制。如果我们未配置任何发现机制,则使用默认机制,即Hazelcast尝试使用多播在同一网络中查找其他成员。
这种方法有两个优点:

  • 设置集群非常容易,并且
  • 数据访问非常快。

我们不需要设置单独的缓存集群。这意味着我们可以通过添加几行代码来非常快速地创建集群。
如果我们想从集群中读取数据,则数据访问是低延迟的,因为我们不需要通过网络向缓存集群发送请求。
但这也带来了弊端。假设我们有一个系统,该系统需要一百个应用程序实例。在此集群拓扑中,这意味着即使我们不需要集群成员,我们也将拥有一百个集群成员。如此大量的缓存成员将消耗大量内存。
而且,复制和同步将非常昂贵。每当在缓存中添加或更新条目时,该条目就会与群集的其他成员同步,这会导致大量网络通信。
另外,我们必须注意Hazelcast是一个Java库。这意味着该成员只能嵌入在Java应用程序中。
当我们必须使用缓存中的数据执行高性能计算时,应使用嵌入式缓存拓扑。
我们可以通过将一个Config对象传递给factory方法来定制配置缓存。让我们看几个配置参数:
@Component
public class CacheClient {

    public static final String CARS = "cars";
    private final HazelcastInstance hazelcastInstance 
         = Hazelcast.newHazelcastInstance(createConfig());

    public Config createConfig() {
        Config config = new Config();
        config.addMapConfig(mapConfig());
        return config;
    }

    private MapConfig mapConfig() {
        MapConfig mapConfig = new MapConfig(CARS);
        mapConfig.setTimeToLiveSeconds(360);
        mapConfig.setMaxIdleSeconds(20);
        return mapConfig;
    }
    
   
// other methods omitted
}

我们可以分别配置Map集群中的每个或其他数据结构。
通过setTimeToLiveSeconds(360)我们定义条目在缓存中保留的时间。360秒后,该条目将被逐出。如果条目被更新,逐出时间将再次重置为0。
该方法setMaxIdleSeconds(20)定义条目在缓存中停留多长时间而不会被访问。每次读取操作都会“访问”一个条目。如果20秒钟内未访问任何条目,则该条目将被驱逐。

客户端-服务器拓扑
这种拓扑结构意味着我们建立了一个单独的缓存集群,而我们的应用程序就是该集群的客户端。

成员形成一个单独的群集,客户端从外部访问该群集。
为了构建集群,我们可以创建一个设置Hazelcast成员的Java应用程序,但是在此示例中,我们将使用准备好的Hazelcast 服务器
或者,我们可以启动Docker容器 作为集群成员。每个服务器或每个Docker容器都将使用默认配置启动集群的新成员。
现在,我们需要创建一个客户端来访问缓存集群。Hazelcast使用TCP套接字通信。这就是为什么不仅可以使用Java创建客户端的原因。Hazelcast提供了用其他语言编写的客户列表。为简单起见,让我们看看如何使用Spring创建客户端。
首先,我们将依赖项添加到Hazelcast客户端:

compile group: 'com.hazelcast', name: 'hazelcast', version: '4.0.1'

接下来,我们在Spring应用程序中创建一个Hazelcast客户端,类似于对嵌入式缓存拓扑所做的操作:

@Component
public class CacheClient {

    private static final String CARS = "cars";

    private HazelcastInstance client = HazelcastClient.newHazelcastClient();

    public Car put(String key, Car car){
        IMap<String, Car> map = client.getMap(CARS);
        return map.putIfAbsent(key, car);
    }

    public Car get(String key){
        IMap<String, Car> map = client.getMap(CARS);
        return map.get(key);
    }
    
   
// other methods omitted

}

要创建Hazelcast客户端,我们需要调用方法 HazelcastClient.newHazelcastClient()。Hazelcast将自动找到缓存群集。之后,我们可以通过Map 再次使用缓存。如果我们向map放入数据或从地图获取数据,则Hazelcast客户端会连接群集以访问数据。

现在,我们可以独立部署和扩展应用程序和缓存集群。例如,我们可以有50个应用程序实例和5个缓存集群成员。这是这种拓扑的最大优势。
如果我们在集群中遇到一些问题,则由于客户端和缓存是分开的而不是混合的,因此更容易识别和解决此问题。
但是,这种方法也有缺点。
首先,无论何时从集群写入或读取数据,我们都需要网络通信。与嵌入式缓存相比,它可能需要更长的时间。这种差异对于读取操作尤其重要。
其次,我们必须注意集群成员与客户端之间的版本兼容性。
当应用程序的部署大于群集缓存时,我们应该使用客户端-服务器拓扑。
由于我们的应用程序现在仅包含缓存的客户端,而不包含缓存本身,因此我们需要在测试中启动缓存实例。我们可以使用Hazelcast Docker映像Testcontainers轻松地做到这一点(请参阅GitHub上的示例)。

Near近缓存
当使用客户端-服务器拓扑时,我们正在产生网络流量以从缓存中请求数据。它在两种情况下发生:

  • 客户端从缓存成员读取数据时,以及
  • 当高速缓存成员开始与其他高速缓存成员进行通信以同步高速缓存中的数据时。

我们可以通过使用近缓存来避免这种缺点。
Near-cache是​​在Hazelcast成员或客户端上创建的本地缓存。让我们看一下在hazelcast客户端上创建近缓存时的工作方式:

每个客户端都创建其近缓存。当应用程序从缓存请求数据时,它首先在近缓存中查找数据。如果找不到数据,我们称其为高速缓存未命中。在这种情况下,数据是从远程缓存群集中请求的,并添加到了近缓存中。当应用程序想要再次读取此数据时,可以在近缓存中找到它。我们称此为缓存命中。
因此,near近缓存是二级缓存-或“缓存的缓存”。
我们可以在Spring应用程序中轻松配置近缓存:
@Component
public class CacheClient {

    private static final String CARS = "cars";

    private HazelcastInstance client 
       = HazelcastClient.newHazelcastClient(createClientConfig());

    private ClientConfig createClientConfig() {
        ClientConfig clientConfig = new ClientConfig();
        clientConfig.addNearCacheConfig(createNearCacheConfig());
        return clientConfig;
    }

    private NearCacheConfig createNearCacheConfig() {
        NearCacheConfig nearCacheConfig = new NearCacheConfig();
        nearCacheConfig.setName(CARS);
        nearCacheConfig.setTimeToLiveSeconds(360);
        nearCacheConfig.setMaxIdleSeconds(60);
        return nearCacheConfig;
    }
    
   
// other methods omitted

}

方法createNearCacheConfig()创建Near缓存的配置。通过调用将配置添加到Hazelcast客户端配置clientConfig.addNearCacheConfig()。请注意,这仅是此客户端上的Near缓存的配置。每个客户端都必须自己配置Near-cache。
使用近缓存,我们可以减少网络流量。但重要的是要了解我们必须接受可能的数据不一致。由于近缓存具有自己的配置,因此它将根据此配置逐出数据。如果数据在缓存集群中被更新或收回,我们仍然可以在近缓存中保存陈旧的数据。稍后将根据驱逐配置将这些数据逐出,然后我们将获得缓存未命中。只有从近缓存中逐出数据后,才会再次从缓存集群中读取数据。
当我们非常频繁地从高速缓存中读取数据时,并且当高速缓存群集中的数据很少变化时,我们应该使用Near高速缓存。