I have an extension method for the Microsoft.ApplicationServer.Caching.DataCache object found in the Windows Server AppFabric SDK that looks like this:
using System;
using System.Collections.Generic;
using Microsoft.ApplicationServer.Caching;
namespace Caching
{
public static class CacheExtensions
{
private static Dictionary<string, object> locks = new Dictionary<string, object>();
public static T Fetch<T>(this DataCache @this, string key, Func<T> func)
{
return @this.Fetch(key, func, TimeSpan.FromSeconds(30));
}
public static T Fetch<T>(this DataCache @this, string key, Func<T> func, TimeSpan timeout)
{
var result = @this.Get(key);
if (result == null)
{
lock (GetLock(key))
{
result = @this.Get(key);
if (result == null)
{
result = func();
if (result != null)
{
@this.Put(key, result, timeout);
}
}
}
}
return (T)result;
}
private static object GetLock(string key)
{
object @lock = null;
if (!locks.TryGetValue(key, out @lock))
{
lock (locks)
{
if (!locks.TryGetValue(key, out @lock))
{
@lock = new object();
locks.Add(key, @lock);
}
}
}
return @lock;
}
}
}
The intent is to let the developer write code that says, "fetch me some data by trying the cache first. if it's not available in cache execute the specified function, put the results in cache for the next caller, then return the results". Like this:
var data = dataCache.Fetch("key", () => SomeLongRunningOperation());
The locking limits executing the potentially long running function call to a single thread but only within a single process on the same machine. How would you expand on this pattern to make the locking distributed to prevent multiple processes/machines from executing the function at once?
AppFabric has it's own distributed locking mechanism which you can access through the GetAndLock/PutAndUnlock
family of methods. If your item is locked, a normal Get
call will still succeed and return the last value, but further GetAndLock
calls will throw an Exception. In the case where your client is requesting a cached object for the first time, you can still lock the key even though it doesn't really exist yet (it's kind of more like a reservation than a solid lock).
public static T Fetch<T>(this DataCache @this, string key, Func<T> func, TimeSpan timeout)
{
var result = @this.Get(key);
if (result == null)
(
DataCacheLockHandle handle;
// We need a timespan to allow func time to run
TimeSpan funcTimespan = New TimeSpan(0,1,0);
try
{
// Lock the key
// If something goes wrong here it will unlock at the end of funcTimespan
var result = @this.GetAndLock(key, funcTimespan, handle);
if (result == null)
{
// Still no value so go and run func
result = func();
@this.PutAndUnlock(key, result, handle, timeout);
}
else
{
// There's a value now so we'll unlock the key and reset it's timeout
@this.Unlock(key, handle, timeout);
}
}
catch (DataCacheException ex)
{
if (ex.ErrorCode == DataCacheErrorCode.ObjectLocked)
{
// Another process has locked the key so func must be running right now
// We'll return null to the client
result = null;
}
}
if (result == null)
{
return null;
}
else
{
return (T)result;
}
)
}
I was looking for a good implementation of this and came up with my own:
Distributed Lock with AppFabric Caching
Essentially it's an AcquireLock() extension method to the DataCache class which you can use like this:
DataCache cache = factory.GetCache("MyCache");
using (cache.AcquireLock("MyLock"))
{
// Lock acquired, perform synchronized work here
}
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