Ignite数据流处理

数据流处理

#1.概述

Ignite提供了一个数据流API,可用于将大量连续的数据流注入Ignite集群,数据流API支持容错和线性扩展,并为注入Ignite的数据提供了至少一次保证,这意味着每个条目至少会被处理一次。

数据通过与缓存关联的数据流处理器流式注入到缓存中。数据流处理器自动缓冲数据并将其分组成批次以提高性能,并将其并行发送到多个节点。

数据流API提供以下功能:

  • 添加到数据流处理器的数据将在节点之间自动分区和分布;
  • 可以以并置方式并发处理数据;
  • 客户端可以在注入数据时对数据执行并发SQL查询。

#2.数据流处理器

数据流处理器与某个缓存关联,并提供用于将数据注入缓存的接口。

在典型场景中,用户拿到数据流处理器之后,会使用其中某个方法将数据流式注入缓存中,而Ignite根据分区规则对数据条目进行批处理,从而避免不必要的数据移动。

拿到某个缓存的数据流处理器的方法如下:

  • Java
  • C#/.NET
// Get the data streamer reference and stream data.
try (IgniteDataStreamer<Integer, String> stmr = ignite.dataStreamer("myCache")) {
    // Stream entries.
    for (int i = 0; i < 100000; i++)
        stmr.addData(i, Integer.toString(i));
}
System.out.println("dataStreamerExample output:" + cache.get(99999));

在Ignite的Java版本中,数据流处理器是IgniteDataStreamer接口的实现,IgniteDataStreamer提供了一组addData(…​)方法来向缓存中添加键-值对,完整的方法列表,可以参见IgniteDataStreamer的javadoc。

#3.覆写已有的数据

数据流处理器默认不会覆盖已有的数据,通过将allowOverwrite属性配置为true,可以修改该行为。

  • Java
  • C#/.NET
stmr.allowOverwrite(true);

提示

如果allowOverwrite配置为false(默认),更新不会传播到外部存储(如果开启)。

#4.处理数据

如果需要在添加新数据之前执行自定义逻辑,则可以使用数据流接收器。在将数据存储到缓存之前,数据流接收器用于以并置方式处理数据,其中实现的逻辑会在存储数据的节点上执行。

  • Java
  • C#/.NET
try (IgniteDataStreamer<Integer, String> stmr = ignite.dataStreamer("myCache")) {

    stmr.allowOverwrite(true);

    stmr.receiver((StreamReceiver<Integer, String>) (cache, entries) -> entries.forEach(entry -> {

        // do something with the entry

        cache.put(entry.getKey(), entry.getValue());
    }));
}

提示

注意数据流接收器不会自动将数据注入缓存,需要显式地调用put(…​)方法之一。

警告

要在远端节点执行的接收器类定义必须在该节点可用,这可通过2种方式实现:

#4.1.StreamTransformer

StreamTransformerStreamReceiver的简单实现,用于更新流中的数据。数据流转换器利用了并置的特性,并在将要存储数据的节点上更新数据。

在下面的示例中,使用StreamTransformer为文本流中找到的每个不同单词增加一个计数:

  • Java
  • C#/.NET
String[] text = { "hello", "world", "hello", "Ignite" };
CacheConfiguration<String, Long> cfg = new CacheConfiguration<>("wordCountCache");

IgniteCache<String, Long> stmCache = ignite.getOrCreateCache(cfg);

try (IgniteDataStreamer<String, Long> stmr = ignite.dataStreamer(stmCache.getName())) {
    // Allow data updates.
    stmr.allowOverwrite(true);

    // Configure data transformation to count instances of the same word.
    stmr.receiver(StreamTransformer.from((e, arg) -> {
        // Get current count.
        Long val = e.getValue();

        // Increment count by 1.
        e.setValue(val == null ? 1L : val + 1);

        return null;
    }));

    // Stream words into the streamer cache.
    for (String word : text)
        stmr.addData(word, 1L);

}

#4.2.StreamVisitor

