之前的文章我們其實已經用到了兩種不同的方式訪問Ignite中的資料。一種方式是第一篇文章中提到通過JDBC客戶端用SQL訪問資料,在這篇文章中我們也會看到不使用JDBC,如何通過Ignite API用SQL訪問資料。還有用一種方式我稱之為cache API, 即用get/put來訪問資料。Ignite實現了JCache(JSR 107)標準,所以除了基本的cache操作外,我們也會介紹一些cache的原子操作和EntryProcessor的使用。
Cache API
Ignite提供了類似Map的API用來操作快取上的資料,只不過Ignite的實現把這個Map上的資料分佈在多個節點上,並且保證了這些操作是多執行緒/程式安全的。我們可以簡單的在多個節點上使用get/put往Ignite快取裡讀寫資料,而把資料同步,併發控制等複雜問題留給Ignite來解決。除了get/put操作外,Ignite還提供了其他的原子操作以及非同步操作,比如getAndPutIfAbsent, getAndPutAsync, putIfAbsent, putIfAbsentAsync, getAndReplace, getAndReplaceAsync等,完整的API列表可以看這裡。
Ignite也支援在JCache標準中定義的entry processor。我沒仔細讀過JCache中對entry processor的定義,但根據Ignite的文件和使用經驗,相比於基本的快取get/put操作,entry processor有下面幾個特性/優點:
- 相比於get/put等基本操作,在entry processor中我們可以實現更為複雜的cache更新邏輯,比如我們可以讀出快取中的某個值,然後做一些自定義計算後,再更新快取中的值。
- 和get/put/putIfAbsent等操作一樣,在entry processor中所有的操作是原子性的, 即保證了entry processor中定義的操作要麼都成功,要麼都失敗。如果不用entry processor,為了達到相同目的,我們需要對需要要更新的快取資料加鎖,更新快取資料,最後釋放鎖。而有了entry proce,我們可以更專注於快取更新的邏輯,而不用考慮如何加解鎖。
- Entry processor允許在資料節點上直接進行操作。分散式快取中,如果更新的快取資料需要根據已經在快取中的資料計算得到,往往需要在多個節點之間傳送的快取資料。而entry processor是把操作序列化後傳送到快取資料所在的節點,比起序列化快取資料,要更高效。
Entry Processor程式碼示例
下面我們改造一下之前的例子,看看在Ignite中如何實現並呼叫一個entry processor。在這個例子中,cache中key的值依舊是城市的名字,但是value的值不再是簡單的城市所在省份的名字,而是一個City類的例項。下面是City類的定義:
public class City {
private String cityName;
private String provinceName;
private long population;
public City(String cityName, String provinceName, long population) {
this.cityName = cityName;
this.provinceName = provinceName;
this.population = population;
}
...
}
在City類中,我們放了一個population的成員變數,用來表示該城市的人口數量。在主程式中,我們建立多個執行緒,通過entry processor不斷修改不同城市的人口數量。每個entry processor做的事情也很簡單: 讀取當前人口數量加1,再把新值更新到cache中。下面是主程式的程式碼
public class IgniteEntryProcessorExample {
public static void main(String[] args) {
// start an ignite cluster
Ignite ignite = startCluster(args);
CacheConfiguration<String, City> cacheCfg = new CacheConfiguration<>();
cacheCfg.setName("CITY");
cacheCfg.setCacheMode(CacheMode.PARTITIONED);
cacheCfg.setBackups(1);
IgniteCache<String, City> cityProvinceCache = ignite.getOrCreateCache(cacheCfg);
// let's create a city and put it in the cache
City markham = new City("Markham", "Ontario", 0);
cityProvinceCache.put(markham.getCityName(), markham);
System.out.println("Insert " + markham.toString());
// submit two tasks to increase population
ExecutorService service = Executors.newFixedThreadPool(2);
IncreaseCityPopulationTask task1 = new IncreaseCityPopulationTask(cityProvinceCache, markham.getCityName(), 10000);
IncreaseCityPopulationTask task2 = new IncreaseCityPopulationTask(cityProvinceCache, markham.getCityName(), 20000);
Future<?> result1 = service.submit(task1);
Future<?> result2 = service.submit(task2);
System.out.println("Submit two tasks to increase the population");
service.shutdown();
try {
service.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
// get the population and check whether it is 30000
City city = cityProvinceCache.get(markham.getCityName());
if (city.getPopulation() != 30000) {
System.out.println("Oops, the population is " + city.getPopulation() + " instead of 30000");
} else {
System.out.println("Yeah, the population is " + city.getPopulation());
}
}
public static class IncreaseCityPopulationTask implements Runnable {
private IgniteCache<String, City> cityProvinceCache;
private String cityName;
private long population;
public IncreaseCityPopulationTask(IgniteCache<String, City> cityProvinceCache,
String cityName, long population) {
this.cityProvinceCache = cityProvinceCache;
this.cityName = cityName;
this.population = population;
}
@Override
public void run() {
long p = 0;
while(p++ < population) {
cityProvinceCache.invoke(cityName, new EntryProcessor<String, City, Object>() {
@Override
public Object process(MutableEntry<String, City> mutableEntry, Object... objects)
throws EntryProcessorException {
City city = mutableEntry.getValue();
if (city != null) {
city.setPopulation(city.getPopulation() + 1);
mutableEntry.setValue(city);
}
return null;
}
});
}
}
}
private static Ignite startCluster(String[] args) {
...
}
}
- 4~10行,和之前的例子一樣,我們啟動一個Ignite節點,並且建立一個名為“CITY”的cache,cache的key是城市的名字(String),cache的value是一個City的物件例項。
- 13~15行,我們建立了一個名字為“Markham”的City例項,它的初始population值是0。
- 18~30行,我們建立了2個執行緒,每個執行緒啟動後都會呼叫IncreaseCityPopulationTask的Run()函式,不同的是線上程建立時我們指定了不同的population增加次數,一個增加10000次,一個增加20000次。
- 在33~38行,我們從cache中取回名為"Markham"的例項,並檢查它最終的人口數量是不是30000。如果兩個執行緒之間的操作(讀cache,增加人口,寫cache)是原子操作的話,那麼最終結果應該是30000。
- 57~68是Entry Processor的具體用法,通過cityProvinceCache.invoke()函式就可以呼叫entry processor,invoke()函式的第一引數是entry processor要作用的資料的key。第二個引數是entry processor的一個例項,該例項必須要實現介面類EntryProcessor的process()函式。在第二個引數之後,還可以傳入多個引數,呼叫時這些引數會傳給process()函式。
- 在process()函式的中,第一個引數mutableEntry包含了process()函式作用的資料的key和value,可以通過MutableEntry.getKey()和MutableEntry.getValue()得到(如果該key的value不存在cache中,getValue()會返回null)。第二個之後的objects引數,是呼叫invoke()函式時除了key和EntryProcessor之外,傳入的引數。
- 在entry processor中可以實現一些複雜的邏輯,然後呼叫MutableEntry.setValue()對value值進行修改。如果需要刪除value,呼叫MutableEntry.remove()。
- EntryProcessor()被呼叫時,cache中對應的key值會被加鎖,所以對同一個鍵值的不同entry processor之間是互斥的,保證了一個entry processor中的所有操作是原子操作。
- 另外,有一點需要注意的是,在entry processor中的操作需要時無狀態的,因為同一個entry processor有可能會在primary和backup節點上執行多次,所以要保證entry processor中的操作只和cache中的當前值相關,如果還和當前節點的一些引數和狀態相關,會導致在不同節點上執行entry processor後寫入cache的值不一致。詳情見invoke()函式的文件。
總結
這篇文章我們介紹了Ignite Cache基本的put/get()操作外的其他操作,比如非同步的操作和entry processor**這篇文章裡用到的例子的完整程式碼和maven工程可以在這裡找到。
下一篇文章,我們會繼續看看如何使用Ignite的SQL API對cache進行查詢和修改。