前情提要
同程艺龙基础架构部推出的数据获取组件DAL.Connection,我们要做到在切换连接配置时清空数据库连接池, 这就涉及到切换连接的时候,触发变更通知。
- .NET 如何清空连接池?
- 面试官:实现一个带值变更通知能力的Dictionary
仔细阅读《面试官:实现一个带值变更通知能力的Dictionary》一文的童靴们有没有发现一个细节: 我使用了lock
语法糖无脑加锁。
这在高并发下会有问题:大多数时候下DBA并不会变更业务方的数据库连接,这是一个多读少写的场景, 我们无脑使用lock在多数时间会人为阻塞请求。
到这个时候,我们就要想到读写锁ReaderWriterLockSlim
。
宝藏好物:ReaderWriterLockSlim
简而言之:
ReaderWriterLockSlim
提供对某资源在某时刻下的多线程同读、 或单线程独占写。
此外,ReaderWriterLockSlim
还提供从读模式无缝升级到独占写模式。
总结下来:
读写锁处于以下四种状态:
- 未进入: 没有线程进入锁(或者所有线程退出锁)
- 读模式:每次调用
EnterReadlock
时,锁计数都会增加,但允许您读取其中的代码块。 - 写模式: 独占、排他
- 可升级的读模式(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组件研发过程的一个小插曲,有心的读者可以往上翻一翻,了解上下文背景、了解小码甲的思考过程。
这就像我们高中做数学题,直接看答案并不能快速提升,结合上下文自然、流畅的转到这个方向才是最重要的。