3.3. 分布式缓存
分发会尝试在缓存中保留任意条目的固定副本数,配置为 numOwners
。这允许缓存线性扩展,在节点添加到集群中时存储更多数据。
当节点加入并离开集群时,会出现一个键超过 numOwners
副本的时间。特别是,如果 numOwners
节点快速连续,一些条目将会丢失,因此我们表示分布式缓存容许 numOwners - 1
节点失败。
副本数代表在性能和数据持久性之间权衡。您维护的更副本,性能越低,但由于服务器或网络故障而丢失数据的风险也较低。无论有多少副本被维护,分布仍然会线性扩展,这对于 Data Grid 的可扩展性至关重要。
密钥的所有者被分成 一个主所有者,它协调对密钥的写入,以及零个或更多 备份所有者。要了解有关如何分配主和备份所有者的更多信息,请阅读 主要所有权 部分。
图 3.3. 分布式模式
读取操作将从主所有者请求值,但如果它没有以合理的时间响应,我们也从备份所有者请求值。( infinispan.stagger.delay
系统属性(以毫秒为单位)控制请求之间的延迟。) 如果本地缓存中存在密钥,或者所有所有者都速度较慢,则读取操作可能需要 0
个信息。
写入操作也会生成最多 2 个 * numOwners
消息:一个消息从原始者到主所有者,numOwners - 1
消息来自主所有者,即来自主到备份的消息,以及对应的 ACK 消息。
缓存拓扑更改可能会导致重试和其他消息,同时用于读取和写入。
与复制模式一样,分布式模式也可以是同步或异步的。与在复制模式中一样,不建议异步复制,因为它可能会丢失更新。除了丢失更新外,当线程写入密钥时,异步分布式缓存也可以看到过时的值,然后立即读取同一密钥。
事务分布式缓存使用与事务复制缓存相同的消息类型,除了 lock/prepare/commit/unlock 消息外,消息只会发送到受影响的节点(至少属于事务的节点),而不是广播到集群中的所有节点。作为优化,如果事务写入单个密钥,并且原始器是密钥的主要所有者,则不会复制锁定消息。
3.3.1. 读取一致性
即使使用同步复制,分布式缓存也无法线性。(对于事务缓存,我们说它们不支持序列化/快照隔离。) 我们可以有一个线程进行单个放置:
cache.get(k) -> v1 cache.put(k, v2) cache.get(k) -> v2
但是,另一个线程可能会以不同顺序看到值:
cache.get(k) -> v2 cache.get(k) -> v1
原因在于,读取 可以从任何 所有者返回值,具体取决于主所有者的回复速度。写入不是所有 owners owners- iwlin 事实的原子,只有在从备份收到确认后,主才会提交更新。当主要正在等待备份中的确认消息时,从备份中读取将会看到新值,但从主读取将看到旧值。
3.3.2. 密钥所有权
分布式缓存将条目分成固定数量的片段,并将每个网段分配给所有者节点列表。复制的缓存执行相同的操作,但每个节点都是所有者的除外。
所有者列表中的第一个节点 是主所有者。列表中的其他节点是 备份所有者。当缓存拓扑更改时,因为节点加入或离开集群,段所有权表会广播到每个节点。这允许节点查找密钥,而不生成多播请求或维护每个密钥的元数据。
numSegments
属性配置可用的片段数量。但是,除非重启了集群,否则片段的数量无法更改。
同样,key-to-segment 映射无法更改。无论集群拓扑更改如何,键都必须映射到同一段。务必要确保 key-to-segment 映射平均分配分配给每个节点的片段数量,同时尽量减少集群拓扑更改时必须移动的片段数量。
您可以通过配置 KeyPartitioner 或使用 Grouping API 来自定义 key-to-segment 映射。
但是,Data Grid 提供以下实现:
- SyncConsistentHashFactory
使用基于 一致的哈希 的算法。当禁用服务器提示时,默认选择。
只要集群是对称,这个实现始终将密钥分配给每个缓存中的同一节点。换句话说,所有缓存在所有节点上运行。这种实现存在一些负点,因此负载分布稍不均匀。它还在加入或离开时移动更多片段,而不是严格必要。
- TopologyAwareSyncConsistentHashFactory
-
与
SyncConsistentHashFactory
类似,但针对 Server Hinting 进行调整。当启用服务器提示时,默认选择。 - DefaultConsistentHashFactory
实现比
SyncConsistentHashFactory
甚至一个缺点。加入集群的顺序决定了哪些节点拥有哪些部分。因此,可能会将密钥分配给不同缓存中的不同节点。是从 5.2 到版本 8.1 到禁用服务器提示的版本 8.1 的默认设置。
- TopologyAwareConsistentHashFactory
与 DefaultConsistentHashFactory 类似,但针对 Server Hinting 进行调整。
是从 5.2 到版本 8.1 到启用服务器提示的版本 8.1 的默认设置。
- ReplicatedConsistentHashFactory
- 用于在内部实施复制缓存。您永远不会在分布式缓存中明确选择此算法。
3.3.2.1. 容量因素
容量因素根据节点可用的资源分配片段到节点的映射。
要配置容量因素,您可以指定任何非负数,Data Grid 哈希算法会为每个节点分配一个负载因其容量因子(作为主要所有者和备份所有者)权重。
例如,nodeA 在同一 Data Grid 集群中有 2 倍的内存,它比 nodeB 可用。在这种情况下,将 capacityFactor
设置为 2
的值,将 Data Grid 配置为将片段数量分配给 nodeA。
可以设置容量因数 0,
但仅在节点没有加入集群时,才有足够长以有用的数据所有者。
3.3.3. 零容量节点
您可能需要配置一个整个节点,其中每个缓存、用户定义的缓存和内部缓存的容量因素都是 0。
在定义零容量节点时,节点不会保存任何数据。这是您如何声明零容量节点:
<cache-container zero-capacity-node="true" />
new GlobalConfigurationBuilder().zeroCapacityNode(true);
3.3.4. 哈希配置
这是您如何通过 XML 以声明性方式配置哈希:
<distributed-cache name="distributedCache" owners="2" segments="100" capacity-factor="2" />
这是您在 Java 中以编程方式配置它:
Configuration c = new ConfigurationBuilder() .clustering() .cacheMode(CacheMode.DIST_SYNC) .hash() .numOwners(2) .numSegments(100) .capacityFactor(2) .build();
3.3.5. 初始集群大小
在处理拓扑更改时,数据网格非常动态性质(例如,在运行时添加 / 节点)意味着,节点在启动前不会等待其他节点存在。虽然这非常灵活,但可能不适用于在缓存启动前需要特定节点加入集群的应用程序。因此,您可以在继续缓存初始化前指定应该加入集群的节点数量。要做到这一点,请使用 initialClusterSize
和 initialClusterTimeout
传输属性。声明性 XML 配置:
<transport initial-cluster-size="4" initial-cluster-timeout="30000" />
编程 Java 配置:
GlobalConfiguration global = new GlobalConfigurationBuilder() .transport() .initialClusterSize(4) .initialClusterTimeout(30000, TimeUnit.MILLISECONDS) .build();
以上配置将在初始化前等待 4 个节点加入集群。如果初始节点没有出现在指定的超时时间中,缓存管理器将无法启动。
3.3.6. L1 缓存
启用 L1 后,节点将在短时间内在本地进行远程读取(默认为配置 10 分钟),重复查找将返回本地 L1 值,而不是再次询问所有者。
图 3.4. L1 缓存
L1 缓存不能自由。启用它的成本是,每个条目更新都必须将无效的消息广播到所有节点。当缓存配置有最大大小时,L1 条目可以像任何其他条目一样被驱除。启用 L1 将提高对非本地密钥重复读取的性能,但它会减慢写速度,它将增加对某种程度的内存消耗。
L1 缓存是否适合您?正确的方法是,在启用了 L1 的情况下对应用程序进行基准测试,并查看哪个最适合您的访问模式。
3.3.7. 服务器提示
可以指定以下拓扑提示:
- 机器
- 当多个 JVM 实例在同一节点上运行,或者多个虚拟机在同一物理机上运行时,这可能最有用。
- rack
- 在大型集群中,位于同一机架中的节点更有可能同时遇到硬件或网络故障。
- 站点
- 有些集群可能有多个物理位置的节点,以实现额外的弹性。请注意,跨站点复制是需要跨越两个或多个数据中心的集群的另一个替代方案。
以上所有都是可选的。提供时,分发算法将尝试尽可能将每个段的所有权分散到多个站点、机架和机器(按这个顺序)。
3.3.7.1. 配置
提示在传输级别配置:
<transport cluster="MyCluster" machine="LinuxServer01" rack="Rack01" site="US-WestCoast" />
3.3.8. 密钥关联性服务
在分布式缓存中,密钥被分配给具有不透明算法的节点列表。无法撤销计算,并生成映射到特定节点的密钥。但是,我们可以生成一系列(伪)随机键,查看其主所有者是什么,并在需要密钥映射到特定节点时将它们分发给应用程序。
3.3.8.1. API
以下代码片段描述了如何获取和使用对此服务的引用。
// 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 步中使用它。
3.3.8.2. 生命周期
KeyAffinityService
扩展了 生命周期
,允许停止和(重新)启动它:
public interface Lifecycle { void start(); void stop(); }
该服务通过 KeyAffinityServiceFactory
进行实例化。所有工厂方法都有一个 Executor
参数,该参数用于异步密钥生成(因此它不会在调用者的线程中发生)。用户负责处理此可执行文件的关机 。
启动后,需要明确停止 KeyAffinityService
。这会停止后台密钥生成,并释放其他持有的资源。
KeyAffinityService
本身停止的唯一情形是注册它的缓存管理器关闭的唯一情况。
3.3.8.3. 拓扑更改
当缓存拓扑更改时(例如节点加入或离开集群),KeyAffinityService
生成的密钥的所有权可能会改变。密钥关联性服务跟踪这些拓扑更改,且不会返回当前映射到不同节点的键,但不会对之前生成的密钥进行任何操作。
因此,应用程序应完全将 KeyAffinityService
视为优化,它们不应依赖生成的密钥的位置进行正确的性。
特别是,应用程序不应依赖 KeyAffinityService
生成的密钥来始终放在同一地址。密钥共存仅由 Grouping API 提供。
3.3.8.4. Grouping API
对 Key affinity 服务 的补充,分组 API 允许您在同一节点上并置一组条目,但无法选择实际节点。
3.3.8.5. 它如何工作?
默认情况下,使用键的 hashCode ()
计算密钥片段。如果使用 grouping API,Data Grid 将计算组的片段,并用作密钥的片段。有关如何将段映射到节点的更多详细信息,请参阅 Key Ownership 部分。
在使用组 API 时,每个节点仍然可以计算每个密钥的所有者,而无需联系其他节点。因此,无法手动指定组。组可以涉及条目(由密钥类生成)或 extrinsic (由外部功能生成)。
3.3.8.6. 如何使用 grouping API?
首先,您必须启用组。如果您要以编程方式配置数据网格,请调用:
Configuration c = new ConfigurationBuilder() .clustering().hash().groups().enabled() .build();
或者,如果您使用 XML:
<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; } } }
group 方法必须返回 String
如果您没有对密钥类进行控制,或者组的确定是键类的不当问题,我们建议使用 extrinsic 组。通过实施 Grouper
接口来指定 extrinsic 组。
public interface Grouper<T> { String computeGroup(T key, String group); Class<T> getKeyType(); }
如果为同一键类型配置了多个 Grouper
类,则调用它们的所有类,接收上一个键计算的值。如果键类也具有 @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; } }
grouper
实现必须在缓存配置中显式注册。如果您要以编程方式配置数据网格:
Configuration c = new ConfigurationBuilder() .clustering().hash().groups().enabled().addGrouper(new KXGrouper()) .build();
或者,如果您使用 XML:
<distributed-cache> <groups enabled="true"> <grouper class="com.acme.KXGrouper" /> </groups> </distributed-cache>
3.3.8.7. 高级接口
AdvancedCache
有两个特定于组的方法:
- getGroup(groupName)
- 检索属于某个组的缓存中的所有密钥。
- removeGroup(groupName)
- 删除属于组缓存中的所有密钥。
两种方法都会迭代整个数据容器和存储(如果存在),因此当缓存包含大量小组时,它们可能会很慢。