Requirement: A sequence ID (Long) generator which would work in a distributed environment (multiple JVMs). There will be multiple threads working on each JVM.
Solution: We have a centralized persistent key-value store. But we do not want to make a remote call for every incoming request, so we thought of fetching batch of sequence IDs from this centralized key-value store and keep in local JVM and then use it.
Key Value Store: This is our centralized key-value store in which we keep an object SEQUENCE_ID with a Long value. This key-value store of ours has a feature of controlling concurrent updates through a version number.
BatchRetriever: This performs following operations:
SEQUENCE_ID from key-value storeSEQUENCE_IDMultiple threads could be trying to do this, so all these 3 steps will be performed as a single atomic work. We use version-number feature of this key-value store to control this concurrent updates.
SequenceHolder: A queue based data structure which would hold the batch of sequence IDs.
SequenceObserver: An observer (implemented through Observer design pattern) which can check if the size of SequenceHolder has gone down to a threshold value, it will use BatchRetriever to retrieve next batch.
Appreciate if someone can validate this design and suggest a better one.
~ NN
These are good approaches.
A simpler solution might be to use shared memory. This has the performance of AtomicLong, while being shared across processes on the same machine.
import net.openhft.lang.io.DirectBytes;
import net.openhft.lang.io.MappedStore;
import java.io.File;
import java.io.IOException;
import java.nio.channels.FileChannel;
public class CounterExampleMain {
static volatile long id;
public static void main(String... ignored) throws IOException {
int counters = 128;
int repeats = 100000;
File file = new File(System.getProperty("java.io.tmpdir") + "/counters");
MappedStore ms = new MappedStore(file, FileChannel.MapMode.READ_WRITE, counters * 8);
DirectBytes slice = ms.bytes();
long start = System.nanoTime();
for (int j = 0; j < repeats; j++) {
for (int i = 0; i < counters; i++) {
id = slice.addAtomicLong(i * 8, 1);
}
}
long time = System.nanoTime() - start;
System.out.printf("Took %.3f second to increment %,d counters, %,d times, last id=%,d%n",
time / 1e9, counters, repeats, id);
ms.free();
}
}
Each time I run it on my laptop, I get
Took 0.252 second to increment 128 counters, 100,000 times, last id=100,000
Took 0.267 second to increment 128 counters, 100,000 times, last id=200,000
Took 0.255 second to increment 128 counters, 100,000 times, last id=300,000
As you can see this is really cheap, averaging ~25 ns per increment and persisted between runs of the program. It is also thread safe and can be shared between JVMs.
BTW in a contented example where multiple threads are updating the same counters, I would expect closer to 50 ns.
The library I used was
<dependency>
<groupId>net.openhft</groupId>
<artifactId>lang</artifactId>
<version>6.4.8</version>
</dependency>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With