解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
大石头 authored at 2018-05-15 21:21:05
4.10 KiB
X
namespace NewLife.Caching;

/// <summary>分布式锁</summary>
public class CacheLock : DisposeBase
{
    private ICache Client { get; set; }

    private Boolean _hasLock;
    /// <summary>是否持有锁</summary>
    public Boolean HasLock => _hasLock;

    private String _token = "";
    /// <summary>锁令牌。用于释放时校验归属,避免误删超时后被其它进程接管的锁</summary>
    public String Token => _token;

    /// <summary>键</summary>
    public String Key { get; set; }

    /// <summary>实例化</summary>
    /// <param name="client">缓存客户端</param>
    /// <param name="key">锁键</param>
    public CacheLock(ICache client, String key)
    {
        if (client == null) throw new ArgumentNullException(nameof(client));
        if (key.IsNullOrEmpty()) throw new ArgumentNullException(nameof(key));

        Client = client;
        Key = key;
    }

    /// <summary>申请锁</summary>
    /// <param name="msTimeout">锁等待时间,申请加锁时如果遇到冲突则等待的最大时间,单位毫秒</param>
    /// <param name="msExpire">锁过期时间,超过该时间如果没有主动释放则自动释放锁,必须整数秒,单位毫秒</param>
    /// <returns>是否成功获取锁</returns>
    public Boolean Acquire(Int32 msTimeout, Int32 msExpire)
    {
        var ch = Client;
        var now = Runtime.TickCount64;

        // 生成唯一令牌,释放时校验归属,避免锁超时被接管后误删新持有者的锁
        var token = Guid.NewGuid().ToString("N");
        // 锁值格式:令牌|绝对过期毫秒。缓存TTL向上取整,避免小于1秒的锁因取整为0而立即失效
        var value = $"{token}|{now + msExpire}";
        var expire = msExpire > 0 ? (msExpire + 999) / 1000 : 0;

        // 循环等待。至少尝试一次,msTimeout=0 时也允许直接抢锁(否则空闲锁也永远拿不到)
        var end = now + msTimeout;
        while (true)
        {
            // 申请加锁。没有冲突时可以直接返回
            var rs = ch.Add(Key, value, expire);
            if (rs)
            {
                _token = token;
                return _hasLock = true;
            }

            // 死锁超期检测
            var dt = ParseExpire(ch.Get<String>(Key));
            if (dt <= now)
            {
                // 开抢死锁。所有竞争者都会修改该锁的时间戳,但是只有一个能拿到旧的超时的值
                var old = ParseExpire(ch.Replace(Key, value));
                // 如果拿到超时值,说明抢到了锁。其它线程会抢到一个为超时的值
                if (old <= dt)
                {
                    ch.SetExpire(Key, TimeSpan.FromMilliseconds(msExpire));
                    _token = token;
                    return _hasLock = true;
                }
            }

            // 超时退出
            now = Runtime.TickCount64;
            if (now >= end) break;

            // 没抢到,继续
            Thread.Sleep(200);
        }

        return false;
    }

    /// <summary>解析锁值中的过期时间戳。兼容旧格式(纯数字时间戳)</summary>
    /// <param name="value">锁值</param>
    /// <returns>过期时间戳(毫秒)</returns>
    private static Int64 ParseExpire(String? value)
    {
        if (value.IsNullOrEmpty()) return 0;

        var p = value.IndexOf('|');
        var str = p > 0 ? value[(p + 1)..] : value;

        return str.ToLong();
    }

    /// <summary>销毁</summary>
    /// <param name="disposing"></param>
    protected override void Dispose(Boolean disposing)
    {
        base.Dispose(disposing);

        // 如果客户端已释放,则不删除
        if (Client is DisposeBase db && db.Disposed)
        {
        }
        else
        {
            if (_hasLock && !_token.IsNullOrEmpty())
            {
                // 仅当锁仍由本实例持有时才删除,避免误删超时后被其它进程接管的锁
                var str = Client.Get<String>(Key);
                if (str != null && str.StartsWith(_token + "|")) Client.Remove(Key);
            }
        }
    }
}