作者介绍:Redfox
原文链接:Ignite Transaction
1. Atomicity Mode
Ignite的支持跨cache的transaction,跨cache的transaction时要求不同cache之间的Atomicity Mode是相同的。
Cache的Atomicity Mode通过CacheConfiguration中的atomicityMode进行配置,主要支持下面三种Atomicity Mode:
TRANSACTIONAL_SNAPSHOT
支持多个操作的多个key放在一个逻辑操作(事务)中,放在一个逻辑操作中的事务要么全成功,要么全失败,支持ACID特性
同时支持K/V事务和SQL事务,使用MVCC来支持两种类型的事务
不支持near cache
TRANSACTIONAL
支持多个操作的多个key放在一个逻辑操作(事务)中,放在一个逻辑操作中的事务要么全成功,要么全失败,支持ACID特性
支持K/V事务
支持near cache
ATOMIC
所有的操作都是原子的,一次同时只执行一个操作
因为不用去做事务锁,所以性能比TRANSACTIONAL、TRANSACTIONAL_SNAPSHOT要高
批量写入(例如PutAll、RemoveAll)可能会部分成功部分失败,这个操作会抛出一个异常,异常中间包含没有写入成功的K/V
2. Concurrency & Isolation
Concurrency:Ignite支持乐观并发控制模式和悲观并发控制模式,选择什么样的并发控制模式决定了在什么时机对entry进行加锁操作。悲观并发控制模式是在prepare阶段就对事务所操作的entry进行加锁,乐观并发控制模式是在访问数据的时候,就对entry进行加锁。无论选择哪种并发控制模式,在事务commit之前都存在一个时间区间,在这个区间内,事务所操作的所有entry,都是加锁状态的。
Isolation:隔离级别定义了并发执行的事务,对同时操作的key,是如何看待和如何处理的。Ignite支持READ_COMMITED、REPEATABLE_READ、SERIALIZABLE几种隔离级别。
所以并发控制模型和隔离级别是一个事务的两个方面,下面是ignite在这两个方面的支持。
2.1 悲观事务
悲观事务在事务执行第一条读取或者写入(根据隔离级别来看是第一条读取还是写入)的时候,就会对事务中所有的entry进行加锁,直到事务commit或者rollback才释放锁定。在这个模式下,锁定首先在primary节点上进行,在prepare阶段将锁定推送到backup节点上去,悲观事务的加锁顺序很重要(避免死锁)。ignite中会对entry按照约定的顺序规则进行加锁,悲观事务下支持的隔离级别如下:
READ_COMMITED
数据读取的时候不进行加锁,也不需要将数据读取到transaction中进行缓存,在第一次写入的时候才对所有的entry加锁。如果cache配置中允许的话,可以在backup节点上读取数据。READ_COMMITED可能的问题就是会导致不可重复读,即在一个事务中读取两次数据,可能会结果不一样。
REPEATABLE_READ
在第一次读写数据时,就将数据从primary节点读取到transaction map中,并且对所有的entry进行加锁。所有在这个事务中后续对同一份数据的读取,都可以读到在这个事务里面更新的最新数据,其它并发事务不能修改这个事务里的数据,所以可以获得可重复读。
SERIALIZABLE
和REPEATABLE_READ的工作方式相同。
悲观事务下的注意点
性能:因为悲观事务需要对所有的entry提前加锁,而且加锁是要按照一定的顺序控制的。所以,类似于putAll这种操作,如果将顺序相同的key放到一个part上进行操作,可能会减少在客户端与服务端之间加锁的锁定往来次数;
Long Transaction:因为在悲观事务下,我们不能修改集群的topology,要尽量减少时间比较长的悲观事务;
读取的一致性:只有在REPEATABLE_READ和SERIALIZABLE的隔离级别下,才能保证完整的读取一致性,通过读锁避免事务在过程中更新。
2.2 乐观事务
乐观事务在两阶段提交的prepare阶段才在primary节点上对entry进行加锁,加锁之后提交到backup节点上,在commit阶段释放所拥有的锁。
READ_COMMITED
在cache中的数据修改是在发起节点上先收集所有的更改,然后在事务commit阶段进行应用,数据读取的时候不进行加锁,也不需要将数据读取到transaction中进行缓存。如果cache配置中允许的话,可以在backup节点上读取数据,READ_COMMITED可能的问题就是会导致不可重复读,即在一个事务中读取两次数据,可能会结果不一样。
REPEATABLE_READ
和READ_COMMITED的实现方式相同,区别在于REPEATABLE_READ会把所有的读取过来的数据在发起节点上进行缓存,这样所有后续这个事务操作的读取,都可以确保是local的。
SERIALIZABLE
在第一次读取某个entry的时候,保存这个entry的版本号。如果在事务commit的时候,发现这个事务操作的所有entry存在一个entry的版本号和集群当前最新的entry版本号不相同,则这个事务失败,并对这个事务的修改操作进行回滚,用户需要处理这个异常,并且重试这个事务。这个中间需要特别强调的是即使这个事务只有读操作,没有写操作,这个事务也可能失败(假设后面读取的数据和事务刚开始时的数据不相同)。
乐观事务下的注意点
SERIALIZABLE下的重试:因为乐观事务是在操作过程中发现不能操作时,返回和抛出异常。所以我们需要捕获这种异常,然后进行重试,可能导致多个乐观事务循环重试的状态,所以我们在加锁时,也要考虑对entry加锁的顺序,降低循环重试的可能性;
读取一致性:只有在SERIALIZABLE的情况下,才能保证读取的一致性。但是即使在SERIALIZABLE的隔离级别下,在COMMIT之前,都可能发生部分读的现象,所以一定需要去捕获抛出的异常,以确保读取是一致的。
3. MVCC
只有TRANSACTIONAL_SNAPSHOT的原子模式,才支持MVCC特征,这个模式同时支持SQL事务和K/V事务,这两种类型的事务都支持MVCC。
MVCC是数据被多个用户并发访问时候,控制数据一致性的方法。每个事务在启动时获得数据的一致性快照,这个事务后续的操作只能看到和修改这个快照内的数据。当事务更新一个entry时,它需要确保这个entry没有被其它事务更新,然后创建这个entry的一个新的版本。这个新版本只有在这个事务commit之后才能被其它事务可见,如果这个entry被更新了,那么这个事务就会失败。Snapshot并不是物理的,而是逻辑的。它是由集群中的某个节点进行管理这些活动事务(MVCC-coordinator),这个节点追踪所有的活动事务,并且在每个事务结束时,都会通知到该节点。所有打开MVCC的cache操作,都需要从这个节点上获得快照数据。
目前TRANSACTIONAL_SNAPSHOT只支持悲观事务和REPEATABLE_READ隔离级别。
并发更新
当在一个事务里面(事务A),先读取一个entry,再更新这个entry过程中,可能存在另外一个事务在两个操作中间将这个entry更新的情况发生,如果这种情况发生,那么事务A会被置为“rollback only”(只允许回滚)的状态,ignite会抛出异常和设置SQL的状态码,让用户来处理这种状态,通常这个事务就需要重试。
限制
跨cache事务
在同一个事务里面要求所有的cache都设置为TRANSACTIONAL_SNAPSHOT模式,因此,如果需要在一个SQL事务中操作多个table,那么所有的table创建时,也要配置为TRANSACTIONAL_SNAPSHOT模式
嵌套事务
Ignite支持3种方案来处理嵌套事务,方案在JDBC、ODBC的连接参数中配置。
ERROR方案:当碰到嵌套事务,则抛出错误,同时外面的事务进行回滚,默认采用这种方案;
COMMIT方案:外围事务进行提交,嵌套事务在碰到COMMIT语句时进行提交,外围事务剩下的语句采用隐含事务模式;
IGNORE: 嵌套事务的BEGIN语句被忽略,嵌套事务中的语句被和外围事务一样执行,碰到COMMIT之后,进行事务提交,外围事务剩余的语句采用隐含事务模式。
持续查询
如果有其它操作在更新数据,那么持续查询获得的结果可能是过期的数据,因为更新数据需要时间通知到MVCC-coordinator节点上;
一个事务同时更新的记录数会被限制,因为可能造成内存泄漏,默认限制为20000条;
MVCC模式不支持:near cache、超时策略、事件通知、cache解析器、第三方持久化、堆内cache。
4. Two Phase Commit
两阶段提交
在分布式的ignite中,一个事务中的操作可能牵涉到修改多个partition上的数据,一个partition里面也存在主节点和备份节点,要保持一个事务中对所有节点的数据修改是一致的。ignite采用两阶段提交的方案来进行事务操作,两阶段提交事务中存在3个角色:
事务协调者(Transaction Coordinator):事务发起节点,它来控制事务的两阶段提交过程。
主节点(Primary Partition):ignite中某个partition的主节点,数据读写操作的主要对象。
备份节点(Backup Partition):ignite中某个partition数据备份的所在节点。
5. Near Node & Remote Node
在ignite中,Transactional coordinator也被称为Near Node。Near Node控制整个事务操作的全部流程,包括发起事务、跟踪事务状态、发起prepare、发起commit以及等待事务完成。
Near Node通常是也是集群的client节点。
6. Transaction Lifecycle
在ignite中,事务由tx.start()发起,然后在Near Node上创建Transaction的上下文环境,同时在Near Node上还要做以下事情:
1)为Transaction分配一个事务ID;
2)记录Transaction开始的时间;
3)记录当前topology的version、状态和信息。
将Transaction设置为active状态,然后执行读写操作,再执行Commit操作,完成事务。Transaction commit之后,开始执行两阶段提交,在prepare阶段,primary完成时需要做做以下事情:
1)检查保存在Transaction上下文中的topology version和当前的topology version是否相同;
2)获得所有的锁;
3)创建一个DHT上下文,保存所有的数据;
4)等待或者跳过备份节点完成prepare;
5)Near Node发起执行commit操作;
6)等待所有节点完成commit操作。
7. Pessimistic & READ_COMMITED
在这种组合模式下,读取数据(例如get、getAll等操作),是不会对entry进行加锁的。所以,可能存在刚开始读到的某个entry的数据,和事务完成(commit)时读到的某个entry的数据是不相同的。
在收到对某个entry的修改操作时,会对这个entry进行锁定操作。当发起commit之前,事务已经完成对所有该事务需要修改的数据的锁定,commit时,才会对primary或者backup上的二数据发起真正的修改操作。
锁的释放只有在整个事务完成之后才进行释放。
8. Pessimistic & (SERIALIZABLE | REPEATABLE_READ)
在这两种组合模式下,Near Node发现读取数据(例如get、getAll等操作)时,就会对读取的数据进行加锁操作,同样的数据修改操作也会对读取的数据进行加锁操作。
锁的释放只有在整个事务完成之后才进行释放。
9. Optimistic & (READ_COMMITED|REPEATABLE_READ)
在这两种并发控制组合模式下,数据锁定在prepare阶段才会发生(事务的commit,两阶段提交的prepare),相对于悲观并发控制模式(Near Node上发现有读写即进行锁定)来说,延后了对entry的锁定时间。在prepare阶段,不会比较entry的版本号。
10. Optimistic & SERIALIZABLE
在这两种并发控制组合模式下,数据锁定在prepare阶段才会发生(事务的commit,两阶段提交的prepare),相对于悲观并发控制模式(Near Node上发现有读写即进行锁定)来说,延后了对entry的锁定时间。在prepare阶段,会比较entry的版本号,与事务刚开始时的entry版本号。如果在prepare阶段发现版本号与事务刚开始时的版本号不相同,那么在prepare阶段就失败。
11. Lock Timeout
Ignite允许设置一个事务的超时时间,在这种设定模式下,当事务的执行事件超过约定的超时时间时,事务执行失败,需要进行rollback
悲观并发控制模式下:每次对entry进行锁定,都会比较事务执行是否超时
乐观并发控制模式下:只有在prepare阶段进行锁定时,才比较事务执行是否超时
所有参与事务的节点都会检查超时。如果超时,那么会在事务中设计一个标记,以方便NearNode执行事务撤销(回滚操作)
12. Backup Node Failure
如果一个backup节点损坏,那么事务仍旧会在剩余的节点上完成执行,不会对当前事务的执行产生任何影响。
在事务执行完后,会为这个partition选择一个新的backup节点。
13. Primary Node Failure On Prepare
如果primary节点在prepare阶段失效,那么系统会抛出一个异常给到客户端,客户端再将异常给到应用程序,由应用程序来决定是重新执行该事务还是做其它工作。
14. Primary Node Failure On Commit
如果primary节点在prepare节点完成之后失效,那么Near Node会等待backup节点发送过来的消息。
Backup节点发现primary节点失效情况下,会发送消息给到NearNode节点。如果backup已经完成了这个事务需要完成的所有修改操作,则Near Node会继续完成这个事务。
同时topology开始发生改变,并选择一个新的primary来代替失效的primary。
15. Coordinator(Near Node) Failure
如果Near Node失效,那么处理过程会比较复杂,因为系统中可能有的节点已经完成了commit,有的节点还没有完成commit,而且系统中的transaction的状态目前是没有保存的。
系统中的primary节点互相交换信息,只要有一个primary节点没有完成commit,则触发对这个transaction的回滚操作,参与这个事务的所有节点进行回滚。