StreamVisitor也是StreamReceiver的一个方便实现,它会访问流中的每个键-值对,但不会更新缓存。如果键-值对需要存储在缓存内,那么需要显式地调用任意的put(...)方法。

在下面的示例中,有两个缓存:marketDatainstruments,收到market数据的瞬间就会将它们放入marketData缓存的流处理器,映射到某market数据的集群节点上的marketData的流处理器的StreamVisitor就会被调用,在分别收到market数据后就会用最新的市场价格更新instrument缓存。

注意,根本不会更新marketData缓存,它一直是空的,只是直接在数据将要存储的集群节点上简单利用了market数据的并置处理能力。

  • Java
  • C#/.NET
static class Instrument {
    final String symbol;
    Double latest;
    Double high;
    Double low;

    public Instrument(String symbol) {
        this.symbol = symbol;
    }

}

static Map<String, Double> getMarketData() {
    //populate market data somehow
    return new HashMap<>();
}

@Test
void streamVisitorExample() {
    try (Ignite ignite = Ignition.start()) {
        CacheConfiguration<String, Double> mrktDataCfg = new CacheConfiguration<>("marketData");
        CacheConfiguration<String, Instrument> instCfg = new CacheConfiguration<>("instruments");

        // Cache for market data ticks streamed into the system.
        IgniteCache<String, Double> mrktData = ignite.getOrCreateCache(mrktDataCfg);

        // Cache for financial instruments.
        IgniteCache<String, Instrument> instCache = ignite.getOrCreateCache(instCfg);

        try (IgniteDataStreamer<String, Double> mktStmr = ignite.dataStreamer("marketData")) {
            // Note that we do not populate the 'marketData' cache (it remains empty).
            // Instead we update the 'instruments' cache based on the latest market price.
            mktStmr.receiver(StreamVisitor.from((cache, e) -> {
                String symbol = e.getKey();
                Double tick = e.getValue();

                Instrument inst = instCache.get(symbol);

                if (inst == null)
                    inst = new Instrument(symbol);

                // Update instrument price based on the latest market tick.
                inst.high = Math.max(inst.high, tick);
                inst.low = Math.min(inst.low, tick);
                inst.latest = tick;

                // Update the instrument cache.
                instCache.put(symbol, inst);
            }));

            // Stream market data into the cluster.
            Map<String, Double> marketData = getMarketData();
            for (Map.Entry<String, Double> tick : marketData.entrySet())
                mktStmr.addData(tick);
        }
    }
}

#5.配置数据流处理器线程池大小

数据流处理器线程池专用于处理来自数据流处理器的消息。

默认池大小为max(8, CPU总核数),使用IgniteConfiguration.setDataStreamerThreadPoolSize(…​)可以改变线程池的大小。

  • XML
  • Java
IgniteConfiguration cfg = new IgniteConfiguration();
cfg.setDataStreamerThreadPoolSize(10);

Ignite ignite = Ignition.start(cfg);

键-值API

#1.基本缓存操作

#1.1.获取缓存的实例

在缓存上的所有操作都是通过IgniteCache实例进行的,也可以在已有的缓存上拿到IgniteCache,也可以动态创建。

  • Java
  • C#/.NET
  • C++
Ignite ignite = Ignition.ignite();

// Obtain an instance of the cache named "myCache".
// Note that different caches may have different generics.
IgniteCache<Integer, String> cache = ignite.cache("myCache");

#1.2.动态创建缓存

动态创建缓存方式如下:

  • Java
  • C#/.NET
  • C++
Ignite ignite = Ignition.ignite();

CacheConfiguration<Integer, String> cfg = new CacheConfiguration<>();

cfg.setName("myNewCache");
cfg.setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL);

// Create a cache with the given name if it does not exist.
IgniteCache<Integer, String> cache = ignite.getOrCreateCache(cfg);

关于缓存的配置参数,请参见缓存配置章节的内容。

在基线拓扑变更过程中调用创建缓存的方法,会抛出org.apache.ignite.IgniteCheckedException异常:

