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

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

浅谈C#在网络波动时防重复提交-LMLPHP

浅谈C#在网络波动时防重复提交-LMLPHP

    网络上,防重复提交的方法也很多,使用redis锁,代码层面使用lock。

    但是,我没有发现一个符合我心意的解决方案。因为网上的解决方案,第一次提交返回成功,第二次提交返回失败。由于两次返回信息不一致,一次成功一次失败,我们不确定客户端是以哪个返回信息为准,虽然我们希望客户端以第一次返回成功的信息为准,但客户端也可能以第二次失败信息运行,这是一个不确定的结果。

在重复提交后,如果客户端的接收到的信息都相同,都是成功,那客户端就可以正常运行,就不会影响用户体验。

   

    我想到一个缓存类,来源于PetaPoco。

Cache<TKey, TValue>代码如下:

 1     public class Cache<TKey, TValue>
 2     {
 3         private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
 4         private readonly Dictionary<TKey, TValue> _map = new Dictionary<TKey, TValue>();
 5
 6         public int Count {
 7             get { return _map.Count; }
 8         }
 9
10         public TValue Execute(TKey key, Func<TValue> factory)
11         {
12             // Check cache
13             _lock.EnterReadLock();
14             TValue val;
15             try {
16                 if (_map.TryGetValue(key, out val))
17                     return val;
18             } finally {
19                 _lock.ExitReadLock();
20             }
21
22             // Cache it
23             _lock.EnterWriteLock();
24             try {
25                 // Check again
26                 if (_map.TryGetValue(key, out val))
27                     return val;
28
29                 // Create it
30                 val = factory();
31
32                 // Store it
33                 _map.Add(key, val);
34
35                 // Done
36                 return val;
37             } finally {
38                 _lock.ExitWriteLock();
39             }
40         }
41
42         public void Clear()
43         {
44             // Cache it
45             _lock.EnterWriteLock();
46             try {
47                 _map.Clear();
48             } finally {
49                 _lock.ExitWriteLock();
50             }
51         }
52     }

    Cache<TKey, TValue>符合我的要求,第一次运行后,会将值缓存,第二次提交会返回第一次的值。

    但是,细细分析Cache<TKey, TValue> 类,可以发现有以下几个缺点

         1、 不会自动清空缓存,适合一些key不多的数据,不适合做为网络接口。

         2、 由于_lock.EnterWriteLock,多线程会变成并单线程,不适合做为网络接口。

         3、 没有过期缓存判断。

    于是我对Cache<TKey, TValue>进行改造。

