面试官: 你平时用过读写锁吗?
前情提要
同程艺龙基础架构部推出的数据获取组件DAL.Connection,我们要做到在切换连接配置时清空数据库连接池, 这就涉及到切换连接的时候,触发变更通知。
- .NET 如何清空连接池?
- 面试官:实现一个带值变更通知能力的Dictionary
仔细阅读《面试官:实现一个带值变更通知能力的Dictionary》一文的童靴们有没有发现一个细节: 我使用了lock
语法糖无脑加锁。
这里面有个前置知识点:C# Dictionary线程不安全。
什么叫线程不安全,请看这个: https://www.cnblogs.com/JulianHuang/p/14720042.html。
这在高并发下会有问题:大多数时候下DBA并不会变更业务方的数据库连接,这是一个多读少写的场景, 我们无脑使用lock在多数时间会人为阻塞请求。
到这个时候,我们就要想到读写锁ReaderWriterLockSlim
。
宝藏好物:ReaderWriterLockSlim
Use ReaderWriterLockSlim to protect a resource that is read by multiple threads and written to by one thread at a time. ReaderWriterLockSlim allows multiple threads to be in read mode, allows one thread to be in write mode with exclusive ownership of the lock, and allows one thread that has read access to be in upgradeable read mode, from which the thread can upgrade to write mode without having to relinquish its read access to the resource.
简而言之:
ReaderWriterLockSlim
提供对某资源在某时刻下的多线程同读、 或单线程独占写。
此外,ReaderWriterLockSlim
还提供从读模式无缝升级到独占写模式。
总结下来:
读写锁处于以下四种状态:
- 未进入: 没有线程进入锁(或者所有线程退出锁)
- 读模式:每次调用
EnterReadlock
时,锁计数都会增加,但允许您读取其中的代码块。 - 写模式: 独占、排他
- 可升级的读模式(upgradeable read mode): 多线程读,其中一个线程具备在某时刻升级到排他写模式的可能。
btw,读写锁相比常规lock之外,还具备锁超时的机制,能避免未知原因持续占有锁导致的死锁。
这个就很适合常见的多读少写场景, 微软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组件研发过程的一个小插曲,有心的读者可以往上翻一翻,了解上下文背景、了解小码甲的思考过程。
这就像我们高中做数学题,直接看答案并不能快速提升,结合上下文自然、流畅的转到这个方向才是最重要的。
原文地址:https://www.cnblogs.com/JulianHuang/p/15211546.html
- Nginx权限提升漏洞(CVE-2016-1247 )分析
- 初识 Fuzzing 工具 WinAFL
- 如何使用Oozie API接口向Kerberos环境的CDH集群提交Spark2作业
- 如何编译及使用TPC-DS生成测试数据
- ASP.NET MVC编程——缓存
- ASP.NET MVC编程——错误处理与日记
- Jenkins 未授权远程代码执行漏洞(CVE-2017-1000353)
- ASP.NET MVC编程——路由
- ASP.NET MVC编程——模型
- ASP.NET MVC编程——验证、授权与安全
- 如何使用SAML配置CDSW的身份验证
- ASP.NET MVC编程——控制器
- ASP.NET MVC编程——视图
- 设计原则
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 团体程序设计天梯赛-练习集 L1-002 打印沙漏
- 团体程序设计天梯赛-练习集 L1-003 个位数统计
- 团体程序设计天梯赛-练习集 L1-004 计算摄氏温度
- 团体程序设计天梯赛-练习集 L1-007 念数字
- 团体程序设计天梯赛-练习集 L1-008 求整数段和
- K8s多租户场景下的多层级namespace规则解析
- 团体程序设计天梯赛-练习集 L1-010 比较大小
- 团体程序设计天梯赛-练习集 L1-012 计算指数
- 团体程序设计天梯赛-练习集 L1-013 计算阶乘和
- 团体程序设计天梯赛-练习集 L1-036 A乘以B
- 团体程序设计天梯赛-练习集 L1-015 跟奥巴马一起画方块
- 团体程序设计天梯赛-练习集 L1-011 A-B
- vuepress引入vue-qr组件后build报错navigator is not defined问题
- 团体程序设计天梯赛-练习集 L1-022 奇偶分家
- k8s——针对有状态服务实现数据持久化