javax.cache.CacheException: class org.apache.ignite.IgniteCheckedException: Failed to start/stop cache, cluster state change is in progress.
        at org.apache.ignite.internal.processors.cache.GridCacheUtils.convertToCacheException(GridCacheUtils.java:1323)
        at org.apache.ignite.internal.IgniteKernal.createCache(IgniteKernal.java:3001)
        at org.apache.ignite.internal.processors.platform.client.cache.ClientCacheCreateWithNameRequest.process(ClientCacheCreateWithNameRequest.java:48)
        at org.apache.ignite.internal.processors.platform.client.ClientRequestHandler.handle(ClientRequestHandler.java:51)
        at org.apache.ignite.internal.processors.odbc.ClientListenerNioListener.onMessage(ClientListenerNioListener.java:173)
        at org.apache.ignite.internal.processors.odbc.ClientListenerNioListener.onMessage(ClientListenerNioListener.java:47)
        at org.apache.ignite.internal.util.nio.GridNioFilterChain$TailFilter.onMessageReceived(GridNioFilterChain.java:278)
        at org.apache.ignite.internal.util.nio.GridNioFilterAdapter.proceedMessageReceived(GridNioFilterAdapter.java:108)
        at org.apache.ignite.internal.util.nio.GridNioAsyncNotifyFilter$3.body(GridNioAsyncNotifyFilter.java:96)
        at org.apache.ignite.internal.util.worker.GridWorker.run(GridWorker.java:119)

        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
        at java.base/java.lang.Thread.run(Thread.java:834)

如果拿到这个异常,可以进行重试。

#1.3.销毁缓存

要在整个集群中删除一个缓存,需要调用destroy()方法:

  • Java
Ignite ignite = Ignition.ignite();

IgniteCache<Long, String> cache = ignite.cache("myCache");

cache.destroy();

#1.4.原子化操作

拿到缓存实例后,就可以对其进行读写操作:

  • Java
  • C#/.NET
  • C++
IgniteCache<Integer, String> cache = ignite.cache("myCache");

// Store keys in the cache (the values will end up on different cache nodes).
for (int i = 0; i < 10; i++)
    cache.put(i, Integer.toString(i));

for (int i = 0; i < 10; i++)
    System.out.println("Got [key=" + i + ", val=" + cache.get(i) + ']');

提示

putAll()putAll()这样的批量操作方法,是以原子化的模式按顺序执行,可能部分失败。发生这种情况时,会抛出包含了更新失败数据列表的CachePartialUpdateException异常。 如果希望在一个操作中更新条目的集合,建议考虑使用事务

下面是更多基本缓存操作的示例:

  • Java
  • C#/.NET
  • C++
// Put-if-absent which returns previous value.
String oldVal = cache.getAndPutIfAbsent(11, "Hello");

// Put-if-absent which returns boolean success flag.
boolean success = cache.putIfAbsent(22, "World");

// Replace-if-exists operation (opposite of getAndPutIfAbsent), returns previous
// value.
oldVal = cache.getAndReplace(11, "New value");

// Replace-if-exists operation (opposite of putIfAbsent), returns boolean
// success flag.
success = cache.replace(22, "Other new value");

// Replace-if-matches operation.
success = cache.replace(22, "Other new value", "Yet-another-new-value");

// Remove-if-matches operation.
success = cache.remove(11, "Hello");

#1.5.异步执行

大多数缓存操作方法都有对应的异步执行模式,方法名带有Async后缀。

  • Java
  • C#/.NET
  • C++
// a synchronous get
V get(K key);

// an asynchronous get
IgniteFuture<V> getAsync(K key);

异步操作会返回一个代表操作结果的对象,可以以阻塞或非阻塞的方式,等待操作的完成。

以非阻塞的方式等待结果,可以使用IgniteFuture.listen()IgniteFuture.chain()方法注册一个闭包,其会在操作完成后被调用。

  • Java
  • C#/.NET
  • C++
IgniteCompute compute = ignite.compute();

// Execute a closure asynchronously.
IgniteFuture<String> fut = compute.callAsync(() -> "Hello World");

