头号码甲的错题本

头号码甲的错题本

前情提要

同程艺龙基础架构部推出的数据获取组件DAL.Connection,我们要做到在切换连接配置时清空数据库连接池, 这就涉及到切换连接的时候,触发变更通知。

  • .NET 如何清空连接池?
  • 面试官:实现一个带值变更通知能力的Dictionary

仔细阅读《面试官:实现一个带值变更通知能力的Dictionary》一文的童靴们有没有发现一个细节: 我使用了lock语法糖无脑加锁。


这在高并发下会有问题:大多数时候下DBA并不会变更业务方的数据库连接,这是一个多读少写的场景, 我们无脑使用lock在多数时间会人为阻塞请求。

到这个时候,我们就要想到读写锁ReaderWriterLockSlim

宝藏好物:ReaderWriterLockSlim

简而言之:

ReaderWriterLockSlim提供对某资源在某时刻下的多线程同读、 或单线程独占写。
此外,ReaderWriterLockSlim还提供从读模式无缝升级到独占写模式。

总结下来:

读写锁处于以下四种状态:

  1. 未进入: 没有线程进入锁(或者所有线程退出锁)
  2. 读模式:每次调用EnterReadlock时,锁计数都会增加,但允许您读取其中的代码块。
  3. 写模式: 独占、排他
  4. 可升级的读模式(upgradeable read mode): 多线程读,其中一个线程具备在某时刻升级到排他写模式的可能。

这个就很适合常见的多读少写场景, 微软ReaderWriterLockSlim页面很贴心的提供了一个基于读写锁的缓存操作类SynchronizedCache

开箱即用的缓存操作类

基于ReaderWriterLockSlim对线程不安全的Dictionary进行了包装, 可以作为一个多读少写的缓存操作类。

public class SynchronizedCache
{
    private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
    private Dictionary<int, string> innerCache = new Dictionary<int, string>();

    public int Count
    { get { return innerCache.Count; } }

    public string Read(int key)
    {
        cacheLock.EnterReadLock();
        try
        {
            return innerCache[key];
        }
        finally
        {
            cacheLock.ExitReadLock();
        }
    }

    public void Add(int key, string value)
    {
        cacheLock.EnterWriteLock();
        try
        {
            innerCache.Add(key, value);
        }
        finally
        {
            cacheLock.ExitWriteLock();
        }
    }

    public bool AddWithTimeout(int key, string value, int timeout)
    {
        if (cacheLock.TryEnterWriteLock(timeout))
        {
            try
            {
                innerCache.Add(key, value);
            }
            finally
            {
                cacheLock.ExitWriteLock();
            }
            return true;
        }
        else
        {
            return false;
        }
    }

    public AddOrUpdateStatus AddOrUpdate(int key, string value)
    {
        cacheLock.EnterUpgradeableReadLock();
        try
        {
            string result = null;
            if (innerCache.TryGetValue(key, out result))
            {
                if (result == value)
                {
                    return AddOrUpdateStatus.Unchanged;
                }
                else
                {
                    cacheLock.EnterWriteLock();
                    try
                    {
                        innerCache[key] = value;
                    }
                    finally
                    {
                        cacheLock.ExitWriteLock();
                    }
                    return AddOrUpdateStatus.Updated;
                }
            }
            else
            {
                cacheLock.EnterWriteLock();
                try
                {
                    innerCache.Add(key, value);
                }
                finally
                {
                    cacheLock.ExitWriteLock();
                }
                return AddOrUpdateStatus.Added;
            }
        }
        finally
        {
            cacheLock.ExitUpgradeableReadLock();
        }
    }

    public void Delete(int key)
    {
        cacheLock.EnterWriteLock();
        try
        {
            innerCache.Remove(key);
        }
        finally
        {
            cacheLock.ExitWriteLock();
        }
    }

    public enum AddOrUpdateStatus
    {
        Added,
        Updated,
        Unchanged
    };

    ~SynchronizedCache()
    {
       if (cacheLock != null) cacheLock.Dispose();
    }
}

缓存操作类SynchronizedCache如常规的字典类一样, 不带值变更通知的能力,为满足【变更前清空连接池】的需求,我们还是添加event ,注册变更逻辑。

public event EventHandler<ValueChangedEventArgs<string>> OnValueChanged;

//--- 节选自AddOrUpdate方法
cacheLock.EnterWriteLock();
try
{
   OnValueChanged?.Invoke(this, new ValueChangedEventArgs<string>(key));
   innerCache[key] = value;
}
finally
{
    cacheLock.ExitWriteLock();
}
return AddOrUpdateStatus.Updated;

//---

if (sc.AddOrUpdate(key, value) == SynchronizedCache.AddOrUpdateStatus.Updated)
{
    Console.WriteLine($"已经发生了值变更,原key对应的键值已经被重写。");}
}

旁白

本文记录了读写锁在日常开发中的实践, 大多数场景都是多读少写,读者可以思考一下是不是也可以将项目中的无脑lock替换为SynchronizedCache


本文是同程艺龙DAL.Connection组件研发过程的一个小插曲,有心的读者可以往上翻一翻,了解上下文背景、了解小码甲的思考过程。

这就像我们高中做数学题,直接看答案并不能快速提升,结合上下文自然、流畅的转到这个方向才是最重要的。

09-01 16:46