A fully async, pipelining, high-performance Memcached library for .NET
(A full rewrite of Enyim.Memcached.)
- Only the binary protocol is supporrted (unless using a very old Memcached server, this should not be a blocker)
- Uses
Memory<T>
/Span<T>
, so currently it only runs on .NET Core 3.0 (netstandard2.1)
dotnet add package Enyim.Memcached2 --version 0.6.0-preview
<PackageReference Include="Enyim.Memcached2" Version="0.6.0-preview" />
static async Task Main(string[] args)
{
// connect to a list of memcached servers
var cluster = new MemcachedCluster("localhost,localhost:11212,localhost:11213");
// this is mandatory
cluster.Start();
var client = cluster.GetClient();
await client.SetAsync("hello", new { hello= "world" });
// more work
// stop the cluster
cluster.Dispose();
}
var cluster = new MemcachedCluster("localhost,localhost:11212,localhost:11213");
cluster.Start();
Memcached2 has no SocketPool
, like the previous version: it maintains a persistent connection to each server. It also handles all communications (sending operations, parsing replies) with all servers via its own IO Thread.
Because of this, it should be used as a singleton, or at least a "long-lived instance":
- initialize at the start of the application ("bootstrapping" phase)
Dispose
it when quitting (or when it's not needed anymore)
Each cluster can only be used to submit work to the servers specified at construction: for each group of servers a new instance must be created (and kept alive until the application exits).
var client = cluster.GetClient();
The MemcachedClient is used for performing the operations.
The GetClient
(without parameters) is cheap to call, there is no need to save the returned instance. (It is cached by the cluster.)
When using an IoC container it is recommended to register it as a singleton.
services.AddSingleton<IMemcachedClient>(cluster.GetClient())
Or, if the Cluster is also registered:
services.AddSingleton<IMemcachedClient>(ctx => ctx.GetRequiredService<IMemcachedCluster>().GetClient())
Note: if the Cluster is not started before the GetClient
is called an exception will be thrown.
The constructor accepts a IMemcachedClusterOptions
instance:
public interface IMemcachedClusterOptions
{
MemoryPool<byte> Allocator { get; }
INodeLocator Locator { get; }
IReconnectPolicyFactory ReconnectPolicyFactory { get; }
IFailurePolicyFactory FailurePolicyFactory { get; }
ISocketFactory SocketFactory { get; }
}
There is also a default implementation:
var cluster = new MemcachedCluster(hosts, new MemcachedClusterOptions
{
// ...
});
Used for allocating buffers (byte arrays) to reduce the GC pressure.
Defaults to MemoryPool<byte>.Shared
.
Implement the INodeLocator
interface to define how the items (keys) are mapped to the Memcached servers in the cluster.
The default DefaultNodeLocator
uses Ketama consistent hashing with a Murmurhash3 hash function.
Implement the IReconnectPolicy
and its Factory
to control how the Cluster reconnects to failed nodes.
The default PeriodicReconnectPolicyFactory
reconnects every 10 seconds.
var cluster = new MemcachedCluster(hosts, new MemcachedClusterOptions
{
ReconnectPolicyFactory = new PeriodicReconnectPolicyFactory { Interval = TimeSpan.FromSeconds(10) }
});
Implement the IFailurePolicy
and its Factory
to control how the Cluster fails nodes when a socket error occurs.
The default ImmediateFailurePolicyFactory
fails the node at the first error.
Some firewalls abort long-open TCP connections, no matter if the connection is still valid. In this case you can implement a custom policy which
- tries to immediately reconnect when a socket error occurs (possibly the firewall caused it)
- fails the node if a second error occurs in a given timeframe (the node is probably down)
If the network connection to the server(s) is not stable, the ThrottlingFailurePolicyFactory
can help, as it only fails a node when a given amount of failures occur in a configurable time window. (E.g. 3 fails in 30 secs.)
var cluster = new MemcachedCluster(hosts, new MemcachedClusterOptions
{
FailurePolicyFactory = new ThrottlingFailurePolicyFactory { ResetAfter = TimeSpan.FromSeconds(30), Threshold = 3 }
});
However, it is a better long-term solution just making the connection more stable.
The main use-case is to customize the behavior of the Sockets used by the Cluster.
var cluster = new MemcachedCluster(hosts, new MemcachedClusterOptions
{
SocketFactory = new AsyncSocketFactory (new SocketOptions
{
ConnectionTimeout = TimeSpan.FromSeconds(20)
})
});
See the SocketOptions and AsyncSocket for further options and their default values.
Some aspects of the Client's behavior can be customized using the IMemcachedClientOptions
.
public interface IMemcachedClientOptions
{
IKeyFormatter KeyFormatter { get; }
IItemFormatter ItemFormatter { get; }
}
Pass an instance to the MemcachedCluster's constructor to customize the default instance. (The instance returned by GetClient
.)
var cluster = new MemcachedCluster(hosts, clientOptions: new MemcachedClientOptions
{
// ...
});
Pass an instance to MemcachedCluster.GetClient()
to get a customized instance.
Note: these Client instances are not cached by the Cluster
var client = cluster.GetClient(new MemcachedClientOptions
{
});
The same can be achieved by just creating a new MemcachedClient instance:
var client = new MemcachedClient(cluster, new MemcachedClientOptions
{
});
There is no difference between the two options (GetClient also uses the constructor), pick one based on personal preference.
Implement the IKeyFormatter
interface to control how the item keys are serialized to byte[]
(so that they can be sent to Memcached).
The default Utf8KeyFormatter
converts the keys to their UTF-8 representation.
Use the NamespacingKeyFormatter
to make the Client prefix all item keys with a string, e.g. when multiple applications use the same keys but their data should be separate.
var client = cluster.GetClient(new MemcachedClientOptions
{
KeyFormatter = new NamespacingKeyFormatter("customprefix:")
});
Implement the ItemFormatter
interface to control how the items are serialized to byte[]
(so that they can be sent to Memcached).
The default BinaryItemFormatter
serializes items the following way:
- numbers are sent as their big-endian representation
bool
: 0/1DateTime
aslong
(.ToBinary()
)string
as UTF-8 bytes- anything else is serialized using
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
The client supports emitting its diagnostics messages to the Console or some of the popular logging libraries. (Internal logging is disabled if no diagnostics logger is installed.)
The logger musy be installed before creating the CLuster:
var factory = ...;
Enyim.LogManager.AssignFactory(factory);
The Console logger is built-in.
var factory = ...;
Enyim.LogManager.AssignFactory(new Enyim.Diagnostics.ConsoleLoggerFactory(LogLevel.Information));
Requires a Nuget package:
dotnet add package Enyim.Diagnostics.Serilog --version 0.6.0-preview
Usage:
Enyim.LogManager.AssignFactory(new Enyim.Diagnostics.SerilogLoggerFactory());
If no Serilog.LoggerConfiguration
is passed to the constructor the default Serilog.Log.Logger
will be used (i.e. the global log configuration.)
Requires a Nuget package:
dotnet add package Enyim.Diagnostics.NLog --version 0.6.0-preview
Usage:
Enyim.LogManager.AssignFactory(new Enyim.Diagnostics.NLogLoggerFactory());
If no NLog.LogFactory
is passed to the constructor the default NLog.LogManager.LogFactory
will be used (i.e. the global log configuration.)
Requires a Nuget package:
dotnet add package Enyim.Diagnostics.ExtensionsLogging --version 0.6.0-preview
Usage:
/*
// application specific code
var loggerFactory = new LoggerFactory(...)
// or
var loggerFactory = app.ApplicationServices.GetService<Microsoft.Extensions.Logging.LoggerFactory>();
*/
Enyim.LogManager.AssignFactory(new Enyim.Diagnostics.MicrosoftLoggerFactory(loggerFactory));
dotnet add package Enyim.Memcached2.Extensions.AspNetCore --version 0.6.0-preview
Use the AddMemcached extension method to
- configure the Cluster
- add the Client to the IoC container
- start the Cluster during application startup
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// ...
services
.AddMemcached("localhost")
.UseKeyFormatter(_ => new NamespacingKeyFormatter("customprefix:"));
}
}
Now the IMemcachedCluser
and IMemcachedClient
can be injected into pages/controllers:
@inject Enyim.Caching.Memcached.IMemcachedClient client
public class IndexModel: PageModel
{
public IndexModel(IMemcachedCluster cluster)
{
}
}
See the source code for further customization options.