// Listen for completion and print out the result.
fut.listen(f -> System.out.println("Job result: " + f.get()));

闭包执行和线程池

如果在将闭包传递给IgniteFuture.listen()IgniteFuture.chain()方法时已完成异步操作,则该闭包由调用线程同步执行。否则当操作完成时,闭包将异步执行。

根据操作的类型,闭包将被系统线程池中的线程(异步缓存操作)或公共线程池中的线程(异步计算操作)调用。因此应避免在闭包内部调用同步缓存和计算操作,因为由于线程池不足,它可能导致死锁。

为了实现异步计算操作的嵌套执行,可以利用自定义线程池

#2.使用二进制对象

#2.1.概述

在Ignite中,数据以二进制格式存储,然后在每次读取时再反序列化为对象,不过可以直接操作二进制对象避免反序列化。

二进制对象是缓存数据的二进制表示的包装器,每个二进制对象都有field(name)方法(返回对应字段的值)和type()方法(提取对象的类型信息)。当只需要处理对象的部分字段而不需要反序列化整个对象时,二进制对象会很有用。

处理二进制对象时不需要具体的类定义,不重启集群就可以动态修改对象的结构

在所有支持的平台上,二进制对象格式都是统一的,包括Java、.NET和C++。可以启动一个Java版Ignite集群,然后使用.NET和C++客户端接入集群,然后在这些客户端上使用二进制对象而不需要持有类定义。

限制

  1. 在内部二进制对象的类型和字段以ID来标识,该ID由对应字符串名字的哈希值计算得出,这意味着属性或者类型不能有同样的名字哈希,因此不允许使用具有相同名字哈希的字段或类型。但是,可以通过配置提供自定义的ID生成实现
  2. 同样的原因,BinaryObject格式在类的不同层次上也不允许有同样的属性名;
  3. 如果类实现了Externalizable接口,Ignite会使用OptimizedMarshallerOptimizedMarshaller会使用writeExternal()readExternal()来进行类对象的序列化和反序列化,这需要将实现Externalizable的类加入服务端节点的类路径中。

#2.2.启用缓存的二进制模式

当从缓存中拿数据时,默认返回的是反序列化格式,要处理二进制格式,需要使用withKeepBinary()方法拿到缓存的实例,这个实例会尽可能返回二进制格式的对象。

  • Java
  • C#/.NET
// Create a regular Person object and put it into the cache.
Person person = new Person(1, "FirstPerson");
ignite.cache("personCache").put(1, person);

// Get an instance of binary-enabled cache.
IgniteCache<Integer, BinaryObject> binaryCache = ignite.cache("personCache").withKeepBinary();
BinaryObject binaryPerson = binaryCache.get(1);

注意并不是所有的对象都会转为二进制对象格式,下面的类不会进行转换(即toBinary(Object)方法返回原始对象,以及这些类的实例存储不会发生变化):

  • 所有的基本类型(byteint等)及其包装类(ByteInteger等);
  • 基本类型的数组(byte[]int[]等);
  • String及其数组;
  • UUID及其数组;
  • Date及其数组;
  • Timestamp及其数组;
  • Enum及其数组;
  • 对象的映射、数组和集合(但如果它们是可以转成二进制的,则内部对象将被重新转换)。

#2.3.创建和修改二进制对象

二进制对象实例是不可变的,要更新字段或者创建新的二进制对象,需要使用二进制对象的建造器工具类,其可以在没有对象的类定义的前提下,修改二进制对象的字段。

限制

  • 无法修改已有字段的类型;
  • 无法变更枚举值的顺序,也无法在枚举值列表的开始或者中部添加新的常量,但是可以在列表的末尾添加新的常量。

二进制对象建造器实例获取方式如下:

  • Java
  • C#/.NET
BinaryObjectBuilder builder = ignite.binary().builder("org.apache.ignite.snippets.Person");

builder.setField("id", 2L);
builder.setField("name", "SecondPerson");

binaryCache.put(2, builder.build());

