浅谈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 = , int expireSecond = )
{
if (maxCount < ) {
_maxCount = -;
} else {
_maxCount = maxCount;
}
if (expireSecond < ) {
_expireTicks = -;
} else {
_expireTicks = expireSecond * TimeSpan.FromSeconds().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 == ) { return factory(); } Tuple<long, TValue> tuple;
long lastTicks;
_lock.EnterReadLock();
try {
if (_map.TryGetValue(key, out tuple)) {
if (_expireTicks == -) 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 == -) 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 == -) 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 > ) {
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 == ) {
_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>(, ); 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#在网络波动时防重复提交的更多相关文章
- 浅谈测试rhel7新功能时的感受及遇到的问题【转载】
半夜起来看世界杯,没啥激情,但是又怕错误意大利和英格兰的比赛,就看了rhel7 相关新功能的介绍. rhel7的下载地址: https://access.redhat.com/site/downloa ...
- AJAX防重复提交的办法总结
最近的维护公司的一个代理商平台的时候,客服人员一直反映说的统计信息的时候有重复数据,平台一直都很正常,这个功能是最近新进的一个实习生同事写的功能,然后就排查问题人所在,发现新的这个模块的AJAX提交数 ...
- 使用aop注解实现表单防重复提交功能
原文:https://www.cnblogs.com/manliu/articles/5983888.html 1.这里采用的方法是:使用get请求进入表单页面时,后台会生成一个tokrn_flag分 ...
- (亿级流量)分布式防重复提交token设计
大型互联网项目中,很多流量都达到亿级.同一时间很多的人在使用,而每个用户提交表单的时候都可能会出现重复点击的情况,此时如果不做好控制,那么系统将会产生很多的数据重复的问题.怎样去设计一个高可用的防重复 ...
- (九)Struts2 防重复提交
所有的学习我们必须先搭建好Struts2的环境(1.导入对应的jar包,2.web.xml,3.struts.xml) 第一节:重复提交示例演示 struts.xml <?xml version ...
- SpringMVC后台token防重复提交解决方案
本文介绍如何使用token来防止前端重复提交的问题. 目录 1.思路 2.拦截器源码实现 3.注解源码 4.拦截器的配置 5.使用指南 6.结语 思路 1.添加拦截器,拦截需要防重复提交的请求 2.通 ...
- Spring MVC表单防重复提交
利用Spring MVC的过滤器及token传递验证来实现表单防重复提交. 创建注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RU ...
- struts2学习(15)struts2防重复提交
一.重复提交的例子: 模拟一种情况,存在延时啊,系统比较繁忙啊啥的. 模拟延迟5s钟,用户点了一次提交,又点了一次提交,例子中模拟这种情况: 这样会造成重复提交: com.cy.action.St ...
- JavaWeb -- Struts2,对比, 简单表单提交,校验,防重复提交, 文件上传
Struts2核心流程图 1. Struts2 和 Struts1 对比 struts1:基于Servlet(ActionServlet),actionForm众多(类的爆炸),action单例(数据 ...
随机推荐
- 原生JS和jQuery操作DOM的区别小结
一.Js原生对象和jQuery实例对象的相互转化: (1).原生JS对象转JQ对象: $(DOM对象); (2). JQ对象转原生JS对象: $(DOM对象).get(index); //注意区分eq ...
- Labview-vi的可重入性
VI可重入性: labview多线程中 同时对一个子vi访问时,可能会造成同时对同一块内存地址读写所造成的数据混乱,当选择 vi属性(Ctrl+i)中执行选项卡允许可重入时,labview会分配不同的 ...
- LXC vs Docker
https://www.sumologic.com/blog/code/lxc-lxd-explaining-linux-containers/ see also: https://linuxcont ...
- 我的学习目标(目前已初步学习完Java语言基础)
操作系统.尤其是内存/线程/进程方面 计算机网络协议,重点关注 TCP/UDP/HTTP. 数据结构与算法. 数据库 设计模式,熟练掌握常用的几种设计模式. Java语言基础.熟悉java语言基础,了 ...
- 微信小程序前端开发踩坑(一)
之前由于不了解微信小程序的整个的运行开发机制,走了很多的弯路,脑子灵光的可能不会遇到,这个主题系列的帖子希望可以帮助到像我一样理解能力慢的孩子. 不论是开发微信小程序还是说学习任何一门编程语言,最重要 ...
- spring 集成redis客户端jedis(java)
spring集成jedis简单实例 jedis是redis的java客户端,spring将redis连接池作为一个bean配置. “redis.clients.jedis.JedisPool”,这 ...
- 直接执行sql字符串
$sql_tmp= "UPDATE `eabc_order_detail` set send_number=num where order_sn='".$model_order-& ...
- Stackoverflow热门问题
1. JavaScript如何重定向到其他网页 如何使用JavaScript将用户从一个网页重定向到另一个网页? 2. JavaScript闭包是如何工作的 只知道JavaScript闭包的概念,但是 ...
- HDU - 5833: Zhu and 772002 (高斯消元-自由元)
pro:给定N个数Xi(Xi<1e18),保证每个数的素因子小于2e3:问有多少种方案,选处一些数,使得数的乘积是完全平方数.求答案%1e9+7: N<300; sol:小于2e3的素数只 ...
- Python全栈之路----常用模块----re 模块
正则表达式就是字符串的匹配规则,在多数编程语言里都有相应的支持,python里对应的模块是 re. re的匹配语法有以下几种 re.match 从头开始匹配 re.search 匹配包含 re.fin ...