2.2. 分布式缓存
数据网格会尝试将缓存中任何条目的固定副本数配置为 numOwners
。这允许分布式缓存线性扩展,在节点添加到集群中时存储更多数据。
当节点加入并离开集群时,当键超过 numOwners
的副本时,会出现一些时间。特别是,如果 numOwners
节点保留了快速成功,则一些条目将会丢失,因此,分布式缓存容许 numOwners - 1
个节点失败。
复制次数代表性能和数据持久性之间的利弊。您维护的副本数越多,较低性能也会降低因为服务器或网络故障而丢失数据的风险。
数据网格将密钥的所有者拆分为一个 主要所有者,后者协调对密钥的写入,以及零个或多个 备份所有者。
下图显示了客户端发送到备份所有者的写入操作。在这种情况下,备份节点会将写操作转发到主所有者,然后将写入复制到备份。
图 2.2. 集群复制
图 2.3. 分布式缓存
读取操作
读取操作从主所有者请求值。如果主所有者在合理的时间内没有响应,Data Grid 也从备份所有者请求值。
如果本地缓存中存在密钥,读取操作可能需要 0
信息,如果所有所有者都很慢,则读取操作可能需要最多 2 * numOwners
信息。
写操作
写入操作最多为 2 * numOwners
信息。从原始机构到主所有者和 numOwners - 1
信息(从主到备份节点)的消息以及相应的确认信息。
缓存拓扑更改可能会导致重试和写入操作的额外信息。
同步或异步复制
不建议使用异步复制,因为它可能会丢失更新。除了丢失更新外,异步分布式缓存还会在线程写入密钥时看到过时的值,然后立即读取相同的密钥。
Transactions
事务性分布式缓存仅将锁定/准备/过量消息发送到受影响的节点,这意味着所有拥有对事务影响的键的节点。作为优化,如果事务写入一个密钥,并且原始器是密钥的主所有者,则不会复制锁定消息。
2.2.1. 读取一致性
即使使用同步复制,分布式缓存也不是线性化的。对于事务缓存,不支持序列化/快照隔离。
例如,线程会执行一个请求:
cache.get(k) -> v1 cache.put(k, v2) cache.get(k) -> v2
但是,另一个线程可能会以不同的顺序看到值:
cache.get(k) -> v2 cache.get(k) -> v1
其原因在于,读取可以从任何所有者返回值,具体根据主所有者回复速度。写入不是在所有所有者中原子的。实际上,仅在从备份收到确认后更新的主要提交。在 primary 正在等待备份的确认消息时,从备份中读取时将看到新值,但从主中读取内容将看到旧信息。
2.2.2. 密钥所有权
分布式缓存将条目分成固定数量的片段,并将每个网段分配给所有者节点列表。复制缓存执行相同的操作,但每个节点都是一个所有者。
所有者列表中的第一个节点是主所有者。列表中的其他节点是 备份所有者。当缓存拓扑更改时,因为节点加入或离开集群,片段所有权表会广播到每个节点。这样,节点可以在不为每个密钥发出多播请求或维护元数据的情况下定位密钥。
numSegments
属性配置可用的片段数量。但是,除非集群重启,否则片段的数量无法更改。
与键对映射相同,也无法更改。无论集群拓扑的变化如何,键必须始终映射到同一片段。重要的一点是,在尽量减少集群拓扑更改时,键与分段映射平均分配分配给每个节点的片段数量非常重要。
一致的哈希值工厂实现 | 描述 |
---|---|
| 根据 一致的散列 使用算法。当服务器提示被禁用时,默认选择。 只要集群是对称,这个实现始终都会将密钥分配给每个缓存中的同一节点。换句话说,所有缓存在所有节点上都运行。这个实现的负点会有一些负点,负载分布略微不均匀。它还可使片段比加入或保留的严格必要条件移动。 |
|
相当于 |
|
实现比 |
|
相当于 |
| 内部用于实施复制缓存。您不能在分布式缓存中显式选择这个算法。 |
散列配置
您可以配置 ConsistentHashFactory
实现,包括只带有内嵌缓存的自定义设置。
XML
<distributed-cache name="distributedCache" owners="2" segments="100" capacity-factor="2" />
ConfigurationBuilder
Configuration c = new ConfigurationBuilder() .clustering() .cacheMode(CacheMode.DIST_SYNC) .hash() .numOwners(2) .numSegments(100) .capacityFactor(2) .build();
其他资源
2.2.3. 容量因素
容量因素根据集群中每个节点的可用资源分配片段数量。
节点的容量因素适用于该节点主要所有者和备份所有者的片段。换句话说,capacity factor 指定的容量是节点与集群中的其他节点相比的总容量。
默认值为 1
,表示集群中的所有节点都有相等的容量和数据网格,为集群中的所有节点分配相同的片段数量。
但是,如果节点有不同数量的可用内存,您可以配置容量因素,以便 Data Grid 哈希算法为每个节点分配容量加权的片段。
容量工厂配置的值必须是正数,可以是 1.5 分的一部分。您还可以配置容量因 0,
但建议只针对临时加入集群的节点,而应使用零容量配置。
2.2.3.1. 零容量节点
您可以为每个缓存、用户定义的缓存和内部缓存配置容量因素为 0
的节点。在定义零容量节点时,节点不会保存任何数据。
零容量节点配置
XML
<infinispan> <cache-container zero-capacity-node="true" /> </infinispan>
JSON
{ "infinispan" : { "cache-container" : { "zero-capacity-node" : "true" } } }
YAML
infinispan: cacheContainer: zeroCapacityNode: "true"
ConfigurationBuilder
new GlobalConfigurationBuilder().zeroCapacityNode(true);
2.2.4. 级别一(L1)缓存
当数据节点从集群中的另一节点检索条目时,Data Grid 节点会创建本地副本。L1 缓存避免重复在主所有者节点上查找条目并添加性能。
下图演示了 L1 缓存如何工作:
图 2.4. L1 缓存
在 "L1 缓存" 图表中:
-
客户端调用
cache.get ()
来读取集群中另一节点是主所有者的条目。 - originator 节点将读取操作转发到主所有者。
- 主所有者返回 key/value 条目。
- 原始器节点创建本地副本。
-
后续的
cache.get ()
调用会返回本地条目,而不转发到主所有者。
L1 缓存性能
启用 L1 提高了读取操作的性能,但需要主所有者节点在修改条目时广播无效的消息。这样可确保数据网格在集群中删除任何日期副本。然而,这也降低了写操作的性能并增加内存用量,从而减少缓存的整体容量。
与任何其他缓存条目一样,数据网格驱除并过期本地副本或 L1 条目。
L1 缓存配置
XML
<distributed-cache l1-lifespan="5000" l1-cleanup-interval="60000"> </distributed-cache>
JSON
{ "distributed-cache": { "l1-lifespan": "5000", "l1-cleanup-interval": "60000" } }
YAML
distributedCache: l1Lifespan: "5000" l1-cleanup-interval: "60000"
ConfigurationBuilder
ConfigurationBuilder builder = new ConfigurationBuilder(); builder.clustering().cacheMode(CacheMode.DIST_SYNC) .l1() .lifespan(5000, TimeUnit.MILLISECONDS) .cleanupTaskFrequency(60000, TimeUnit.MILLISECONDS);
2.2.5. 服务器提示
服务器提示通过尽可能在多个服务器、机架和数据中心中复制条目来增加分布式缓存中的数据可用性。
服务器提示仅适用于分布式缓存。
当数据网格发布数据的副本时,它会按照优先级顺序排列:站点、机架、机器和节点。所有配置属性都是可选的。例如,当您只指定机架 ID 时,Data Grid 会将副本分布到不同的机架和节点上。
如果缓存的片段数量太低,则服务器提示可能会影响集群重新平衡操作。
多个数据中心中的集群的替代选择是跨站点复制。
服务器提示配置
XML
<cache-container> <transport cluster="MyCluster" machine="LinuxServer01" rack="Rack01" site="US-WestCoast"/> </cache-container>
JSON
{ "infinispan" : { "cache-container" : { "transport" : { "cluster" : "MyCluster", "machine" : "LinuxServer01", "rack" : "Rack01", "site" : "US-WestCoast" } } } }
YAML
cacheContainer: transport: cluster: "MyCluster" machine: "LinuxServer01" rack: "Rack01" site: "US-WestCoast"
GlobalConfigurationBuilder
GlobalConfigurationBuilder global = GlobalConfigurationBuilder.defaultClusteredBuilder() .transport() .clusterName("MyCluster") .machineId("LinuxServer01") .rackId("Rack01") .siteId("US-WestCoast");
2.2.6. 关键关联性服务
在分布式缓存中,密钥被分配到一个带有不透明算法的节点列表。无法轻松撤销计算并生成映射到特定节点的键。但是,数据网格可生成一系列(伪)随机键,查看其主所有者是哪一个,并在需要密钥映射到特定节点时将其转移到应用程序。
以下代码片段描述了如何获取和使用该服务。
// 1. Obtain a reference to a cache Cache cache = ... Address address = cache.getCacheManager().getAddress(); // 2. Create the affinity service KeyAffinityService keyAffinityService = KeyAffinityServiceFactory.newLocalKeyAffinityService( cache, new RndKeyGenerator(), Executors.newSingleThreadExecutor(), 100); // 3. Obtain a key for which the local node is the primary owner Object localKey = keyAffinityService.getKeyForAddress(address); // 4. Insert the key in the cache cache.put(localKey, "yourValue");
服务从第 2 步启动:此时使用提供的 Executor 生成和队列键。在第 3 步中,我们从服务中获取密钥,并在第 4 步中使用它。
生命周期
KeyAffinityService
扩展 生命周期
,允许停止和(重新)启动它:
public interface Lifecycle { void start(); void stop(); }
该服务通过 KeyAffinityServiceFactory
进行实例化。所有工厂方法都有 Executor
参数,该参数用于异步密钥生成(因此它不会在调用者的线程中发生)。用户负责处理此可执行文件的关闭 。
启动后,需要显式停止 KeyAffinityService
。这会停止后台密钥生成并释放其他保存的资源。
唯一停止的 KeyAffinityService
才会在关闭了它注册的缓存管理器时。
拓扑更改
当缓存拓扑更改时,KeyAffinityService
生成的密钥所有权可能会改变。关键关联性服务会跟踪这些拓扑的更改,且不会返回当前映射到不同节点的键,但它不会执行之前生成的密钥。
因此,应用程序应该完全将 KeyAffinityService
视为优化,它们不应依赖于生成的密钥的位置来获得正确性。
特别是,应用程序不应该依赖 KeyAffinityService
生成的密钥来始终放在一起。密钥的并置仅由组 API 提供
。
2.2.7. 分组 API
除了键关联服务之外,Grouping
API 允许您在同一节点上并置一组条目,但无法选择实际节点。
默认情况下,使用密钥的 hashCode ()
计算密钥片段。如果使用 Grouping
API,Data Grid 将计算该组的片段,并使用该片段作为密钥的片段。
使用 Grouping
API 时,务必要确保每个节点仍然可以计算每个密钥的所有者,而无需联系其他节点。因此,无法手动指定组。组可以不属于条目(由密钥类生成的)或 extrinsic (由外部功能生成)。
要使用 Grouping
API,您必须启用组。
Configuration c = new ConfigurationBuilder() .clustering().hash().groups().enabled() .build();
<distributed-cache> <groups enabled="true"/> </distributed-cache>
如果您已控制密钥类(您可以更改类定义,而不是不可修改库的一部分),那么我们建议使用一个内部组。内部组通过向方法添加 @Group
注释来指定,例如:
class User { ... String office; ... public int hashCode() { // Defines the hash for the key, normally used to determine location ... } // Override the location by specifying a group // All keys in the same group end up with the same owners @Group public String getOffice() { return office; } } }
组方法必须返回 String
如果您没有对密钥类控制,或者组的确定性是与密钥类相关的一个或临时性问题,我们推荐使用 extrinsic 组。通过实施组 器接口来指定 extrinsic 组
。
public interface Grouper<T> { String computeGroup(T key, String group); Class<T> getKeyType(); }
如果为同一密钥类型配置了多个 组
类,则所有这些类都会被调用,接收上一个密钥计算的值。如果键类也具有 @Group
注释,则第一个 Grouper
将接收由注释方法计算的组。这样,在使用内部组时,您可以更好地控制组。
Grouper
实施示例
public class KXGrouper implements Grouper<String> { // The pattern requires a String key, of length 2, where the first character is // "k" and the second character is a digit. We take that digit, and perform // modular arithmetic on it to assign it to group "0" or group "1". private static Pattern kPattern = Pattern.compile("(^k)(<a>\\d</a>)$"); public String computeGroup(String key, String group) { Matcher matcher = kPattern.matcher(key); if (matcher.matches()) { String g = Integer.parseInt(matcher.group(2)) % 2 + ""; return g; } else { return null; } } public Class<String> getKeyType() { return String.class; } }
组器
实施必须在缓存配置中明确注册。如果您要以编程方式配置数据网格:
Configuration c = new ConfigurationBuilder() .clustering().hash().groups().enabled().addGrouper(new KXGrouper()) .build();
或者,如果您使用 XML:
<distributed-cache> <groups enabled="true"> <grouper class="com.example.KXGrouper" /> </groups> </distributed-cache>
高级 API
AdvancedCache
有两个特定于组的方法:
-
getGroup (groupName)
检索到组的缓存中的所有密钥。 -
removeGroup (groupName)
删除属于组的缓存中的所有密钥。
这两种方法都迭代整个数据容器和存储(如果存在),因此当缓存包含大量小组时,它们可能会很慢。