通过这个方式创建的建造器没有任何字段,调用setField(…​)方法可以添加字段:

通过调用toBuilder()方法,也可以从一个已有的二进制对象上获得建造器实例,这时该二进制对象的所有字段都会复制到该建造器中。

在下面的示例中,会在服务端通过EntryProcessor机制更新一个对象,而不需要在该节点部署该对象类定义,也不需要完整对象的反序列化。

  • Java
// The EntryProcessor is to be executed for this key.
int key = 1;
ignite.cache("personCache").<Integer, BinaryObject>withKeepBinary().invoke(key, (entry, arguments) -> {
    // Create a builder from the old value.
    BinaryObjectBuilder bldr = entry.getValue().toBuilder();

    //Update the field in the builder.
    bldr.setField("name", "Ignite");

    // Set new value to the entry.
    entry.setValue(bldr.build());

    return null;
});

#2.4.二进制类型和二进制字段

二进制对象持有其表示的对象的类型信息,类型信息包括字段名、字段类型和关联字段名。

每个字段的类型通过一个BinaryField对象来表示,获得BinaryField对象后,如果需要从集合中的每个对象读取相同的字段,则可以多次重用该对象。重用BinaryField对象比直接从每个二进制对象读取字段值要快,下面是使用二进制字段的示例:

Collection<BinaryObject> persons = getPersons();

BinaryField salary = null;
double total = 0;
int count = 0;

for (BinaryObject person : persons) {
    if (salary == null) {
        salary = person.type().field("salary");
    }

    total += (float) salary.value(person);
    count++;
}

double avg = total / count;

#2.5.二进制对象的调整建议

Ignite为给定类型的每个二进制对象保留一个模式,该模式指定对象中的字段及其顺序和类型。模式在整个集群中复制,具有相同字段但顺序不同的二进制对象被认为具有不同的模式,因此建议以相同的顺序往二进制对象中添加字段。

空字段通常需要5个字节来存储,字段ID4个字节,字段长度1个字节。在内存方面,最好不要包含字段,也不要包含空字段。但是,如果不包括字段,则Ignite会为此对象创建一个新模式,该模式与包含该字段的对象的模式不同。如果有多个字段以随机组合设置为null,那么Ignite会为每种组合维护一个不同的二进制对象模式,这样Java堆可能会被二进制对象模式耗尽。最好为二进制对象提供几个模式,并以相同的顺序设置相同类型的相同字段集。通过提供相同的字段集(即使具有空值)来创建二进制对象时,选择其中一个,这也是需要为空字段提供字段类型的原因。

如果有一个子集的字段是可选的,但要么全部不存在,要么全部存在,那么也可以嵌套二进制对象,可以将它们放在单独的二进制对象中,该对象存储在父对象的字段下,或者设置为null。

如果有大量字段,这些字段在任何组合中都是可选的,并且通常为空,则可以将其存储在映射字段中,值对象中将有几个固定字段,还有一个映射用于其他属性。

#2.6.配置二进制对象

在绝大多数场景中,无需配置二进制对象。但是如果需要更改类型和字段ID的生成或插入自定义序列化器,则可以通过配置来实现。

二进制对象的类型和字段由其ID标识,该ID由相对应的字符串名计算为哈希值,并将其存储在每个二进制对象中,可以在配置中定义自己的ID生成实现。

名字到ID的转换分为两个步骤。首先,由名字映射器转换类型名(类名)或字段名,然后由ID映射器计算ID。可以指定全局名字映射器,全局ID映射器和全局二进制序列化器,以及每个类型的映射器和序列化器。每个类型的配置均支持通配符,这时所提供的配置将应用于与类型名字模板匹配的所有类型。

  • XML
  • Java
  • C#/.NET
IgniteConfiguration igniteCfg = new IgniteConfiguration();

BinaryConfiguration binaryConf = new BinaryConfiguration();
binaryConf.setNameMapper(new MyBinaryNameMapper());
binaryConf.setIdMapper(new MyBinaryIdMapper());

