Apache Ignite 事务架构:并发模型和隔离级别
<p>在本系列的第一篇文章中,我们研究了2阶段提交协议,以及Ignite如何处理各种类型的集群节点,下面是在剩下的文章中要覆盖的主题:</p> <ul> <li>并发模型和隔离级别</li> <li>故障转移和恢复</li> <li>Ignite持久化层中的事务处理(WAL、检查点及其他);</li> <li>第三方持久化中的事务处理</li> </ul> <p>在本文中,我们会聚焦并发模型和隔离级别。 大多数现代多用户应用允许并发数据访问和修改。为了管理此功能,并确保系统从一个一致状态切换到另一个一致状态,使用了事务的概念。事务依赖于锁,它可以在事务开始时(悲观锁)获得,也可以在事务结束提交之前(乐观锁)获得。 Ignite支持两种并发模型: <a href="/misc/goto?guid=4959757301864518788" rel="nofollow,noindex">悲观</a> 和 <a href="/misc/goto?guid=4959757301959617814" rel="nofollow,noindex">乐观</a> ,下面先讲悲观并发模型。</p> <h2>悲观并发模型</h2> <p>悲观并发模型的一个例子是两个银行账户之间的转账,需要确保两个银行账户的借贷状态正确记录。这时需要给两个账户加锁来确保更新全部完成并且余额正确。 在悲观并发模型中,应用需要在事务开始时锁定即将要读、写或者修改的所有数据。Ignite还支持一组悲观并发模型的 <a href="/misc/goto?guid=4959757302043290884" rel="nofollow,noindex">隔离级别</a> ,在读写数据时提供了灵活性:</p> <ul> <li>读提交</li> <li>可重复读</li> <li>序列化</li> </ul> <p>在读提交模型中,锁是在写操作对数据进行任何改变之前获得的,比如 <strong> put() <strong>或者</strong> putAll() </strong> ,而可重复读以及序列化模型用于读写操作都需要获得锁的场景。Ignite还有些内置的功能,使得调试和解决分布式死锁问题更容易。 下面的代码示例展示了可重复读的悲观事务,因为应用需要对一个特定银行账户进行读和写的操作:</p> <pre> <code class="language-objectivec">try (Transaction tx = Ignition.ignite().transactions().txStart(PESSIMISTIC, REPEATABLE_READ)) { Account acct = cache.get(acctId); assert acct != null; ... // Deposit into account. acct.update(amount); // Store updated account in cache. cache.put(acctId, acct); tx.commit(); }</code></pre> <p>本例中,通过**txStart() <strong>和</strong> tx.commit() 方法分别来进行事务的开启和提交。 txStart() 方法传递了PESSIMISTIC和REPEATABLE READ参数,在try块体中,代码在 acctId 键上执行了一个 cache.get() <strong>操作,之后,一些资金存入账户并且缓存使用</strong> cache.put()**进行了更新。 下面的代码示例展示了读提交并且带有死锁处理的悲观事务:</p> <pre> <code class="language-objectivec">try (Transaction tx = ignite.transactions().txStart(TransactionConcurrency.PESSIMISTIC, TransactionIsolation.READ_COMMITTED, TX_TIMEOUT, 0)) { // More code here. tx.commit(); } catch (CacheException e) { if (e.getCause() instanceof TransactionTimeoutException && e.getCause().getCause() instanceof TransactionDeadlockException) System.out.println(e.getCause().getCause().getMessage()); }</code></pre> <p>本例中,代码展示了如何使用Ignite的 <a href="/misc/goto?guid=4959757302126986563" rel="nofollow,noindex">死锁检测机制</a> ,这简化了可能由应用代码导致的分布式死锁的调试。要开启这个特性,需要开启一个超时时间非0的Ignite事务(TX_TIMEOUT > 0),还需要捕获包含死锁详细信息的TransactionDeadlockException。 下面再看一下不同隔离级别的消息流,对于读提交,如图1所示,在这个隔离模型中,Ignite对于读操作不会获得锁,比如 <strong> get() <strong>或者</strong> getAll() </strong> ,这对很多场景可能更适合。</p> <p><img src="https://simg.open-open.com/show/66760fcbd7fdb917916320daf3f3cee0.png"></p> <ol> <li>事务开始( <strong>1 tx.Start</strong> );</li> <li>事务协调器在内部管理事务请求( <strong>2 IgniteInternalTx</strong> );</li> <li>应用写入键K1和K2( <strong>3 tx.putAll(K1-V1, K2-V2)</strong> );</li> <li>事务协调器将K1写入本地事务映射( <strong>4 Put(K1)</strong> );</li> <li>事务协调器向存储K1的主节点发起一个锁请求( <strong>5 lock(K1)</strong> );</li> <li>主节点在内部管理事务请求( <strong>6 IgniteInternalTx</strong> );</li> <li>主节点向事务协调者发送一个已经准备好的确认( <strong>7 ACK</strong> );</li> <li>对于K2重复如图1的4-7步骤;</li> <li>发起事务提交请求( <strong>12 tx.commit</strong> );</li> <li>K1和K2写入相应的主节点( <strong>13 Write(K1)和13 Write(K2)</strong> );</li> <li>主节点确认事务提交( <strong>14 ACK</strong> );</li> </ol> <p>下一步,看一下可重复读和序列化的消息流,如图2所示:</p> <p><img src="https://simg.open-open.com/show/a6aadba87a8983720d82ecc18bb2c85a.png"></p> <ol> <li>事务开始( <strong>1 tx.Start</strong> );</li> <li>事务协调器在内部管理事务请求( <strong>2 IgniteInternalTx</strong> );</li> <li>应用读取键K1和K2( <strong>3 tx.getAll(K1-V1, K2-V2)</strong> );</li> <li>事务协调器开始键K1的读请求处理( <strong>4 Get(K1)</strong> );</li> <li>事务协调器向存储K1的主节点发起一个锁请求( <strong>5 lock(K1)</strong> );</li> <li>主节点在内部管理事务请求( <strong>6 IgniteInternalTx</strong> );</li> <li>主节点向事务协调者发送一个已经准备好的确认( <strong>7 ACK</strong> )并且返回K1的值;</li> <li>对于K2重复如图2的4-7步骤;</li> <li>应用写入K1和K2( <strong>12 tx.putAll(K1-V2, K2-V2)</strong> );</li> <li>事务协调器将K1的更新写入本地事务映射( <strong>13 Put(K1)</strong> );</li> <li>事务协调器将K2的更新写入本地事务映射( <strong>14 Put(K2)</strong> );</li> <li>发起事务提交请求( <strong>15 tx.commit</strong> );</li> <li>K1和K2写入相应的主节点( <strong>16 Write(K1)和16 Write(K2)</strong> );</li> <li>主节点确认事务提交( <strong>17 ACK</strong> );</li> </ol> <p>总结一下,在悲观模型中,在事务完成之前锁一直持有,并且锁会阻止其他事务对数据的访问。 下一步看一下乐观并发模型。</p> <h2>乐观并发模型</h2> <p>乐观并发模型的一个例子是计算机辅助设计(CAD),这里一个设计师工作于整个设计的一部分,通常会将设计从中央仓库中检出到本地工作区,然后进行部分更新之后将成果检入中央仓库,因为设计师只负责整个设计的一部分,所以不可能与其他部分的更新产生冲突。 与悲观并发模型相反,乐观并发模型延迟了锁的获取,这样更适合于资源争用较少的应用,比如上面描述的CAD的例子。Ignite还支持一些乐观并发模型的 <a href="/misc/goto?guid=4959757302043290884" rel="nofollow,noindex">隔离级别</a> ,这提供了读写数据方面的灵活性:</p> <ul> <li>读提交</li> <li>可重复读</li> <li>序列化( <a href="/misc/goto?guid=4959757302222624979" rel="nofollow,noindex">无死锁</a> )</li> </ul> <p>回顾一下前文中关于2阶段提交中各个阶段的讨论,当使用乐观并发模型时,在准备阶段,锁是在主节点获取的。在使用序列化模式时,如果通过事务请求的数据已经改变,在准备阶段事务会失败。这时,开发者需要编程控制应用的行为,即是否需要重启事务。而其他的两个模式,可重复读和读提交,不会检查数据是否改变。虽然这会带来性能方面的好处,但是没有了数据的原子性保证,因此,这两个模式在生产中很少用到。 下面的代码示例展示了序列化的乐观事务,因为应用需要对一个特定银行账户进行读和写的操作:</p> <pre> <code class="language-objectivec">while (true) { try (Transaction tx = ignite.transactions().txStart(TransactionConcurrency.OPTIMISTIC, TransactionIsolation.SERIALIZABLE)) { Account acct = cache.get(acctId); assert acct != null; ... // Deposit into account. acct.update(amount); // Store updated account in cache. cache.put(acctId, acct); tx.commit(); // Transaction succeeded. Exiting the loop. break; } catch (TransactionOptimisticException e) { // Transaction has failed. Retry. } }</code></pre> <p>本例中,在外侧有个while循环,判断事务是否失败,它可以重试。下一步,有**txStart() <strong>和</strong> tx.commit()**方法,分别用于事务的开始和提交。**txStart() 方法传递了OPTIMISTIC和SERIALIZABLE参数,在try块体中,代码先在acctId键上执行了 cache.get() <strong>操作,之后,一些资金存入账户并且缓存使用</strong> cache.put()**进行了更新。如果事务成功,代码会从循环中中断,如果事务不成功,会抛出异常然后事务重试。对于乐观的序列化事务,访问键的顺序不受限制,因为Ignite为了 <a href="/misc/goto?guid=4959757302222624979" rel="nofollow,noindex">避免死锁</a> ,事务锁是通过一个额外的检查并行地获得的。 下面看一下不同隔离级别下的消息流,先从序列化开始,如图3所示:</p> <p><img src="https://simg.open-open.com/show/203d0ce1ab009f4cdb7dbd931c289cfb.png"></p> <ol> <li>事务开始( <strong>1 tx.Start</strong> );</li> <li>事务协调器在内部管理事务请求( <strong>2 IgniteInternalTx</strong> );</li> <li>应用写入键K1( <strong>3 tx.put(K1-V1)</strong> );</li> <li>事务协调器将K1写入本地事务映射( <strong>4 Put(K1)</strong> );</li> <li>应用写入键K2( <strong>5 tx.put(K2-V2)</strong> );</li> <li>事务协调器将K2写入本地事务映射( <strong>6 Put(K2)</strong> );</li> <li>发起事务提交请求( <strong>7 tx.commit</strong> );</li> <li>事务协调器向存储K1和K2的主节点发起锁请求( <strong>8 lock(K1, TV1) and 8 lock(K2, TV1)</strong> );</li> <li>主节点在内部管理事务请求( <strong>9 IgniteInternalTx</strong> );</li> <li>主节点向事务协调者发送一个已经准备好的确认( <strong>10 ACK</strong> );</li> <li>K1和K2写入相应的主节点( <strong>11 Write(K1)和11 Write(K2)</strong> );</li> <li>如果没有数据冲突(即K1和K2没有被其他的应用更新),主节点确认事务提交( <strong>12 ACK</strong> )。</li> </ol> <p>最后,看一下可重复读和读提交的消息流,如图4所示:</p> <p><img src="https://simg.open-open.com/show/82cf96301696f20203b308da6df90d64.png"></p> <ol> <li>事务开始( <strong>1 tx.Start</strong> );</li> <li>事务协调器在内部管理事务请求( <strong>2 IgniteInternalTx</strong> );</li> <li>应用写入键K1( <strong>3 tx.put(K1-V1)</strong> );</li> <li>事务协调器将K1写入本地事务映射( <strong>4 Put(K1)</strong> );</li> <li>应用写入键K2( <strong>5 tx.put(K2-V2)</strong> );</li> <li>事务协调器将K2写入本地事务映射( <strong>6 Put(K2)</strong> );</li> <li>发起事务提交请求( <strong>7 tx.commit</strong> );</li> <li>事务协调器向存储K1和K2的主节点发起锁请求( <strong>8 lock(K1, TV1) and 8 lock(K2, TV1)</strong> );</li> <li>主节点向事务协调者发送一个已经准备好的确认( <strong>9 ACK</strong> );</li> <li>K1和K2写入相应的主节点( <strong>10 Write(K1)和10 Write(K2)</strong> );</li> <li>主节点在内部管理事务请求( <strong>11 IgniteInternalTx</strong> );</li> <li>主节点确认事务提交( <strong>12 ACK</strong> )。</li> </ol> <h2>总结</h2> <p>在本文中,研究了Ignite支持的主要的锁模型和隔离级别,我们看到,有很大的灵活性和选择空间,本系列的后面文章中,会研究故障转移和恢复。</p> <p>本文译自GridGain技术布道师Akmal B. Chaudhri的 <a href="/misc/goto?guid=4959757302329092285" rel="nofollow,noindex">博客</a> 。</p> <p> </p> <p>来自:https://my.oschina.net/liyuj/blog/1627248</p> <p> </p>
本文由用户 feng2r200 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
转载本站原创文章,请注明出处,并保留原始链接、图片水印。
本站是一个以用户分享为主的开源技术平台,欢迎各类分享!