淺談C#在網路波動時防重複提交
前幾天,公司資料庫出現了兩條相同的資料,而且時間相同(毫秒也相同)。排查原因,發現是網路波動造成了重複提交。
由於網路波動而重複提交的例子也比較多:
網路上,防重複提交的方法也很多,使用redis鎖,程式碼層面使用lock。
但是,我沒有發現一個符合我心意的解決方案。因為網上的解決方案,第一次提交返回成功,第二次提交返回失敗。由於兩次返回資訊不一致,一次成功一次失敗,我們不確定客戶端是以哪個返回資訊為準,雖然我們希望客戶端以第一次返回成功的資訊為準,但客戶端也可能以第二次失敗資訊執行,這是一個不確定的結果。
在重複提交後,如果客戶端的接收到的資訊都相同,都是成功,那客戶端就可以正常執行,就不會影響使用者體驗。
我想到一個快取類,來源於PetaPoco。
Cache<TKey, TValue>程式碼如下:
1public class Cache<TKey, TValue> 2{ 3private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); 4private readonly Dictionary<TKey, TValue> _map = new Dictionary<TKey, TValue>(); 5 6public int Count { 7get { return _map.Count; } 8} 9 10public TValue Execute(TKey key, Func<TValue> factory) 11{ 12// Check cache 13_lock.EnterReadLock(); 14TValue val; 15try { 16if (_map.TryGetValue(key, out val)) 17return val; 18} finally { 19_lock.ExitReadLock(); 20} 21 22// Cache it 23_lock.EnterWriteLock(); 24try { 25// Check again 26if (_map.TryGetValue(key, out val)) 27return val; 28 29// Create it 30val = factory(); 31 32// Store it 33_map.Add(key, val); 34 35// Done 36return val; 37} finally { 38_lock.ExitWriteLock(); 39} 40} 41 42public void Clear() 43{ 44// Cache it 45_lock.EnterWriteLock(); 46try { 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> 6public class AntiDupCache<TKey, TValue> 7{ 8private readonly int _maxCount;//快取最高數量 9private readonly long _expireTicks;//超時 Ticks 10private long _lastTicks;//最後Ticks 11private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); 12private readonly ReaderWriterLockSlim _slimLock = new ReaderWriterLockSlim(); 13private readonly Dictionary<TKey, Tuple<long, TValue>> _map = new Dictionary<TKey, Tuple<long, TValue>>(); 14private readonly Dictionary<TKey, AntiDupLockSlim> _lockDict = new Dictionary<TKey, AntiDupLockSlim>(); 15private readonly Queue<TKey> _queue = new Queue<TKey>(); 16class 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> 23public AntiDupCache(int maxCount = 100, int expireSecond = 1) 24{ 25if (maxCount < 0) { 26_maxCount = -1; 27} else { 28_maxCount = maxCount; 29} 30if (expireSecond < 0) { 31_expireTicks = -1; 32} else { 33_expireTicks = expireSecond * TimeSpan.FromSeconds(1).Ticks; 34} 35} 36 37/// <summary> 38/// 個數 39/// </summary> 40public int Count { 41get { return _map.Count; } 42} 43 44/// <summary> 45/// 執行 46/// </summary> 47/// <param name="key">值</param> 48/// <param name="factory">執行方法</param> 49/// <returns></returns> 50public TValue Execute(TKey key, Func<TValue> factory) 51{ 52// 過期時間為0 則不快取 53if (object.Equals(null, key) || _expireTicks == 0L || _maxCount == 0) { return factory(); } 54 55Tuple<long, TValue> tuple; 56long lastTicks; 57_lock.EnterReadLock(); 58try { 59if (_map.TryGetValue(key, out tuple)) { 60if (_expireTicks == -1) return tuple.Item2; 61if (tuple.Item1 + _expireTicks > DateTime.Now.Ticks) return tuple.Item2; 62} 63lastTicks = _lastTicks; 64} finally { _lock.ExitReadLock(); } 65 66 67AntiDupLockSlim slim; 68_slimLock.EnterUpgradeableReadLock(); 69try { 70_lock.EnterReadLock(); 71try { 72if (_lastTicks != lastTicks) { 73if (_map.TryGetValue(key, out tuple)) { 74if (_expireTicks == -1) return tuple.Item2; 75if (tuple.Item1 + _expireTicks > DateTime.Now.Ticks) return tuple.Item2; 76} 77lastTicks = _lastTicks; 78} 79} finally { _lock.ExitReadLock(); } 80 81_slimLock.EnterWriteLock(); 82try { 83if (_lockDict.TryGetValue(key, out slim) == false) { 84slim = new AntiDupLockSlim(); 85_lockDict[key] = slim; 86} 87slim.UseCount++; 88} finally { _slimLock.ExitWriteLock(); } 89} finally { _slimLock.ExitUpgradeableReadLock(); } 90 91 92slim.EnterWriteLock(); 93try { 94_lock.EnterReadLock(); 95try { 96if (_lastTicks != lastTicks && _map.TryGetValue(key, out tuple)) { 97if (_expireTicks == -1) return tuple.Item2; 98if (tuple.Item1 + _expireTicks > DateTime.Now.Ticks) return tuple.Item2; 99} 100} finally { _lock.ExitReadLock(); } 101 102var val = factory(); 103_lock.EnterWriteLock(); 104try { 105_lastTicks = DateTime.Now.Ticks; 106_map[key] = Tuple.Create(_lastTicks, val); 107if (_maxCount > 0) { 108if (_queue.Contains(key) == false) { 109_queue.Enqueue(key); 110if (_queue.Count > _maxCount) _map.Remove(_queue.Dequeue()); 111} 112} 113} finally { _lock.ExitWriteLock(); } 114return val; 115} finally { 116slim.ExitWriteLock(); 117_slimLock.EnterWriteLock(); 118try { 119slim.UseCount--; 120if (slim.UseCount == 0) { 121_lockDict.Remove(key); 122slim.Dispose(); 123} 124} finally { _slimLock.ExitWriteLock(); } 125} 126} 127/// <summary> 128/// 清空 129/// </summary> 130public void Clear() 131{ 132_lock.EnterWriteLock(); 133try { 134_map.Clear(); 135_queue.Clear(); 136_slimLock.EnterWriteLock(); 137try { 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,實現垃圾回收,
程式碼使用 :
1private readonly static AntiDupCache<int, int> antiDupCache = new AntiDupCache<int, int>(50, 1); 2 3antiDupCache.Execute(key, () => { 4 5.... 6 7return 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)“暫留”。 這意味著整個程式中任何給定字串都只有一個例項,就是這同一個物件表示了所有執行的應用程式域的所有執行緒中的該文字。因此,只要在應用程式程序中的任何位置處具有相同內容的字串上放置了鎖,就將鎖定應用程式中該字串的所有例項。