public class PutAll extends AbstractInvocable implements PortableObject {
private String cacheName;
private Map<Object,Object> values;
public PutAll() {
}
public PutAll(String cacheName, Map<Object, Object> values) {
this.cacheName = cacheName;
this.values = values;
}
@Override
public void run() {
}
@Override
public void readExternal(PofReader pofReader) throws IOException {
cacheName = pofReader.readString(1);
values = pofReader.readMap(2, new HashMap());
}
@Override
public void writeExternal(PofWriter pofWriter) throws IOException {
pofWriter.writeString(1, cacheName);
pofWriter.writeMap(2, values);
}
}
接下来我们要去实现run方法,让我们回顾下这里我们需要做的事情:
1:根据key所在的节点来对这些数据进行分组
2:使用另外一个Invocable将这些分组好组的数据发送给各个节点进行处理
因为我们知道每个Invocable将会在各个数据存在的节点执行——在这种情况下,调用服务扩展代理的代理服务。这意味着我们可以对缓存的引用,我们将更新和使用该缓存的CacheService告诉我们哪些集群成员拥有哪些键。
run方法实现如下的第一步
@Override
public void run() {
NamedCache cache = CacheFactory.getCache(cacheName);
PartitionedService cacheService = (PartitionedService) cache.getCacheService();
Map<Member,Map<Object,Object>> valuesByMember = new HashMap<Member,Map<Object,Object>>();
for (Map.Entry entry : values.entrySet()) {
Object key = entry.getKey();
Member member = cacheService.getKeyOwner(key);
if (!valuesByMember.containsKey(member)) {
valuesByMember.put(member, new HashMap<Object,Object>());
}
valuesByMember.get(member).put(key, entry.getValue());
}
}
我们遍历值和使用PartitionedService getKeyOwnwer()进行分组.我们现在拥有一个发送到每个集群成员映射值,所以我们现在需要发送他们。这就是我们使用第二个Invocable,InvocationService将让他在所有集群成员上运行。我们将调用第二个Invocable-PutAllForMember,他的构造参数包括需要更新的缓存名称以及等待更新的数据。
下面PutAllForMember类框架代码。这是一个可调用和实现PortableObject的类:
public class PutAllForMember extends AbstractInvocable implements PortableObject {
private String cacheName;
private Map<Object,Object> values;
public PutAllForMember() {
}
public PutAllForMember(String cacheName, Map<Object, Object> values) {
this.cacheName = cacheName;
this.values = values;
}
@Override
public void run() {
}
@Override
public void readExternal(PofReader pofReader) throws IOException {
cacheName = pofReader.readString(1);
values = pofReader.readMap(2, new HashMap());
}
@Override
public void writeExternal(PofWriter pofWriter) throws IOException {
pofWriter.writeString(1, cacheName);
pofWriter.writeMap(2, values);
}
}
现在我们必须实现局部更新的实际代码。正如我们已经说过的,基本上运行方法都需要迭代更新缓存的值与捕获每一个异常。然后将他们返回。
@Override
public void run() {
Map<Object,Throwable> errors = new HashMap<Object,Throwable>();
NamedCache cache = CacheFactory.getCache(cacheName);
for (Map.Entry entry : values.entrySet()) {
try {
cache.put(entry.getKey(), entry.getValue());
} catch (Throwable t) {
errors.put(entry.getKey(), t);
}
}
setResult(errors);
}
现在我们有Invocable本地更新使我们可以回去完成PutAll.run()方法。
@Override
public void run() {
NamedCache cache = CacheFactory.getCache(cacheName);
DistributedCacheService cacheService = (DistributedCacheService) cache.getCacheService();
Map<Member,Map<Object,Object>> valuesByMember = new HashMap<Member,Map<Object,Object>>();
for (Map.Entry entry : values.entrySet()) {
Object key = entry.getKey();
Member member = cacheService.getKeyOwner(key);
if (!valuesByMember.containsKey(member)) {
valuesByMember.put(member, new HashMap<Object,Object>());
}
valuesByMember.get(member).put(key, entry.getValue());
}
Map<Object,Throwable> results = new HashMap<Object,Throwable>();
InvocationService service = (InvocationService) CacheFactory.getService("InvocationService");
for (Map.Entry<Member,Map<Object,Object>> entry : valuesByMember.entrySet()) {
PutAllForMember putAllForMember = new PutAllForMember(cacheName, entry.getValue());
Map<Member,Map<Object,Throwable>> results = service.query(putAllForMember, Collections.singleton(entry.getKey()));
results.putAll(results.get(entry.getValue());
}
setResult(results);
}
上面的代码对我们之前已经分好的组进行分批的发送给远程的节点去执行,并且获取他们返回的结果.
到此,基本代码就已经完成了,我们可以想如下进行调用:
Map<Object,Object> values = new HashMap<Object,Object>();
// --- Populate the values map with the key/value pairs to put ---
PutAll putAll = new PutAll("dist-test", values);
InvocationService service = (InvocationService) CacheFactory.getService("remote-invocation-service");
Map<Member,Map<Object,Throwable>> results = service.query(putAll, null);
Map<Object,Throwable> errors = results.values().iterator().next();
// --- check the errors map for any failures ---
经过一些测试,上面的程序确实可以正常的执行和工作,不过测试结果不让人满意,执行速度太慢了.
优化
虽然上面的代码得到正常的运作,执行效率虽然不是最佳的,但是确实解决了我们最初设计他的问题,如果你自己构建和编译执行了上面的代码,你会发现他的执行会有点慢,我做了一个基本的测试,就是先通过原来的PutAll操作存储1000个数据,大概在170ms左右,但是通过我们的新的PutAll存储相同的数据大概在700ms左右,这个测试环境是在我的MacBook Pro电脑上,启动的缓存节点个数是3个,虽然这个不是严谨的测试,不过我们只关注的是相对的性能,显然这个性能不足以让人满意.
异步调用
首先我们可以对PutAll的run方法进行改造,目前的代码是我们在每一次迭代中去调用PutAllForMember去执行,其中每次都需要等待结果返回才能进行下一次的迭代.Coherence允许我们异步提交,通过调用InvocationService的execute()方法,而且会被回调在每次调用结束后,这将是更有效的,因为我们可以提交所有PutAllForMember Invocables
它允许我们使用异步调用方法,所以我们需要编写另一个类来接收这些回调,这是一个InvocationObserver的实现。这个类有许多事情要做.
1:监听每个调用的结果,这些调用有可能会成功完成,也有可能会在执行的时候失败或者调用节点的离开
2:从每个调用收集并返回结果
3:有一个阻塞方法,等到所有的invocables完成全部结果集的返回。
下面是InvocationObserver基本代码,可以称之为PutAllObserver。
public class PutAllObserver implements InvocationObserver {
private Map<Member,Map<Object,Object>> membersAndValues;
private Map<Object,Throwable> results;
private volatile int actualCount = 0;
public PutAllObserver(Map<Member,Map<Object,Object>> membersAndValues) {
this.membersAndValues = membersAndValues;
this.results = new HashMap<Object,Throwable>();
}
public synchronized Map<Object,Throwable> getResults() {
}
@Override
public synchronized void memberCompleted(Member member, Object memberResult) {
}
@Override
public synchronized void memberFailed(Member member, Throwable throwable) {
}
@Override
public synchronized void memberLeft(Member member) {
}
@Override
public synchronized void invocationCompleted() {
}
}
在构造函数中我们传入了所有节点对应数据的map,这样使得PutAllObserver知道我们有多少个调用和期待多少个结果返回,在构造函数中我们也创建了一个空的map去接受所有的返回结果我们有一个计数字段来跟踪有多少调用完成。我们所有的同步方法可以被多个线程调用结果进来所以我们想成为线程安全的。我们还添加了getResults()方法将由PutAll类和调用会阻塞,直到所有invocables都完成了。在getResult方法我们会判断进行阻塞知道所有调用都返回结果.
public synchronized Map<Object,Throwable> getResults() {
while(actualCount < membersAndValues.size()) {
try {
this.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return results;
}
@Override
public synchronized void memberCompleted(Member member, Object memberResult) {
results.putAll((Map<Object,Throwable>) memberResult);
actualCount++;
this.notifyAll();
}
@Override
public synchronized void memberLeft(Member member) {
Throwable throwable = new PutAllMemberLeftException(member);
for (Object key : membersAndValues.get(member).keySet()) {
results.put(key, throwable);
}
actualCount++;
this.notifyAll();
}
我们现在有一个InvocationObserver我们时,我们可以使用异步可调用调用,我们可以将它添加到PutAll运行方法。
@Override
public void run() {
NamedCache cache = CacheFactory().getCache(cacheName);
DistributedCacheService cacheService = (DistributedCacheService) cache.getCacheService();
Map<Member,Map<Object,Object>> valuesByMember = new HashMap<Member,Map<Object,Object>>();
for (Map.Entry entry : values.entrySet()) {
Object key = entry.getKey();
Member member = cacheService.getKeyOwner(key);
if (!valuesByMember.containsKey(member)) {
valuesByMember.put(member, new HashMap<Object,Object>());
}
valuesByMember.get(member).put(key, entry.getValue());
}
InvocationService service = (InvocationService) CacheFactory.getService("InvocationService");
PutAllObserver observer = new PutAllObserver(valuesByMember);
for (Map.Entry<Member,Map<Object,Object>> entry : valuesByMember.entrySet()) {
PutAllForMember putAllForMember = new PutAllForMember(cacheName, entry.getValue());
service.execute(putAllForMember, Collections.singleton(entry.getKey()), observer);
}
Map<Object, Throwable> results = observer.getResults();
setResult(results);
}
@Override
public void writeExternal(PofWriter pofWriter) throws IOException {
pofWriter.writeString(1, cacheName);
pofWriter.writeInt(2, values.size());
Serializer serializer = pofWriter.getPofContext();
int id = 3;
for (Map.Entry entry : values.entrySet()) {
pofWriter.writeObject(id++, entry.getKey());
pofWriter.writeBinary(id++, ExternalizableHelper.toBinary(entry.getValue(), serializer));
}
}
@SuppressWarnings({"unchecked"})
@Override
public void readExternal(PofReader pofReader) throws IOException {
cacheName = pofReader.readString(1);
values = new HashMap<Object,Object>();
int count = pofReader.readInt(2);
int id = 3;
for (int i=0; i<count; i++) {
Object key = pofReader.readObject(id++);
Binary value = pofReader.readBinary(id++);
values.put(key, value);
}
}
ublic class BinaryValueUpdater extends AbstractProcessor implements PortableObject {
private Binary binaryValue;
public BinaryValueUpdater() {
}
public BinaryValueUpdater(Binary binaryValue) {
this.binaryValue = binaryValue;
}
@Override
public Object process(InvocableMap.Entry entry) {
((BinaryEntry)entry).updateBinaryValue(binaryValue);
return null;
}
@Override
public void readExternal(PofReader pofReader) throws IOException {
binaryValue = pofReader.readBinary(1);
}
@Override
public void writeExternal(PofWriter pofWriter) throws IOException {
pofWriter.writeBinary(1, binaryValue);
}
}
到此基本上就完成了。我们现在可以修改PutAllForMember run()方法使用的二进制值Map
@Override
public void run() {
Map<Object,Throwable> errors = new HashMap<Object,Throwable>();
NamedCache cache = CacheFactory.getCache(cacheName);
for (Map.Entry entry : values.entrySet()) {
try {
cache.invoke(entry.getKey(), new BinaryValueUpdater((Binary) entry.getValue()));
} catch (Throwable t) {
errors.put(entry.getKey(), t);
}
}
setResult(errors);
}
public class PutAll extends AbstractInvocable implements PortableObject {
public static final String INVOCATION_SERVICE_NAME = "InvocationService";
private String cacheName;
private Map<Object,Object> values;
private boolean valuesAreBinary = false;
public PutAll() {
}
public PutAll(String cacheName, Map<Object, Object> values) {
this.cacheName = cacheName;
this.values = values;
}
@SuppressWarnings({"unchecked"})
@Override
public void run() {
NamedCache cache = CacheFactory.getCache(cacheName);
DistributedCacheService cacheService = (DistributedCacheService) cache.getCacheService();
ensureValuesAreBinary(cacheService.getSerializer());
Map<Member,Map<Object,Object>> valuesByMember = new HashMap<Member,Map<Object,Object>>();
for (Map.Entry entry : values.entrySet()) {
Object key = entry.getKey();
Member member = cacheService.getKeyOwner(key);
if (!valuesByMember.containsKey(member)) {
valuesByMember.put(member, new HashMap<Object,Object>());
}
valuesByMember.get(member).put(key, entry.getValue());
}
InvocationService service = (InvocationService) CacheFactory.getService(INVOCATION_SERVICE_NAME);
PutAllObserver observer = new PutAllObserver(valuesByMember);
for (Map.Entry<Member,Map<Object,Object>> entry : valuesByMember.entrySet()) {
PutAllForMember putAllForMember = new PutAllForMember(cacheName, entry.getValue());
service.execute(putAllForMember, Collections.singleton(entry.getKey()), observer);
}
Map<Object, Throwable> results = observer.getResults();
setResult(results);
}
@SuppressWarnings({"unchecked"})
@Override
public void readExternal(PofReader pofReader) throws IOException {
cacheName = pofReader.readString(1);
values = new HashMap<Object,Object>();
int count = pofReader.readInt(2);
int id = 3;
for (int i=0; i<count; i++) {
Object key = pofReader.readObject(id++);
Binary value = pofReader.readBinary(id++);
values.put(key, value);
}
valuesAreBinary = true;
}
@Override
public void writeExternal(PofWriter pofWriter) throws IOException {
pofWriter.writeString(1, cacheName);
pofWriter.writeInt(2, values.size());
Serializer serializer = pofWriter.getPofContext();
int id = 3;
for (Map.Entry entry : values.entrySet()) {
pofWriter.writeObject(id++, entry.getKey());
pofWriter.writeBinary(id++, ExternalizableHelper.toBinary(entry.getValue(), serializer));
}
}
private synchronized void ensureValuesAreBinary(Serializer serializer) {
if (!valuesAreBinary) {
Map<Object, Object> converted;
if (this.values.isEmpty()) {
converted = this.values;
} else {
converted = new HashMap<Object,Object>();
for (Map.Entry entry : values.entrySet()) {
converted.put(entry.getKey(), ExternalizableHelper.toBinary(entry.getValue(), serializer));
}
}
values = converted;
valuesAreBinary = true;
}
}
}
Map<Object,Object> values = new HashMap<Object,Object>();
// --- Populate the values map with the key/value pairs to put ---
PutAll putAll = new PutAll("dist-test", values);
putAll.run();
Map<Object,Throwable> errors = putAll.getResult();
// --- check the errors map for any failures
Ordering
One use of putAll in the system I am currently working on is part of the initial data population when the cluster starts up. Because of the way data is loaded we do multiple updates for the same key as we build up the data. We needed the putAll to preserve the ordering of the key/value pairs so we could in effect do a putAll where we sent multiple values for the same key in a single putAll call but ensuring we maintained the order. This is easy to do by replacing the Maps holding the keys and values in the PutAll and PutAllFormember invocables with a List of some sort of tuple class to hold the key and corresponding value. Instead of iterating over a Map we iterate over a List so maintaining order.
Setting Expiry
The NamedCache class has a version of the put method that takes a third parameter to specify the expiry time for the key and value being put but there is no putAll equivalent of this method. We could enhance out version of PutAll to set a bulk expiry value for all the entries; we just need to pass a long value to the PutAll and PutAllForMember invocables constructors, not forgetting to add them to the readExternal and writeExternal methods.
Conclusions
So there you have it, an alternative version of Coherence putAll that allows you to recover from exceptions thrown from updates. The approach has proved to be work well and remarkably fast in a stable cluster - that is one in which partitions are not moving. Given that partitions only move when nodes leave or join the cluster that is not too bad. If a node leaves the cluster then there might be problems if that node was executing one of the PutAllForMember invocables, but you should be able to catch the error and recover. But then you really don't want nodes leaving your cluster very often, if ever, do you.