1. 程式人生 > >淺談C#在網絡波動時防重復提交

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

很多 但是 放置 cut 鎖定 int 繼承 ade 清空緩存

前幾天,公司數據庫出現了兩條相同的數據,而且時間相同(毫秒也相同)。排查原因,發現是網絡波動造成了重復提交。

由於網絡波動而重復提交的例子也比較多:

技術分享圖片

技術分享圖片

網絡上,防重復提交的方法也很多,使用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)“暫留”。 這意味著整個程序中任何給定字符串都只有一個實例,就是這同一個對象表示了所有運行的應用程序域的所有線程中的該文本。因此,只要在應用程序進程中的任何位置處具有相同內容的字符串上放置了鎖,就將鎖定應用程序中該字符串的所有實例。

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