当前位置: 首页 > 编程笔记 >

浅谈C#在网络波动时防重复提交的方法

祁远
2023-03-14
本文向大家介绍浅谈C#在网络波动时防重复提交的方法,包括了浅谈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)“暂留”。 这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持小牛知识库。

 类似资料:
  • 问题内容: 我的表单需要服务器处理一些时间。我需要确保用户等待并且不会再次单击按钮来尝试重新提交表单。我尝试使用以下jQuery代码: 当我在Firefox中尝试此操作时,所有功能都被禁用,但是该表单未与应该包含的任何POST数据一起提交。我不能使用jQuery提交表单,因为我需要将按钮与表单一起提交,因为有多个提交按钮,并且我确定POST中包含哪个值使用了哪个按钮。我需要像平常一样提交表单,并且

  • 本文向大家介绍浅谈C++中replace()方法,包括了浅谈C++中replace()方法的使用技巧和注意事项,需要的朋友参考一下 本文主要针对c++中常用replace函数用法给出九个样例程序: 用法一:  运行结果: 用法二:  运行结果: 用法三:  运行结果: 用法四:string转char*时编译器可能会报出警告,不建议这样做  运行结果: 用法五:string转char*时编译器可能会

  • 本文向大家介绍springboot实现防重复提交和防重复点击的示例,包括了springboot实现防重复提交和防重复点击的示例的使用技巧和注意事项,需要的朋友参考一下 背景 同一条数据被用户点击了多次,导致数据冗余,需要防止弱网络等环境下的重复点击 目标 通过在指定的接口处添加注解,实现根据指定的接口参数来防重复点击 说明 这里的重复点击是指在指定的时间段内多次点击按钮 技术方案 springbo

  • 本文向大家介绍防止Layui form表单重复提交的实现方法,包括了防止Layui form表单重复提交的实现方法的使用技巧和注意事项,需要的朋友参考一下 在提交表单数据时,提交按钮为 submit 类型,以layui为js框架时,会重复提交表单数据,为防止这一情况,有效的做法是: 在<form>里面,加上 lay-filter="formConfig" , οnsubmit="return fa

  • 本文向大家介绍浅谈Servlet 实现网页重定向的方法,包括了浅谈Servlet 实现网页重定向的方法的使用技巧和注意事项,需要的朋友参考一下 本文介绍了Servlet 实现重定向的方法,分享给大家,具体如下: HttpServletResponse 服务器响应客户端请求时,就要用到HttpServletResponse接口。设置响应的类型可以使用setContentType()方法。发送字符数据

  • 问题内容: 我有的: 我有一个通过AJAX提交的表格。 我想要的是: 我想防止重复提交。由于仅在成功完成请求后该表单才会消失,因此只要数据库尚未完成数据写入,用户就可以单击“提交”按钮。显然,我不需要。 我尝试过的 我尝试将其添加到“提交”按钮本身中,但是它不起作用。该按钮被禁用,但是没有数据发送。 我也尝试将点击处理程序添加到“提交”按钮。具有与以前相同的效果。实际上没有数据发送到控制器,但是按