前幾天,公司數據庫出現了兩條相同的數據,並且時間相同(毫秒也相同)。排查緣由,發現是網絡波動形成了重複提交。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)「暫留」。 這意味着整個程序中任何給定字符串都只有一個實例,就是這同一個對象表示了全部運行的應用程序域的全部線程中的該文本。所以,只要在應用程序進程中的任何位置處具備相同內容的字符串上放置了鎖,就將鎖定應用程序中該字符串的全部實例。