BinaryTypeConfiguration binaryTypeCfg = new BinaryTypeConfiguration();
binaryTypeCfg.setTypeName("org.apache.ignite.snippets.*");
binaryTypeCfg.setSerializer(new ExampleSerializer());

binaryConf.setTypeConfigurations(Collections.singleton(binaryTypeCfg));

igniteCfg.setBinaryConfiguration(binaryConf);

#3.使用扫描查询

#3.1.概述

IgniteCache有几个查询方法,他们会接收Query类的子类,然后返回一个QueryCursor

Query表示在缓存上执行的分页查询的抽象,页面大小通过Query.setPageSize(…​)进行配置,默认值为1024

QueryCursor表示结果集,可以透明地按页迭代。当用户迭代到页尾时,QueryCursor会自动在后台请求下一页。对于不需要分页的场景,可以使用QueryCursor.getAll()方法,其会拿到所有的数据,并将其存储在一个集合中。

关闭游标

调用QueryCursor.getAll()方法时,游标会自动关闭。如果在循环中迭代游标,或者显式拿到Iterator,必须手动关闭游标,或者使用try-with-resources语句。

#3.2.执行扫描查询

扫描查询是以分布式的方式从缓存中获取数据的简单搜索查询,如果执行时没有参数,扫描查询会从缓存中获取所有数据。

  • Java
  • C#/.NET
  • C++
IgniteCache<Integer, Person> cache = ignite.getOrCreateCache("myCache");

QueryCursor<Cache.Entry<Integer, Person>> cursor = cache.query(new ScanQuery<>());

如果指定了谓语,扫描查询会返回匹配谓语的数据,谓语应用于远端节点:

  • Java
  • C#/.NET
IgniteCache<Integer, Person> cache = ignite.getOrCreateCache("myCache");

// Find the persons who earn more than 1,000.
IgniteBiPredicate<Integer, Person> filter = (key, p) -> p.getSalary() > 1000;

try (QueryCursor<Cache.Entry<Integer, Person>> qryCursor = cache.query(new ScanQuery<>(filter))) {
    qryCursor.forEach(
            entry -> System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue()));
}

扫描查询还支持可选的转换器闭包,可以在数据返回之前在服务端转换数据,比如,当只想从大对象中获取少量字段,以最小化网络传输时,这个功能就很有用。下面的示例显示如何只返回键,而不返回值:

IgniteCache<Integer, Person> cache = ignite.getOrCreateCache("myCache");

// Get only keys for persons earning more than 1,000.
List<Integer> keys = cache.query(new ScanQuery<>(
        // Remote filter
        (IgniteBiPredicate<Integer, Person>) (k, p) -> p.getSalary() > 1000),
        // Transformer
        (IgniteClosure<Cache.Entry<Integer, Person>, Integer>) Cache.Entry::getKey).getAll();

#3.3.本地扫描查询

扫描查询默认是分布到所有节点上的,不过也可以只在本地执行查询,这时查询只会处理本地节点(查询执行的节点)上存储的数据。

  • Java
  • C#/.NET
  • C++
QueryCursor<Cache.Entry<Integer, Person>> cursor = cache
        .query(new ScanQuery<Integer, Person>().setLocal(true));

#3.4.相关主题

#4.读修复

警告

这是个试验性API。

读修复是指在正常读取操作期间修复主备数据之间不一致的技术。当用户操作读取了某个或某些键时,Ignite会检查给定键在所有备份副本中的值。

读修复模式旨在保持一致性。不过由于检查了备份副本,因此读操作的成本增加了约2倍。通常不建议一直使用此模式,而应一次性使用。

要启用读修复模式,需要获取一个开启了读修复的缓存实例,如下所示:

IgniteCache<Object, Object> cache = ignite.cache("my_cache").withReadRepair();

Object value = cache.get(10);

一致性检查与下面的缓存配置不兼容:

  • 没有备份的缓存;
  • 本地缓存;
  • 近缓存;
  • 开启通读的缓存。

#4.1.事务化缓存

