Apache Ignite事务架构:并发模型和隔离级别
2018-03-03 12:00
483 查看
摘要: 在本系列的第一篇文章中我们研究了2阶段提交协议,在本文中,我们会聚焦并发模型和隔离级别。
在本系列的第一篇文章中,我们研究了2阶段提交协议,以及Ignite如何处理各种类型的集群节点,下面是在剩下的文章中要覆盖的主题:
并发模型和隔离级别
故障转移和恢复
Ignite持久化层中的事务处理(WAL、检查点及其他);
第三方持久化中的事务处理
在本文中,我们会聚焦并发模型和隔离级别。
大多数现代多用户应用允许并发数据访问和修改。为了管理此功能,并确保系统从一个一致状态切换到另一个一致状态,使用了事务的概念。事务依赖于锁,它可以在事务开始时(悲观锁)获得,也可以在事务结束提交之前(乐观锁)获得。
Ignite支持两种并发模型:悲观和乐观,下面先讲悲观并发模型。
在悲观并发模型中,应用需要在事务开始时锁定即将要读、写或者修改的所有数据。Ignite还支持一组悲观并发模型的隔离级别,在读写数据时提供了灵活性:
读提交
可重复读
序列化
在读提交模型中,锁是在写操作对数据进行任何改变之前获得的,比如put()[b]或者putAll()[/b],而可重复读以及序列化模型用于读写操作都需要获得锁的场景。Ignite还有些内置的功能,使得调试和解决分布式死锁问题更容易。
下面的代码示例展示了可重复读的悲观事务,因为应用需要对一个特定银行账户进行读和写的操作:
本例中,通过txStart()和tx.commit()方法分别来进行事务的开启和提交。txStart()方法传递了PESSIMISTIC和REPEATABLE READ参数,在try块体中,代码在acctId键上执行了一个cache.get()操作,之后,一些资金存入账户并且缓存使用cache.put()进行了更新。
下面的代码示例展示了读提交并且带有死锁处理的悲观事务:
本例中,代码展示了如何使用Ignite的死锁检测机制,这简化了可能由应用代码导致的分布式死锁的调试。要开启这个特性,需要开启一个超时时间非0的Ignite事务(TX_TIMEOUT > 0),还需要捕获包含死锁详细信息的TransactionDeadlockException。
下面再看一下不同隔离级别的消息流,对于读提交,如图1所示,在这个隔离模型中,Ignite对于读操作不会获得锁,比如get()[b]或者getAll()[/b],这对很多场景可能更适合。
事务开始(1 tx.Start);
事务协调器在内部管理事务请求(2 IgniteInternalTx);
应用写入键K1和K2(3 tx.putAll(K1-V1, K2-V2));
事务协调器将K1写入本地事务映射(4 Put(K1));
事务协调器向存储K1的主节点发起一个锁请求(5 lock(K1));
主节点在内部管理事务请求(6 IgniteInternalTx);
主节点向事务协调者发送一个已经准备好的确认(7 ACK);
对于K2重复如图1的4-7步骤;
发起事务提交请求(12 tx.commit);
K1和K2写入相应的主节点(13 Write(K1)和13 Write(K2));
主节点确认事务提交(14 ACK);
下一步,看一下可重复读和序列化的消息流,如图2所示:
事务开始(1 tx.Start);
事务协调器在内部管理事务请求(2 IgniteInternalTx);
应用读取键K1和K2(3 tx.getAll(K1-V1, K2-V2));
事务协调器开始键K1的读请求处理(4 Get(K1));
事务协调器向存储K1的主节点发起一个锁请求(5 lock(K1));
主节点在内部管理事务请求(6 IgniteInternalTx);
主节点向事务协调者发送一个已经准备好的确认(7 ACK)并且返回K1的值;
对于K2重复如图2的4-7步骤;
应用写入K1和K2(12 tx.putAll(K1-V2, K2-V2));
事务协调器将K1的更新写入本地事务映射(13 Put(K1));
事务协调器将K2的更新写入本地事务映射(14 Put(K2));
发起事务提交请求(15 tx.commit);
K1和K2写入相应的主节点(16 Write(K1)和16 Write(K2));
主节点确认事务提交(17 ACK);
总结一下,在悲观模型中,在事务完成之前锁一直持有,并且锁会阻止其他事务对数据的访问。
下一步看一下乐观并发模型。
与悲观并发模型相反,乐观并发模型延迟了锁的获取,这样更适合于资源争用较少的应用,比如上面描述的CAD的例子。Ignite还支持一些乐观并发模型的隔离级别,这提供了读写数据方面的灵活性:
读提交
可重复读
序列化(无死锁)
回顾一下前文中关于2阶段提交中各个阶段的讨论,当使用乐观并发模型时,在准备阶段,锁是在主节点获取的。在使用序列化模式时,如果通过事务请求的数据已经改变,在准备阶段事务会失败。这时,开发者需要编程控制应用的行为,即是否需要重启事务。而其他的两个模式,可重复读和读提交,不会检查数据是否改变。虽然这会带来性能方面的好处,但是没有了数据的原子性保证,因此,这两个模式在生产中很少用到。
下面的代码示例展示了序列化的乐观事务,因为应用需要对一个特定银行账户进行读和写的操作:
本例中,在外侧有个while循环,判断事务是否失败,它可以重试。下一步,有txStart()和tx.commit()方法,分别用于事务的开始和提交。txStart()方法传递了OPTIMISTIC和SERIALIZABLE参数,在try块体中,代码先在acctId键上执行了cache.get()操作,之后,一些资金存入账户并且缓存使用cache.put()进行了更新。如果事务成功,代码会从循环中中断,如果事务不成功,会抛出异常然后事务重试。对于乐观的序列化事务,访问键的顺序不受限制,因为Ignite为了避免死锁,事务锁是通过一个额外的检查并行地获得的。
下面看一下不同隔离级别下的消息流,先从序列化开始,如图3所示:
事务开始(1 tx.Start);
事务协调器在内部管理事务请求(2 IgniteInternalTx);
应用写入键K1(3 tx.put(K1-V1));
事务协调器将K1写入本地事务映射(4 Put(K1));
应用写入键K2(5 tx.put(K2-V2));
事务协调器将K2写入本地事务映射(6 Put(K2));
发起事务提交请求(7 tx.commit);
事务协调器向存储K1和K2的主节点发起锁请求(8 lock(K1, TV1) and 8 lock(K2, TV1));
主节点在内部管理事务请求(9 IgniteInternalTx);
主节点向事务协调者发送一个已经准备好的确认(10 ACK);
K1和K2写入相应的主节点(11 Write(K1)和11 Write(K2));
如果没有数据冲突(即K1和K2没有被其他的应用更新),主节点确认事务提交(12 ACK)。
最后,看一下可重复读和读提交的消息流,如图4所示:
事务开始(1 tx.Start);
事务协调器在内部管理事务请求(2 IgniteInternalTx);
应用写入键K1(3 tx.put(K1-V1));
事务协调器将K1写入本地事务映射(4 Put(K1));
应用写入键K2(5 tx.put(K2-V2));
事务协调器将K2写入本地事务映射(6 Put(K2));
发起事务提交请求(7 tx.commit);
事务协调器向存储K1和K2的主节点发起锁请求(8 lock(K1, TV1) and 8 lock(K2, TV1));
主节点向事务协调者发送一个已经准备好的确认(9 ACK);
K1和K2写入相应的主节点(10 Write(K1)和10 Write(K2));
主节点在内部管理事务请求(11 IgniteInternalTx);
主节点确认事务提交(12 ACK)。
本文译自GridGain技术布道师Akmal B. Chaudhri的博客。
在本系列的第一篇文章中,我们研究了2阶段提交协议,以及Ignite如何处理各种类型的集群节点,下面是在剩下的文章中要覆盖的主题:
并发模型和隔离级别
故障转移和恢复
Ignite持久化层中的事务处理(WAL、检查点及其他);
第三方持久化中的事务处理
在本文中,我们会聚焦并发模型和隔离级别。
大多数现代多用户应用允许并发数据访问和修改。为了管理此功能,并确保系统从一个一致状态切换到另一个一致状态,使用了事务的概念。事务依赖于锁,它可以在事务开始时(悲观锁)获得,也可以在事务结束提交之前(乐观锁)获得。
Ignite支持两种并发模型:悲观和乐观,下面先讲悲观并发模型。
悲观并发模型
悲观并发模型的一个例子是两个银行账户之间的转账,需要确保两个银行账户的借贷状态正确记录。这时需要给两个账户加锁来确保更新全部完成并且余额正确。在悲观并发模型中,应用需要在事务开始时锁定即将要读、写或者修改的所有数据。Ignite还支持一组悲观并发模型的隔离级别,在读写数据时提供了灵活性:
读提交
可重复读
序列化
在读提交模型中,锁是在写操作对数据进行任何改变之前获得的,比如put()[b]或者putAll()[/b],而可重复读以及序列化模型用于读写操作都需要获得锁的场景。Ignite还有些内置的功能,使得调试和解决分布式死锁问题更容易。
下面的代码示例展示了可重复读的悲观事务,因为应用需要对一个特定银行账户进行读和写的操作:
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(); }
本例中,通过txStart()和tx.commit()方法分别来进行事务的开启和提交。txStart()方法传递了PESSIMISTIC和REPEATABLE READ参数,在try块体中,代码在acctId键上执行了一个cache.get()操作,之后,一些资金存入账户并且缓存使用cache.put()进行了更新。
下面的代码示例展示了读提交并且带有死锁处理的悲观事务:
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()); }
本例中,代码展示了如何使用Ignite的死锁检测机制,这简化了可能由应用代码导致的分布式死锁的调试。要开启这个特性,需要开启一个超时时间非0的Ignite事务(TX_TIMEOUT > 0),还需要捕获包含死锁详细信息的TransactionDeadlockException。
下面再看一下不同隔离级别的消息流,对于读提交,如图1所示,在这个隔离模型中,Ignite对于读操作不会获得锁,比如get()[b]或者getAll()[/b],这对很多场景可能更适合。
事务开始(1 tx.Start);
事务协调器在内部管理事务请求(2 IgniteInternalTx);
应用写入键K1和K2(3 tx.putAll(K1-V1, K2-V2));
事务协调器将K1写入本地事务映射(4 Put(K1));
事务协调器向存储K1的主节点发起一个锁请求(5 lock(K1));
主节点在内部管理事务请求(6 IgniteInternalTx);
主节点向事务协调者发送一个已经准备好的确认(7 ACK);
对于K2重复如图1的4-7步骤;
发起事务提交请求(12 tx.commit);
K1和K2写入相应的主节点(13 Write(K1)和13 Write(K2));
主节点确认事务提交(14 ACK);
下一步,看一下可重复读和序列化的消息流,如图2所示:
事务开始(1 tx.Start);
事务协调器在内部管理事务请求(2 IgniteInternalTx);
应用读取键K1和K2(3 tx.getAll(K1-V1, K2-V2));
事务协调器开始键K1的读请求处理(4 Get(K1));
事务协调器向存储K1的主节点发起一个锁请求(5 lock(K1));
主节点在内部管理事务请求(6 IgniteInternalTx);
主节点向事务协调者发送一个已经准备好的确认(7 ACK)并且返回K1的值;
对于K2重复如图2的4-7步骤;
应用写入K1和K2(12 tx.putAll(K1-V2, K2-V2));
事务协调器将K1的更新写入本地事务映射(13 Put(K1));
事务协调器将K2的更新写入本地事务映射(14 Put(K2));
发起事务提交请求(15 tx.commit);
K1和K2写入相应的主节点(16 Write(K1)和16 Write(K2));
主节点确认事务提交(17 ACK);
总结一下,在悲观模型中,在事务完成之前锁一直持有,并且锁会阻止其他事务对数据的访问。
下一步看一下乐观并发模型。
乐观并发模型
乐观并发模型的一个例子是计算机辅助设计(CAD),这里一个设计师工作于整个设计的一部分,通常会将设计从中央仓库中检出到本地工作区,然后进行部分更新之后将成果检入中央仓库,因为设计师只负责整个设计的一部分,所以不可能与其他部分的更新产生冲突。与悲观并发模型相反,乐观并发模型延迟了锁的获取,这样更适合于资源争用较少的应用,比如上面描述的CAD的例子。Ignite还支持一些乐观并发模型的隔离级别,这提供了读写数据方面的灵活性:
读提交
可重复读
序列化(无死锁)
回顾一下前文中关于2阶段提交中各个阶段的讨论,当使用乐观并发模型时,在准备阶段,锁是在主节点获取的。在使用序列化模式时,如果通过事务请求的数据已经改变,在准备阶段事务会失败。这时,开发者需要编程控制应用的行为,即是否需要重启事务。而其他的两个模式,可重复读和读提交,不会检查数据是否改变。虽然这会带来性能方面的好处,但是没有了数据的原子性保证,因此,这两个模式在生产中很少用到。
下面的代码示例展示了序列化的乐观事务,因为应用需要对一个特定银行账户进行读和写的操作:
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. } }
本例中,在外侧有个while循环,判断事务是否失败,它可以重试。下一步,有txStart()和tx.commit()方法,分别用于事务的开始和提交。txStart()方法传递了OPTIMISTIC和SERIALIZABLE参数,在try块体中,代码先在acctId键上执行了cache.get()操作,之后,一些资金存入账户并且缓存使用cache.put()进行了更新。如果事务成功,代码会从循环中中断,如果事务不成功,会抛出异常然后事务重试。对于乐观的序列化事务,访问键的顺序不受限制,因为Ignite为了避免死锁,事务锁是通过一个额外的检查并行地获得的。
下面看一下不同隔离级别下的消息流,先从序列化开始,如图3所示:
事务开始(1 tx.Start);
事务协调器在内部管理事务请求(2 IgniteInternalTx);
应用写入键K1(3 tx.put(K1-V1));
事务协调器将K1写入本地事务映射(4 Put(K1));
应用写入键K2(5 tx.put(K2-V2));
事务协调器将K2写入本地事务映射(6 Put(K2));
发起事务提交请求(7 tx.commit);
事务协调器向存储K1和K2的主节点发起锁请求(8 lock(K1, TV1) and 8 lock(K2, TV1));
主节点在内部管理事务请求(9 IgniteInternalTx);
主节点向事务协调者发送一个已经准备好的确认(10 ACK);
K1和K2写入相应的主节点(11 Write(K1)和11 Write(K2));
如果没有数据冲突(即K1和K2没有被其他的应用更新),主节点确认事务提交(12 ACK)。
最后,看一下可重复读和读提交的消息流,如图4所示:
事务开始(1 tx.Start);
事务协调器在内部管理事务请求(2 IgniteInternalTx);
应用写入键K1(3 tx.put(K1-V1));
事务协调器将K1写入本地事务映射(4 Put(K1));
应用写入键K2(5 tx.put(K2-V2));
事务协调器将K2写入本地事务映射(6 Put(K2));
发起事务提交请求(7 tx.commit);
事务协调器向存储K1和K2的主节点发起锁请求(8 lock(K1, TV1) and 8 lock(K2, TV1));
主节点向事务协调者发送一个已经准备好的确认(9 ACK);
K1和K2写入相应的主节点(10 Write(K1)和10 Write(K2));
主节点在内部管理事务请求(11 IgniteInternalTx);
主节点确认事务提交(12 ACK)。
总结
在本文中,研究了Ignite支持的主要的锁模型和隔离级别,我们看到,有很大的灵活性和选择空间,本系列的后面文章中,会研究故障转移和恢复。本文译自GridGain技术布道师Akmal B. Chaudhri的博客。
相关文章推荐
- Apache Ignite事务H5牌九棋牌网站搭建架构:并发模型和隔离级别
- Apache Ignite事务架构:Ignite持久化的事务处理
- Apache Ignite事务架构:第三方持久化的事务处理
- 企业级应用架构模型-并发,事务,锁
- Apache Ignite事务架构:故障和恢复
- Apache Ignite事务架构:2阶段提交协议
- 数据库的三大并发事务问题与四大隔离级别
- apache并发模型
- [转载] 【每周推荐阅读】SEDA:高并发系统设计模型与架构
- Apache-Ignite入门实战之二 - 事务处理
- 数据库的事务、事务并发以及隔离级别
- 业务逻辑架构模式(事务脚本,表模块,活动记录,领域模型)
- 高并发分布式事务解决之道-Actor模型(附Akka与Reactor比较)
- 【读书笔记】企业应用架构模式——并发、事务与锁
- 为什么Actor模型是高并发事务的终极解决方案?
- 为什么Actor模型是高并发事务的终极解决方案?
- 并发控制:(一)并发控制、事务特性与事务的隔离级别
- mysql的架构、并发和事务简介
- [转编] 企业架构模式之领域逻辑模式的实现(事务脚本、领域模型、表模块、活动记录)
- 《七周七并发模型》读书笔记(一)并发架构分析