Memcake is a Java client for Memcached. It speaks only the binary protocol, and provides relatively low level access to memcached primitives. Basically, each command memcached exposes via the binary protocol is exposed 1:1 in memcake.
All operations occur asynchronously on the thread pool for the AsynchronousChannelGroup
that the connection to the server is using. Results are made available via CompletableFuture
. Any callbacks attached to the future should not do much work on the thread they are called on, but should pass off blocking operations of large calculations to alternate threads.
Memcake requires Java 1.8, and has no other runtime dependencies.
As of the currently released version, Memcake only supports communicating with a single memcached server instance per client. The near-term plan is to support libmemcached compatible sharding in Memcake, but it has not been finished yet.
Memcake uses evented IO via NIO. This works well on operating systems with good evented IO, such as Linux, FreeBSD, and Windows. OS X, on the other hand, has a seemingly crippled kqueue implementation and Java does not play nicely with it, so you can see very high levels of kernel CPU consumption using this library even moderately under high load (such as running the tests).
You can find the latest release in Maven Central.
<dependency>
<groupId>org.skife.memcake</groupId>
<artifactId>memcake</artifactId>
<version>${memcake.version}</version>
</dependency>
The simplest way to create a client is just to pass the server to connect to (currently, it only supports one), the max number of in-flight requests to allow (to avoid unbounded queues), and a default timeout:
Memcake mc = Memcake.create(memcached.getAddress(), // memcached address
1000, // max concurrent requests
Duration.ofSeconds(1)); // default operation timeout
If finer grained control is needed over the network configuration, thread pool sizes, etc, you can alternately construct a client providing all of these things:
ScheduledExecutorService timer = Executors.newScheduledThreadPool(1);
ExecutorService pool = Executors.newFixedThreadPool(4);
AsynchronousChannelGroup group = AsynchronousChannelGroup.withThreadPool(pool);
Memcake mc = Memcake.create(Collections.singleton(memcached.getAddress()),
Duration.ofSeconds(1),
(addr) -> {
try {
AsynchronousSocketChannel chan = AsynchronousSocketChannel.open(group);
return Connection.open(addr, 1000, chan, timer);
} catch (IOException e) {
throw new IllegalStateException(e);
}
});
In this, more general, form of client creation you pass in the set of servers it should connect to (which must have a size of one right now), the default duration, and a function which creates an Connection
object from the address. A Connection
represents the low level connection to the memcached server, and needs to receive the address, maximum in flight request count, an AsynchronousSocketChannel
to perform the IO on, and a scheduled executor which is used to trigger timeouts.
The socket channel MUST NOT be connected when it is passed in, but can be otherwise configured as desired.
Operations on Memcake
match 1:1 with the memcached binary protocol, so any questions about behavior can be looked up there. The required argumnents for operations are parameters on the methods on Memcake
, which return a builder that can receive optional arguments, and be used to execute the operation:
Memcake mc = Memcake.create(memcached.getAddress(),
1000,
Duration.ofSeconds(1));
CompletableFuture<Version> fv = mc.set("hello", "world")
.expires(300)
.flags(0xCAFE)
.execute();
Version v1 = fv.get();
CompletableFuture<Optional<Value>> f = mc.get("hello")
.execute();
Optional<Value> ov = f.get();
assertThat(ov).isPresent();
ov.ifPresent((v) -> {
assertThat(v.getVersion()).isEqualTo(v1);
assertThat(v.getFlags()).isEqualTo(0xCAFE);
assertThat(v.getValue()).isEqualTo("world".getBytes(StandardCharsets.UTF_8));
});
mc.close();
This example issues a set
and then get
for a value. Keys and values can generally be String
or byte[]
. If a String
is used, as is the case here, it will be converted to a UTF-8 encoded byte[]
.
Set provides a Value
which can be optionally used as a compare and swap (cas) value on many operations. Get provides a Value
which makes available all the information memcached returns, generally the version (cas), the actual bytes value, and flags. The getk
variant operations also make the key available on the value.
Many operations on memcached support a "quiet" variant, where the server only responds if the response is "interesting." You can see the binary protocol spec for details on what qualifies as interesting for what operations. In Memcake, if the server chooses to NOT send a response, the CompletableFuture
from the client will not complete until a non-quiet operation sent after the quiet operation completes, or the operation timeout is reached (in which case the future will complete exceptionally with a timeout). If you are sending a series of quiet operations, you should generally send the last operation non-quietly, or send a noop()
, in order to have those futures complete normally and know that they succeeded (or failed, in the case of get(...)
).
Memcached's binary protocol (and Memcake at this time) has no first class concept of multiget! Multiget is implemented via a series of getq(...)
operations followed by a final get(...)
operation. You can think of this as a batch operation which optimizes wire transfers. The nice part about this is that you can mix in any combination of operations you like -- increments, sets, gets, etc into a generalized "multi-op" instead of just a multiget. It is important to send that last operation non quietly, or send a noop, though, so you can know when the whole batch completes, and force the Optional.empty()
result for the getqs.
Licensed under Apache License 2.0. No runtime dependencies outside the standard library.