浅谈C#在网络波动时防重复提交的方法
前几天,公司数据库出现了两条相同的数据,而且时间相同(毫秒也相同)。排查原因,发现是网络波动造成了重复提交。
由于网络波动而重复提交的例子也比较多:
网络上,防重复提交的方法也很多,使用redis锁,代码层面使用lock。
但是,我没有发现一个符合我心意的解决方案。因为网上的解决方案,第一次提交返回成功,第二次提交返回失败。由于两次返回信息不一致,一次成功一次失败,我们不确定客户端是以哪个返回信息为准,虽然我们希望客户端以第一次返回成功的信息为准,但客户端也可能以第二次失败信息运行,这是一个不确定的结果。
在重复提交后,如果客户端的接收到的信息都相同,都是成功,那客户端就可以正常运行,就不会影响用户体验。
我想到一个缓存类,来源于PetaPoco。
Cache<TKey, TValue>代码如下:
public class Cache<TKey, TValue> { private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); private readonly Dictionary<TKey, TValue> _map = new Dictionary<TKey, TValue>(); public int Count { get { return _map.Count; } } public TValue Execute(TKey key, Func<TValue> factory) { // Check cache _lock.EnterReadLock(); TValue val; try { if (_map.TryGetValue(key, out val)) return val; } finally { _lock.ExitReadLock(); } // Cache it _lock.EnterWriteLock(); try { // Check again if (_map.TryGetValue(key, out val)) return val; // Create it val = factory(); // Store it _map.Add(key, val); // Done return val; } finally { _lock.ExitWriteLock(); } } public void Clear() { // Cache it _lock.EnterWriteLock(); try { _map.Clear(); } finally { _lock.ExitWriteLock(); } } }
Cache<TKey, TValue>符合我的要求,第一次运行后,会将值缓存,第二次提交会返回第一次的值。
但是,细细分析Cache<TKey, TValue> 类,可以发现有以下几个缺点
1、 不会自动清空缓存,适合一些key不多的数据,不适合做为网络接口。
2、 由于_lock.EnterWriteLock,多线程会变成并单线程,不适合做为网络接口。
3、 没有过期缓存判断。
于是我对Cache<TKey, TValue>进行改造。
AntiDupCache代码如下:
/// <summary> /// 防重复缓存 /// </summary> /// <typeparam name="TKey"></typeparam> /// <typeparam name="TValue"></typeparam> public class AntiDupCache<TKey, TValue> { private readonly int _maxCount;//缓存最高数量 private readonly long _expireTicks;//超时 Ticks private long _lastTicks;//最后Ticks private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); private readonly ReaderWriterLockSlim _slimLock = new ReaderWriterLockSlim(); private readonly Dictionary<TKey, Tuple<long, TValue>> _map = new Dictionary<TKey, Tuple<long, TValue>>(); private readonly Dictionary<TKey, AntiDupLockSlim> _lockDict = new Dictionary<TKey, AntiDupLockSlim>(); private readonly Queue<TKey> _queue = new Queue<TKey>(); class AntiDupLockSlim : ReaderWriterLockSlim { public int UseCount; } /// <summary> /// 防重复缓存 /// </summary> /// <param name="maxCount">缓存最高数量,0 不缓存,-1 缓存所有</param> /// <param name="expireSecond">超时秒数,0 不缓存,-1 永久缓存 </param> public AntiDupCache(int maxCount = 100, int expireSecond = 1) { if (maxCount < 0) { _maxCount = -1; } else { _maxCount = maxCount; } if (expireSecond < 0) { _expireTicks = -1; } else { _expireTicks = expireSecond * TimeSpan.FromSeconds(1).Ticks; } } /// <summary> /// 个数 /// </summary> public int Count { get { return _map.Count; } } /// <summary> /// 执行 /// </summary> /// <param name="key">值</param> /// <param name="factory">执行方法</param> /// <returns></returns> public TValue Execute(TKey key, Func<TValue> factory) { // 过期时间为0 则不缓存 if (object.Equals(null, key) || _expireTicks == 0L || _maxCount == 0) { return factory(); } Tuple<long, TValue> tuple; long lastTicks; _lock.EnterReadLock(); try { if (_map.TryGetValue(key, out tuple)) { if (_expireTicks == -1) return tuple.Item2; if (tuple.Item1 + _expireTicks > DateTime.Now.Ticks) return tuple.Item2; } lastTicks = _lastTicks; } finally { _lock.ExitReadLock(); } AntiDupLockSlim slim; _slimLock.EnterUpgradeableReadLock(); try { _lock.EnterReadLock(); try { if (_lastTicks != lastTicks) { if (_map.TryGetValue(key, out tuple)) { if (_expireTicks == -1) return tuple.Item2; if (tuple.Item1 + _expireTicks > DateTime.Now.Ticks) return tuple.Item2; } lastTicks = _lastTicks; } } finally { _lock.ExitReadLock(); } _slimLock.EnterWriteLock(); try { if (_lockDict.TryGetValue(key, out slim) == false) { slim = new AntiDupLockSlim(); _lockDict[key] = slim; } slim.UseCount++; } finally { _slimLock.ExitWriteLock(); } } finally { _slimLock.ExitUpgradeableReadLock(); } slim.EnterWriteLock(); try { _lock.EnterReadLock(); try { if (_lastTicks != lastTicks && _map.TryGetValue(key, out tuple)) { if (_expireTicks == -1) return tuple.Item2; if (tuple.Item1 + _expireTicks > DateTime.Now.Ticks) return tuple.Item2; } } finally { _lock.ExitReadLock(); } var val = factory(); _lock.EnterWriteLock(); try { _lastTicks = DateTime.Now.Ticks; _map[key] = Tuple.Create(_lastTicks, val); if (_maxCount > 0) { if (_queue.Contains(key) == false) { _queue.Enqueue(key); if (_queue.Count > _maxCount) _map.Remove(_queue.Dequeue()); } } } finally { _lock.ExitWriteLock(); } return val; } finally { slim.ExitWriteLock(); _slimLock.EnterWriteLock(); try { slim.UseCount--; if (slim.UseCount == 0) { _lockDict.Remove(key); slim.Dispose(); } } finally { _slimLock.ExitWriteLock(); } } } /// <summary> /// 清空 /// </summary> public void Clear() { _lock.EnterWriteLock(); try { _map.Clear(); _queue.Clear(); _slimLock.EnterWriteLock(); try { _lockDict.Clear(); } finally { _slimLock.ExitWriteLock(); } } finally { _lock.ExitWriteLock(); } } }
代码分析:
使用两个ReaderWriterLockSlim锁 + 一个AntiDupLockSlim锁,实现并发功能。
Dictionary<TKey, Tuple<long, TValue>> _map实现缓存,long类型值记录时间,实现缓存过期
int _maxCount + Queue<TKey> _queue,_queue 记录key列队,当数量大于_maxCount,清除多余缓存。
AntiDupLockSlim继承ReaderWriterLockSlim,实现垃圾回收,
代码使用 :
private readonly static AntiDupCache<int, int> antiDupCache = new AntiDupCache<int, int>(50, 1); antiDupCache.Execute(key, () => { .... return val; });
测试性能数据:
----------------------- 开始 从1到100 重复次数:1 单位: ms -----------------------
并发数量: 1 2 3 4 5 6 7 8 9 10 11 12
普通并发: 188 93 65 46 38 36 28 31 22 20 18 19
AntiDupCache: 190 97 63 48 37 34 29 30 22 18 17 21
AntiDupQueue: 188 95 63 46 37 33 30 25 21 19 17 21
DictCache: 185 96 64 47 38 33 28 29 22 19 17 21
Cache: 185 186 186 188 188 188 184 179 180 184 184 176
第二次普通并发: 180 92 63 47 38 36 26 28 20 17 16 20
----------------------- 开始 从1到100 重复次数:2 单位: ms -----------------------
并发数量: 1 2 3 4 5 6 7 8 9 10 11 12
普通并发: 368 191 124 93 73 61 55 47 44 37 34 44
AntiDupCache: 180 90 66 48 37 31 28 24 21 17 17 22
AntiDupQueue: 181 93 65 46 39 31 27 23 21 19 18 19
DictCache: 176 97 61 46 38 30 31 23 21 18 18 22
Cache: 183 187 186 182 186 185 184 177 181 177 176 177
第二次普通并发: 366 185 127 95 71 62 56 48 43 38 34 43
----------------------- 开始 从1到100 重复次数:4 单位: ms -----------------------
并发数量: 1 2 3 4 5 6 7 8 9 10 11 12
普通并发: 726 371 253 190 152 132 106 91 86 74 71 69
AntiDupCache: 189 95 64 49 37 33 28 26 22 19 17 18
AntiDupQueue: 184 97 65 51 39 35 28 24 21 18 17 17
DictCache: 182 95 64 45 39 34 29 23 21 18 18 16
Cache: 170 181 180 184 182 183 181 181 176 179 179 178
第二次普通并发: 723 375 250 186 150 129 107 94 87 74 71 67
----------------------- 开始 从1到100 重复次数:12 单位: ms -----------------------
并发数量: 1 2 3 4 5 6 7 8 9 10 11 12
普通并发: 2170 1108 762 569 450 389 325 283 253 228 206 186
AntiDupCache: 182 95 64 51 41 32 28 25 26 20 18 18
AntiDupQueue: 189 93 67 44 37 35 29 30 27 22 20 17
DictCache: 184 97 59 50 38 29 27 26 24 19 18 17
Cache: 174 189 181 184 184 177 182 180 176 176 180 179
第二次普通并发: 2190 1116 753 560 456 377 324 286 249 227 202 189
仿线上环境,性能测试数据:
----------------------- 仿线上环境 从1到1000 单位: ms -----------------------
并发数量: 1 2 3 4 5 6 7 8 9 10 11 12
普通并发: 1852 950 636 480 388 331 280 241 213 198 181 168
AntiDupCache: 1844 949 633 481 382 320 267 239 210 195 174 170
AntiDupQueue: 1835 929 628 479 386 318 272 241 208 194 174 166
DictCache: 1841 935 629 480 378 324 269 241 207 199 176 168
Cache: 1832 1854 1851 1866 1858 1858 1832 1825 1801 1797 1788 1785
第二次普通并发: 1854 943 640 468 389 321 273 237 209 198 177 172
项目:
Github: https://github.com/toolgood/ToolGood.AntiDuplication
Nuget: Install-Package ToolGood.AntiDuplication
后记:
尝试添加 一个Queue<AntiDupLockSlim> 或Stack<AntiDupLockSlim> 用来缓存锁,后发现性能效率相差不大,上下浮动。
使用 lock关键字加锁,速度相差不大,代码看似更简单,但隐藏了一个地雷:一般人使用唯一键都是使用string,就意味着可能使用lock(string),锁定字符串尤其危险,因为字符串被公共语言运行库 (CLR)“暂留”。 这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持猪先飞。
相关文章
- 我们在使用C#做项目的时候,基本上都需要制作登录界面,那么今天我们就来一步步看看,如果简单的实现登录界面呢,本文给出2个例子,由简入难,希望大家能够喜欢。...2020-06-25
- 这篇文章主要介绍了C# 字段和属性的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下...2020-11-03
- 这篇文章主要介绍了C#中截取字符串的的基本方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-03
- 本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
- 这篇文章主要介绍了C#实现简单的Http请求的方法,以实例形式较为详细的分析了C#实现Http请求的具体方法,需要的朋友可以参考下...2020-06-25
- 本文主要介绍了C#中new的几种用法,具有很好的参考价值,下面跟着小编一起来看下吧...2020-06-25
使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序)
这篇文章主要介绍了使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25- 这篇文章主要介绍了C#开发Windows窗体应用程序的简单操作步骤,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-04-12
- 这篇文章主要介绍了C#从数据库读取图片并保存的方法,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下...2021-01-16
- 最近做一个小项目不可避免的需要前端脚本与后台进行交互。由于是在asp.net中实现,故问题演化成asp.net中jiavascript与后台c#如何进行交互。...2020-06-25
- 本文通过例子,讲述了C++调用C#的DLL程序的方法,作出了以下总结,下面就让我们一起来学习吧。...2020-06-25
- 轻松学习C#的基础入门,了解C#最基本的知识点,C#是一种简洁的,类型安全的一种完全面向对象的开发语言,是Microsoft专门基于.NET Framework平台开发的而量身定做的高级程序设计语言,需要的朋友可以参考下...2020-06-25
- 本文主要介绍了C#变量命名规则小结,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-09
- 这篇文章主要介绍了C#绘制曲线图的方法,以完整实例形式较为详细的分析了C#进行曲线绘制的具体步骤与相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25
- 本文主要介绍了C# 中取绝对值的函数。具有很好的参考价值。下面跟着小编一起来看下吧...2020-06-25
- 这篇文章主要介绍了c#自带缓存使用方法,包括获取数据缓存、设置数据缓存、移除指定数据缓存等方法,需要的朋友可以参考下...2020-06-25
- 这篇文章主要介绍了c#中(&&,||)与(&,|)的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-25
- 这篇文章主要用实例讲解C#递归算法的概念以及用法,文中代码非常详细,帮助大家更好的参考和学习,感兴趣的朋友可以了解下...2020-06-25
- 下面小编就为大家带来一篇C#学习笔记- 随机函数Random()的用法详解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
- 这篇文章主要介绍了C#中list用法,结合实例形式分析了C#中list排序、运算、转换等常见操作技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25