拓扑中的值将替换为最新版本的值。

  • 对于配置为TransactionConcurrency.OPTIMISTIC并发模型或TransactionIsolation.READ_COMMITTED隔离级别的事务自动处理;
  • 对于配置为TransactionConcurrency.PESSIMISTIC并发模型和TransactionIsolation.READ_COMMITTED隔离级别的事务,在commit()阶段自动处理;

当检测到备份不一致时,Ignite将生成一个违反一致性事件(如果在配置中启用了该事件),通过监听该事件可以获取有关不一致问题的通知。关于如果进行事件监听,请参见使用事件的介绍。

如果事务中已经缓存了值,则读修复不能保证检查所有副本。例如,如果使用非TransactionIsolation.READ_COMMITTED隔离级别,并且已经读取了该值或执行了写入操作,则将获得缓存的值。

#4.2.原子化缓存

如果发现差异,则抛出违反一致性异常。

由于原子化缓存的性质,可以观察到假阳性结果。比如在缓存加载中尝试检查一致性可能会触发违反一致性异常。读修复的实现会尝试检查给定键三次,尝试次数可以通过IGNITE_NEAR_GET_MAX_REMAPS系统属性来修改。

注意不会为原子缓存记录违反一致性事件。

Ignite事务

#1.概述

要为某个缓存开启事务支持,需要在缓存配置中将atomicityMode设置为TRANSACTIONAL,具体请参见原子化模式

事务可以将多个缓存操作,可能对应一个或者多个键,组合为一个单个原子事务,这些操作在没有任何其他交叉操作的情况下执行,或全部成功或全部失败,没有部分成功的状态。

在缓存配置中可以为缓存开启事务支持:

  • XML
  • Java
  • C#/.NET
CacheConfiguration cacheCfg = new CacheConfiguration();

cacheCfg.setName("cacheName");

cacheCfg.setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL);

IgniteConfiguration cfg = new IgniteConfiguration();

cfg.setCacheConfiguration(cacheCfg);

// Optional transaction configuration. Configure TM lookup here.
TransactionConfiguration txCfg = new TransactionConfiguration();

cfg.setTransactionConfiguration(txCfg);

// Start a node
Ignition.start(cfg);

#2.执行事务

键-值API为开启和完成事务以及获取和事务有关的指标,提供了一个接口,该接口可以通过Ignite实例获得:

  • Java
  • C#/.NET
  • C++
Ignite ignite = Ignition.ignite();

IgniteTransactions transactions = ignite.transactions();

try (Transaction tx = transactions.txStart()) {
    Integer hello = cache.get("Hello");

    if (hello == 1)
        cache.put("Hello", 11);

    cache.put("World", 22);

    tx.commit();
}

#3.并发模型和隔离级别

当原子化模式配置为TRANSACTIONAL时,Ignite对事务支持乐观悲观并发模型。并发模型决定了何时获得一个条目级的事务锁-在访问数据时或者在prepare阶段。锁定可以防止对一个对象的并发访问。比如,当试图用悲观锁更新一个ToDo列表项时,服务端会在该对象上置一个锁,在提交或者回滚该事务之前,其它的事务或者操作都无法更新该条目。不管在一个事务中使用那种并发模型,在提交之前都存在事务中的所有条目被锁定的时刻。

隔离级别定义了并发事务如何"看"以及处理针对同一个键的操作。Ignite支持READ_COMMITTEDREPEATABLE_READSERIALIZABLE隔离级别。

并发模型和隔离级别的所有组合都是可以同时使用的。下面是针对Ignite提供的每一个并发-隔离组合的行为和保证的描述。

#3.1.悲观事务