AntiDupCache代码如下:

  1     /// <summary>
  2     /// 防重复缓存
  3     /// </summary>
  4     /// <typeparam name="TKey"></typeparam>
  5     /// <typeparam name="TValue"></typeparam>
  6     public class AntiDupCache<TKey, TValue>
  7     {
  8         private readonly int _maxCount;//缓存最高数量
  9         private readonly long _expireTicks;//超时 Ticks
 10         private long _lastTicks;//最后Ticks
 11         private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
 12         private readonly ReaderWriterLockSlim _slimLock = new ReaderWriterLockSlim();
 13         private readonly Dictionary<TKey, Tuple<long, TValue>> _map = new Dictionary<TKey, Tuple<long, TValue>>();
 14         private readonly Dictionary<TKey, AntiDupLockSlim> _lockDict = new Dictionary<TKey, AntiDupLockSlim>();
 15         private readonly Queue<TKey> _queue = new Queue<TKey>();
 16         class AntiDupLockSlim : ReaderWriterLockSlim { public int UseCount; }
 17
 18         /// <summary>
 19         /// 防重复缓存
 20         /// </summary>
 21         /// <param name="maxCount">缓存最高数量,0 不缓存,-1 缓存所有</param>
 22         /// <param name="expireSecond">超时秒数,0 不缓存,-1 永久缓存 </param>
 23         public AntiDupCache(int maxCount = 100, int expireSecond = 1)
 24         {
 25             if (maxCount < 0) {
 26                 _maxCount = -1;
 27             } else {
 28                 _maxCount = maxCount;
 29             }
 30             if (expireSecond < 0) {
 31                 _expireTicks = -1;
 32             } else {
 33                 _expireTicks = expireSecond * TimeSpan.FromSeconds(1).Ticks;
 34             }
 35         }
 36
 37         /// <summary>
 38         /// 个数
 39         /// </summary>
 40         public int Count {
 41             get { return _map.Count; }
 42         }
 43
 44         /// <summary>
 45         /// 执行
 46         /// </summary>
 47         /// <param name="key"></param>
 48         /// <param name="factory">执行方法</param>
 49         /// <returns></returns>
 50         public TValue Execute(TKey key, Func<TValue> factory)
 51         {
 52             // 过期时间为0 则不缓存
 53             if (object.Equals(null, key) || _expireTicks == 0L || _maxCount == 0) { return factory(); }
 54
 55             Tuple<long, TValue> tuple;
 56             long lastTicks;
 57             _lock.EnterReadLock();
 58             try {
 59                 if (_map.TryGetValue(key, out tuple)) {
 60                     if (_expireTicks == -1) return tuple.Item2;
 61                     if (tuple.Item1 + _expireTicks > DateTime.Now.Ticks) return tuple.Item2;
 62                 }
 63                 lastTicks = _lastTicks;
 64             } finally { _lock.ExitReadLock(); }
 65
 66
 67             AntiDupLockSlim slim;
 68             _slimLock.EnterUpgradeableReadLock();
 69             try {
 70                 _lock.EnterReadLock();
 71                 try {
 72                     if (_lastTicks != lastTicks) {
 73                         if (_map.TryGetValue(key, out tuple)) {
 74                             if (_expireTicks == -1) return tuple.Item2;
 75                             if (tuple.Item1 + _expireTicks > DateTime.Now.Ticks) return tuple.Item2;
 76                         }
 77                         lastTicks = _lastTicks;
 78                     }
 79                 } finally { _lock.ExitReadLock(); }
 80
 81                 _slimLock.EnterWriteLock();
 82                 try {
 83                     if (_lockDict.TryGetValue(key, out slim) == false) {
 84                         slim = new AntiDupLockSlim();
 85                         _lockDict[key] = slim;
 86                     }
 87                     slim.UseCount++;
 88                 } finally { _slimLock.ExitWriteLock(); }
 89             } finally { _slimLock.ExitUpgradeableReadLock(); }
 90
 91
 92             slim.EnterWriteLock();
 93             try {
 94                 _lock.EnterReadLock();
 95                 try {
 96                     if (_lastTicks != lastTicks && _map.TryGetValue(key, out tuple)) {
 97                         if (_expireTicks == -1) return tuple.Item2;
 98                         if (tuple.Item1 + _expireTicks > DateTime.Now.Ticks) return tuple.Item2;
 99                     }
100                 } finally { _lock.ExitReadLock(); }
101
102                 var val = factory();
103                 _lock.EnterWriteLock();
104                 try {
105                     _lastTicks = DateTime.Now.Ticks;
106                     _map[key] = Tuple.Create(_lastTicks, val);
107                     if (_maxCount > 0) {
108                         if (_queue.Contains(key) == false) {
109                             _queue.Enqueue(key);
110                             if (_queue.Count > _maxCount) _map.Remove(_queue.Dequeue());
111                         }
112                     }
113                 } finally { _lock.ExitWriteLock(); }
114                 return val;
115             } finally {
116                 slim.ExitWriteLock();
117                 _slimLock.EnterWriteLock();
118                 try {
119                     slim.UseCount--;
120                     if (slim.UseCount == 0) {
121                         _lockDict.Remove(key);
122                         slim.Dispose();
123                     }
124                 } finally { _slimLock.ExitWriteLock(); }
125             }
126         }
127         /// <summary>
128         /// 清空
129         /// </summary>
130         public void Clear()
131         {
132             _lock.EnterWriteLock();
133             try {
134                 _map.Clear();
135                 _queue.Clear();
136                 _slimLock.EnterWriteLock();
137                 try {
138                     _lockDict.Clear();
139                 } finally {
140                     _slimLock.ExitWriteLock();
141                 }
142             } finally {
143                 _lock.ExitWriteLock();
144             }
145         }
146
147     }

代码分析:

      使用两个ReaderWriterLockSlim锁 + 一个AntiDupLockSlim锁,实现并发功能。

      Dictionary<TKey, Tuple<long, TValue>> _map实现缓存,long类型值记录时间,实现缓存过期

      int _maxCount + Queue<TKey> _queue,_queue 记录key列队,当数量大于_maxCount,清除多余缓存。

      AntiDupLockSlim继承ReaderWriterLockSlim,实现垃圾回收,

代码使用 :

1    private readonly static AntiDupCache<int, int> antiDupCache = new AntiDupCache<int, int>(50, 1);
2
3     antiDupCache.Execute(key, () => {
4
5          ....
6
7          return val;
8
9     });

测试性能数据:

----------------------- 开始  从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)“暂留”。 这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。

04-13 19:34