第 8 章 locking 和 Concurrency
Data Grid 利用多版本的并发控制(MVCC)- 常见用于相关数据库和其他数据存储的并发方案。MVCC 比粗粒度 Java 同步提供了很多优势,甚至 JDK Locks 用于访问共享数据,包括:
- 允许并发读取器和写入器
- 读取器和写入者不阻止另一个读者
- 可以检测和处理写偏移
- 内部锁定可以被条带化
8.1. 锁定实施详情
网格的 MVCC 实施利用最小锁定和同步,大量采用无锁定技术(如 compare-and-swap 和 lock-free 数据结构),这有助于对多 CPU 和多核心环境进行优化。
特别是,Data Grid 的 MVCC 实现被大量优化用于读取器。读取器线程不会获取条目的显式锁定,而是直接读取问题中的条目。
另一方面,写入者需要获取写锁定。这样可确保每个条目只有一个并发写入器,从而导致并发写入器排队以更改条目。
要允许并发读取,writers 通过在 MVCCEntry
中嵌套条目来制作他们要修改的条目的副本。此副本将并发读取器与看到部分修改的状态隔离。写入完成后,MVCCEntry.commit
() 将清除对数据容器的更改,后续读者将看到写入的更改。
8.1.1. 它如何在集群缓存中工作?
在集群缓存中,每个密钥都有一个节点负责锁定密钥。此节点称为主要所有者。
8.1.1.1. 非事务缓存
- 写入操作发送到密钥的主所有者。
主所有者尝试锁定密钥。
- 如果成功,它会将操作转发到其他所有者;
- 否则,会抛出异常。
如果操作是条件的,且在主所有者上失败,则不会转发到其他所有者。
如果在主所有者中本地执行操作,则将跳过第一步。
8.1.2. 事务缓存
事务缓存支持最佳锁定模式和 pessimistic 锁定模式。如需更多信息,请参阅 Transaction Locking。
8.1.3. 隔离级别
隔离级别会影响与其他事务同时运行时可以读取哪些事务。如需更多信息,请参阅隔离级别。
8.1.4. LockManager
LockManager
是一个组件,负责锁定写入条目。LockManager
使用 LockContainer
来定位/保留/创建锁定。LockContainers
有两个广泛的 flavours,支持锁定条带,支持每个条目一个锁定。
8.1.5. 锁定条带
锁定条带需要对整个缓存使用固定大小、共享锁定集合,并根据条目的键的哈希代码分配给条目。与 JDK 的 ConcurrentHashMap
分配锁定的方式类似,这允许在交换中具有高度可扩展、固定的锁定机制,以便有可能被同一锁阻止相关的条目。
另一种方法是禁用锁定条带 - 这意味着为每个条目 创建新的 锁定。这种方法 可能会 给您带来更高的并发吞吐量,但会牺牲额外的内存用量、垃圾收集混乱等。
默认禁用锁定条带,因为在不同键的锁定最终在同一锁定条带中时可能会出现的死锁。
可以使用 < locking
/> 配置元素的 concurrencyLevel
属性调整锁定条带使用的共享锁定集合的大小。
配置示例:
<locking striping="false|true"/>
或者
new ConfigurationBuilder().locking().useLockStriping(false|true);
8.1.6. 并发级别
除了确定条带锁定容器的大小外,此并发级别还用于调优任何基于 JDK ConcurrentHashMap
的集合,如 DataContainer
内部的集合。如需了解并发级别的详细讨论,请参阅 JDK ConcurrentHashMap
Javadocs,因为此参数在 Data Grid 中以完全相同的方式使用。
配置示例:
<locking concurrency-level="32"/>
或者
new ConfigurationBuilder().locking().concurrencyLevel(32);
8.1.7. 锁定超时
锁定超时指定内容锁定的时间(以毫秒为单位)。
配置示例:
<locking acquire-timeout="10000"/>
或者
new ConfigurationBuilder().locking().lockAcquisitionTimeout(10000); //alternatively new ConfigurationBuilder().locking().lockAcquisitionTimeout(10, TimeUnit.SECONDS);
8.1.8. 一致性
单个所有者被锁定(而不是所有所有者被锁定)不会破坏以下一致性保证:如果密钥 K
被哈希到节点 {A、B}
和事务 TX1
会为 K
获取锁定,让我们表示 A
。如果另一个事务( TX2
)在 B
(或任何其他节点)上启动,并且 TX2
尝试锁定 K
,那么它将因为锁定已经由 TX1
持有而失败。这样做的原因是,关键 K
的锁始终是,确定性,在集群的同一节点上获取,无论事务源自哪里。