Distributed Lock Service with Windows Server AppFa

2019-03-30 08:05发布

问题:

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?

回答1:

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;
        }
    )
}


回答2:

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
}