淺談C#在網絡波動時防重複提交

    前幾天,公司數據庫出現了兩條相同的數據,並且時間相同(毫秒也相同)。排查緣由,發現是網絡波動形成了重複提交。git

    因爲網絡波動而重複提交的例子也比較多:github

 

    網絡上,防重複提交的方法也不少,使用redis鎖,代碼層面使用lock。redis

 

    可是,我沒有發現一個符合我心意的解決方案。由於網上的解決方案,第一次提交返回成功,第二次提交返回失敗。因爲兩次返回信息不一致,一次成功一次失敗,咱們不肯定客戶端是以哪一個返回信息爲準,雖然咱們但願客戶端以第一次返回成功的信息爲準,但客戶端也可能以第二次失敗信息運行,這是一個不肯定的結果。數據庫

 

在重複提交後,若是客戶端的接收到的信息都相同,都是成功,那客戶端就能夠正常運行,就不會影響用戶體驗。緩存

   

    我想到一個緩存類,來源於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> 類,能夠發現有如下幾個缺點性能

         一、 不會自動清空緩存,適合一些key很少的數據,不適合作爲網絡接口。測試

         二、 因爲_lock.EnterWriteLock,多線程會變成並單線程,不適合作爲網絡接口。

         三、 沒有過時緩存判斷。

 

    因而我對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)「暫留」。 這意味着整個程序中任何給定字符串都只有一個實例,就是這同一個對象表示了全部運行的應用程序域的全部線程中的該文本。所以,只要在應用程序進程中的任何位置處具備相同內容的字符串上放置了鎖,就將鎖定應用程序中該字符串的全部實例。

相關文章
相關標籤/搜索