天天看點

淺談C#在網絡波動時防重複送出

轉自部落格:https://www.cnblogs.com/toolgood/p/10700828.html

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

    由于網絡波動而重複送出的例子也比較多:

淺談C#在網絡波動時防重複送出
淺談C#在網絡波動時防重複送出

    網絡上,防重複送出的方法也很多,使用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 -----------------------

      普通并發: 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 -----------------------

      普通并發: 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 -----------------------

      普通并發: 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)“暫留”。 這意味着整個程式中任何給定字元串都隻有一個執行個體,就是這同一個對象表示了所有運作的應用程式域的所有線程中的該文本。是以,隻要在應用程式程序中的任何位置處具有相同内容的字元串上放置了鎖,就将鎖定應用程式中該字元串的所有執行個體。