PESSIMISTIC事务中,锁是在第一次读或者写访问期间获得(取决于隔离级别)然后被事务持有直到其被提交或者回滚。该模式中,锁首先在主节点获得然后在准备阶段提升至备份节点。下面的隔离级别可以配置为PESSIMISTIC并发模型。

  • READ_COMMITTED:数据被无锁地读取并且不会被事务本身缓存。如果缓存配置允许,数据是可能从一个备份节点中读取的。在这个隔离级别中,可以有所谓的非可重复读,因为当在自己的事务中读取数据两次时,一个并发事务可以改变该数据。锁只有在第一次写访问时才会获得(包括EntryProcessor调用)。这意味着事务中已经读取的一个条目在该事务提交时可能有一个不同的值,这种情况是不会抛出异常的。
  • REPEATABLE_READ:获得条目锁以及第一次读或者写访问时从主节点获得数据,然后就存储在本地事务映射中。之后对同一数据的所有连续访问都是本地化的,并且返回最后一次读或者被更新的事务值。这意味着没有其它的并发事务可以改变锁定的数据,这样就获得了事务的可重复读。
  • SERIALIZABLE:在PESSIMISTIC模式中,这个隔离级别与REPEATABLE_READ是一样的工作方式。

注意,在PESSIMISTIC模式中,锁的顺序是很重要的。此外Ignite可以按照指定的顺序依次并且准确地获得锁。

拓扑变化约束

注意,如果至少获取了一个PESSIMISTIC事务锁,则在提交或回滚事务之前,将无法更改缓存拓扑。因此应该避免长时间持有事务锁。

#3.2.乐观事务

OPTIMISTIC事务中,条目锁是在二阶段提交的准备阶段从主节点获得的,然后提升至备份节点,该锁在事务提交时被释放。如果回滚事务没有试图做提交,是不会获得锁的。下面的隔离级别可以与OPTIMISTIC并发模型配置在一起。

  • READ_COMMITTED:应该作用于缓存的改变是在源节点上收集的,然后事务提交后生效。事务数据无锁地读取并且不会在事务中缓存。如果缓存配置允许,该数据是可能从备份节点中读取的。在这个隔离级别中,可以有一个所谓的非可重复读,因为在自己的事务中读取数据两次时另一个事务可以修改数据。这个模式组合在第一次读或者写操作后如果条目值被修改是不会做校验的,并且不会抛出异常。
  • REPEATABLE_READ:这个隔离级别的事务的工作方式类似于OPTIMISTICREAD_COMMITTED的事务,只有一个不同:读取值缓存于发起节点并且所有的后续读保证都是本地化的。这个模式组合在第一次读或者写操作后如果条目值被修改是不会做校验的,并且不会抛出异常。
  • SERIALIZABLE:在第一次读访问之后会存储一个条目的版本,如果Ignite引擎检测到发起事务中的条目只要有一个被修改,Ignite就会在提交阶段放弃该事务,这是在提交阶段对集群内的事务中记载的条目的版本进行内部检查实现的。简而言之,这意味着Ignite如果在一个事务的提交阶段检测到一个冲突,就会放弃这个事务并且抛出TransactionOptimisticException异常以及回滚已经做出的任何改变,开发者应该处理这个异常并且重试该事务。
  • Java
  • C#/.NET
  • C++
CacheConfiguration<Integer, String> cfg = new CacheConfiguration<>();
cfg.setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL);
cfg.setName("myCache");
IgniteCache<Integer, String> cache = ignite.getOrCreateCache(cfg);

// Re-try the transaction a limited number of times.
int retryCount = 10;
int retries = 0;

// Start a transaction in the optimistic mode with the serializable isolation
// level.
while (retries < retryCount) {
    retries++;
    try (Transaction tx = ignite.transactions().txStart(TransactionConcurrency.OPTIMISTIC,
            TransactionIsolation.SERIALIZABLE)) {
        // modify cache entries as part of this transaction.
        cache.put(1, "foo");
        cache.put(2, "bar");
        // commit the transaction
        tx.commit();

        // the transaction succeeded. Leave the while loop.
        break;
    } catch (TransactionOptimisticException e) {
        // Transaction has failed. Retry.
    }
}

这里另外一个需要注意的重要的点是,即使一个条目只是简单地读取(没有改变,cache.put(...)),一个事务仍然可能失败,因为该条目的值对于发起事务中的逻辑很重要。

注意,对于READ_COMMITTEDREPEATABLE_READ

  • 18
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值