前几天,公司数据库出现了两条相同的数据,而且时间相同(毫秒也相同)。排查原因,发现是网络波动造成了重复提交。

由于网络波动而重复提交的例子也比较多:

网络上,防重复提交的方法也很多,使用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>
         , )
         {
             ) {
                 _maxCount = -;
             } else {
                 _maxCount = maxCount;
             }
             ) {
                 _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 则不缓存
             ) { return factory(); }

             Tuple<long, TValue> tuple;
             long lastTicks;
             _lock.EnterReadLock();
             try {
                 if (_map.TryGetValue(key, out tuple)) {
                     ) 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)) {
                             ) 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)) {
                         ) 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 (_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--;
                     ) {
                         _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,实现垃圾回收,

代码使用 :

    , );

     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#在网络波动时防重复提交的更多相关文章

  1. 浅谈测试rhel7新功能时的感受及遇到的问题【转载】

    半夜起来看世界杯,没啥激情,但是又怕错误意大利和英格兰的比赛,就看了rhel7 相关新功能的介绍. rhel7的下载地址: https://access.redhat.com/site/downloa ...

  2. AJAX防重复提交的办法总结

    最近的维护公司的一个代理商平台的时候,客服人员一直反映说的统计信息的时候有重复数据,平台一直都很正常,这个功能是最近新进的一个实习生同事写的功能,然后就排查问题人所在,发现新的这个模块的AJAX提交数 ...

  3. 使用aop注解实现表单防重复提交功能

    原文:https://www.cnblogs.com/manliu/articles/5983888.html 1.这里采用的方法是:使用get请求进入表单页面时,后台会生成一个tokrn_flag分 ...

  4. (九)Struts2 防重复提交

    所有的学习我们必须先搭建好Struts2的环境(1.导入对应的jar包,2.web.xml,3.struts.xml) 第一节:重复提交示例演示 struts.xml <?xml version ...

  5. SpringMVC后台token防重复提交解决方案

    本文介绍如何使用token来防止前端重复提交的问题. 目录 1.思路 2.拦截器源码实现 3.注解源码 4.拦截器的配置 5.使用指南 6.结语 思路 1.添加拦截器,拦截需要防重复提交的请求 2.通 ...

  6. Spring MVC表单防重复提交

    利用Spring MVC的过滤器及token传递验证来实现表单防重复提交. 创建注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RU ...

  7. struts2学习(15)struts2防重复提交

    一.重复提交的例子: 模拟一种情况,存在延时啊,系统比较繁忙啊啥的. 模拟延迟5s钟,用户点了一次提交,又点了一次提交,例子中模拟这种情况: 这样会造成重复提交:   com.cy.action.St ...

  8. JavaWeb -- Struts2,对比, 简单表单提交,校验,防重复提交, 文件上传

    Struts2核心流程图 1. Struts2 和 Struts1 对比 struts1:基于Servlet(ActionServlet),actionForm众多(类的爆炸),action单例(数据 ...

  9. 防止跨站请求伪造(CSRF)攻击 和 防重复提交 的方法的实现

    CSRF的概念可以参考:http://netsecurity.51cto.com/art/200812/102951.htm 本文介绍的是基于spring拦截器的Spring MVC实现 首先配置拦截 ...

随机推荐

  1. Shortest Palindrome

    Given a string S, you are allowed to convert it to a palindrome by adding characters in front of it. ...

  2. 对MySql查询缓存及SQL Server过程缓存的理解及总结

    一.MySql的Query Cache 1.Query Cache MySQL Query Cache是用来缓存我们所执行的SELECT语句以及该语句的结果集.MySql在实现Query Cache的 ...

  3. python中的异常捕获怎么用?

    http://www.2cto.com/kf/201301/184121.html http://www.w3cschool.cc/python/python-exceptions.html try: ...

  4. Linux安装、卸载软件

    在linux环境中,尤其是cenos中安装过一些软件,一般是二进制安装与源码安装,现小结一下linux中的安装与卸载. 一.通常Linux应用软件的安装包有三种: 1) tar包,如software- ...

  5. magento additional &amp; details 分解开来

    <?php foreach ($this->getChildGroup('detailed_info', 'getChildHtml') as $alias => $html):?& ...

  6. python3 树莓派 + usb摄像头 做颜色识别 二维码识别

    今天又啥也没干 我完蛋了哦  就是没办法沉下心来,咋办....还是先来条NLP吧.. 七,凡事必有至少三个解决方法 对事情只有一个方法的人,必陷入困境,因为别无选择. 对事情有两个方法的人也陷入困境, ...

  7. 几个常用的vscode插件

    1.Vetur 可以对.vue文件格式做语法高亮,开发基于Vue项目的标配插件 2.Power Mode 在写代码时出现如烟花.粒子.火焰之类的效果,增加写代码的乐趣 3.Chinese (simpl ...

  8. [angularjs] AngularJs 知识回顾

    AngularJs 知识回顾 简介 1.通过指令扩展了 HTML,通过表达式绑定数据到 HTML: 2.一个 Js 框架: 3.指令: 通过被称为 指令 的新属性来扩展 HTML, 即为应用添加新功能 ...

  9. python爬虫之初始scrapy

    简介: Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架. 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中. 其最初是为了 页面抓取 (更确切来说, 网络抓取 )所设 ...

  10. SpringData使用与整合

    SpringData 整合源码:链接: https://pan.baidu.com/s/1_dDEEJoqaBTfXs2ZWsvKvA 提取码: cp6s(jar包自行寻找) author:Simpl ...