您好,登錄后才能下訂單哦!
前幾天,公司數據庫出現了兩條相同的數據,而且時間相同(毫秒也相同)。排查原因,發現是網絡波動造成了重復提交。
由于網絡波動而重復提交的例子也比較多:
網絡上,防重復提交的方法也很多,使用redis鎖,代碼層面使用lock。
但是,我沒有發現一個符合我心意的解決方案。因為網上的解決方案,第一次提交返回成功,第二次提交返回失敗。由于兩次返回信息不一致,一次成功一次失敗,我們不確定客戶端是以哪個返回信息為準,雖然我們希望客戶端以第一次返回成功的信息為準,但客戶端也可能以第二次失敗信息運行,這是一個不確定的結果。
在重復提交后,如果客戶端的接收到的信息都相同,都是成功,那客戶端就可以正常運行,就不會影響用戶體驗。
我想到一個緩存類,來源于PetaPoco。
Cache<TKey, TValue>代碼如下:
public class Cache<TKey, TValue> { private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); private readonly Dictionary<TKey, TValue> _map = new Dictionary<TKey, TValue>(); public int Count { get { return _map.Count; } } public TValue Execute(TKey key, Func<TValue> factory) { // Check cache _lock.EnterReadLock(); TValue val; try { if (_map.TryGetValue(key, out val)) return val; } finally { _lock.ExitReadLock(); } // Cache it _lock.EnterWriteLock(); try { // Check again if (_map.TryGetValue(key, out val)) return val; // Create it val = factory(); // Store it _map.Add(key, val); // Done return val; } finally { _lock.ExitWriteLock(); } } public void Clear() { // Cache it _lock.EnterWriteLock(); try { _map.Clear(); } finally { _lock.ExitWriteLock(); } } }
Cache<TKey, TValue>符合我的要求,第一次運行后,會將值緩存,第二次提交會返回第一次的值。
但是,細細分析Cache<TKey, TValue> 類,可以發現有以下幾個缺點
1、 不會自動清空緩存,適合一些key不多的數據,不適合做為網絡接口。
2、 由于_lock.EnterWriteLock,多線程會變成并單線程,不適合做為網絡接口。
3、 沒有過期緩存判斷。
于是我對Cache<TKey, TValue>進行改造。
AntiDupCache代碼如下:
/// <summary> /// 防重復緩存 /// </summary> /// <typeparam name="TKey"></typeparam> /// <typeparam name="TValue"></typeparam> public class AntiDupCache<TKey, TValue> { private readonly int _maxCount;//緩存最高數量 private readonly long _expireTicks;//超時 Ticks private long _lastTicks;//最后Ticks private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); private readonly ReaderWriterLockSlim _slimLock = new ReaderWriterLockSlim(); private readonly Dictionary<TKey, Tuple<long, TValue>> _map = new Dictionary<TKey, Tuple<long, TValue>>(); private readonly Dictionary<TKey, AntiDupLockSlim> _lockDict = new Dictionary<TKey, AntiDupLockSlim>(); private readonly Queue<TKey> _queue = new Queue<TKey>(); class AntiDupLockSlim : ReaderWriterLockSlim { public int UseCount; } /// <summary> /// 防重復緩存 /// </summary> /// <param name="maxCount">緩存最高數量,0 不緩存,-1 緩存所有</param> /// <param name="expireSecond">超時秒數,0 不緩存,-1 永久緩存 </param> public AntiDupCache(int maxCount = 100, int expireSecond = 1) { if (maxCount < 0) { _maxCount = -1; } else { _maxCount = maxCount; } if (expireSecond < 0) { _expireTicks = -1; } else { _expireTicks = expireSecond * TimeSpan.FromSeconds(1).Ticks; } } /// <summary> /// 個數 /// </summary> public int Count { get { return _map.Count; } } /// <summary> /// 執行 /// </summary> /// <param name="key">值</param> /// <param name="factory">執行方法</param> /// <returns></returns> public TValue Execute(TKey key, Func<TValue> factory) { // 過期時間為0 則不緩存 if (object.Equals(null, key) || _expireTicks == 0L || _maxCount == 0) { return factory(); } Tuple<long, TValue> tuple; long lastTicks; _lock.EnterReadLock(); try { if (_map.TryGetValue(key, out tuple)) { if (_expireTicks == -1) return tuple.Item2; if (tuple.Item1 + _expireTicks > DateTime.Now.Ticks) return tuple.Item2; } lastTicks = _lastTicks; } finally { _lock.ExitReadLock(); } AntiDupLockSlim slim; _slimLock.EnterUpgradeableReadLock(); try { _lock.EnterReadLock(); try { if (_lastTicks != lastTicks) { if (_map.TryGetValue(key, out tuple)) { if (_expireTicks == -1) return tuple.Item2; if (tuple.Item1 + _expireTicks > DateTime.Now.Ticks) return tuple.Item2; } lastTicks = _lastTicks; } } finally { _lock.ExitReadLock(); } _slimLock.EnterWriteLock(); try { if (_lockDict.TryGetValue(key, out slim) == false) { slim = new AntiDupLockSlim(); _lockDict[key] = slim; } slim.UseCount++; } finally { _slimLock.ExitWriteLock(); } } finally { _slimLock.ExitUpgradeableReadLock(); } slim.EnterWriteLock(); try { _lock.EnterReadLock(); try { if (_lastTicks != lastTicks && _map.TryGetValue(key, out tuple)) { if (_expireTicks == -1) return tuple.Item2; if (tuple.Item1 + _expireTicks > DateTime.Now.Ticks) return tuple.Item2; } } finally { _lock.ExitReadLock(); } var val = factory(); _lock.EnterWriteLock(); try { _lastTicks = DateTime.Now.Ticks; _map[key] = Tuple.Create(_lastTicks, val); if (_maxCount > 0) { if (_queue.Contains(key) == false) { _queue.Enqueue(key); if (_queue.Count > _maxCount) _map.Remove(_queue.Dequeue()); } } } finally { _lock.ExitWriteLock(); } return val; } finally { slim.ExitWriteLock(); _slimLock.EnterWriteLock(); try { slim.UseCount--; if (slim.UseCount == 0) { _lockDict.Remove(key); slim.Dispose(); } } finally { _slimLock.ExitWriteLock(); } } } /// <summary> /// 清空 /// </summary> public void Clear() { _lock.EnterWriteLock(); try { _map.Clear(); _queue.Clear(); _slimLock.EnterWriteLock(); try { _lockDict.Clear(); } finally { _slimLock.ExitWriteLock(); } } finally { _lock.ExitWriteLock(); } } }
代碼分析:
使用兩個ReaderWriterLockSlim鎖 + 一個AntiDupLockSlim鎖,實現并發功能。
Dictionary<TKey, Tuple<long, TValue>> _map實現緩存,long類型值記錄時間,實現緩存過期
int _maxCount + Queue<TKey> _queue,_queue 記錄key列隊,當數量大于_maxCount,清除多余緩存。
AntiDupLockSlim繼承ReaderWriterLockSlim,實現垃圾回收,
代碼使用 :
private readonly static AntiDupCache<int, int> antiDupCache = new AntiDupCache<int, int>(50, 1); antiDupCache.Execute(key, () => { .... return val; });
測試性能數據:
----------------------- 開始 從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)“暫留”。 這意味著整個程序中任何給定字符串都只有一個實例,就是這同一個對象表示了所有運行的應用程序域的所有線程中的該文本。因此,只要在應用程序進程中的任何位置處具有相同內容的字符串上放置了鎖,就將鎖定應用程序中該字符串的所有實例。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。