1. 程式人生 > >針對多個Redis key使用事務方式同步修改時引發的問題

針對多個Redis key使用事務方式同步修改時引發的問題

  前幾天投產碰到一個問題:多個Redis鍵值在一個事務內一併提交發生異常的問題。

1.問題描述

為實現令牌桶(Token Bucket)流控演算法,引入了兩個Redis鍵值由於這兩個鍵值需要同時完成修改,因此引入Redis事務提交方式。

兩個Redis key定義如下:

remainTokenKey:剩餘令牌個數key,命名方式為:

string remainTokenKey = “AppIdTokenBucketAvailable”

oldTsKey:上次訪問時間戳key,命名方式為:

string oldTsKey = “AppIdTokenBucketTs”

程式碼如下:

        private ErrorCode ExecuteTokenBucketProc(string appId)
        {
            if (string.IsNullOrEmpty(appId))
            {
                return ErrorCode.WebAppIdIsEmpty;
            }
 
            bool bPass = true;
            string remainTokenKey = RedisKeyConstValues.REDIS_KEY_TOKEN_BUCKET_AVAILABLE_PREFIX + appId;
            string oldTsKey = RedisKeyConstValues.REDIS_KEY_TOKEN_BUCKET_TS_PREFIX + appId;
            var db = RedisConnector.GetInstance().GetDb();
            decimal availableTokens = 0;
 
            try
            {
                //引入Transaction
                ITransaction trans = db.CreateTransaction();
                DateTime currentTime = DateTime.Now;
 
                RedisValue remainTokens = db.StringGet(remainTokenKey);
                RedisValue lastSendTime = db.StringGet(oldTsKey);
                trans.AddCondition(StackExchange.Redis.Condition.StringEqual(remainTokenKey, remainTokens));
                trans.AddCondition(StackExchange.Redis.Condition.StringEqual(oldTsKey, lastSendTime));
 
                //根據演算法獲取可用token個數
                availableTokens = GetAvailableTokens(remainTokens, lastSendTime, currentTime);
 
                //判斷剩餘可用令牌是否足夠
                if (availableTokens >= 1)
                {
                    availableTokens -= 1;
                    bPass = true;
                }
                else
                {
                    bPass = false;
                }
 
                //儲存可用token數和歷史訪問時間戳到Redis中
                UpdateAvailableAndLastSendTimeKey(trans, remainTokenKey, oldTsKey, availableTokens);
 
                //事務提交
                bool bRet = trans.Execute();
                if (!bRet)
                {
                    LogHelper.Error(ErrorCode.WebUpdateFlowControlTokenBucketKeyFail, "流控: Redis事務執行失敗, appId = " + appId);
                    return ErrorCode.WebUpdateFlowControlTokenBucketKeyFail;
                }
            }
            catch (RedisException ex)
            {
                LogHelper.Fatal(ErrorCode.WebGetRedisBucketTokenFail, "流控獲取Bucket Token失敗", ex);
                return ErrorCode.Success;
            }
            catch (Exception ex)
            {
                LogHelper.Fatal(ErrorCode.WebFlowControlException, "流控流程發生內部失敗", ex);
                return ErrorCode.Success;
            }
 
            if (!bPass)
            {
                LogHelper.Error(ErrorCode.WebFlowControlReject, "流控生效,拒絕本次請求。");
                return ErrorCode.WebFlowControlReject;
            }
            return ErrorCode.Success;
        }

        當執行 trans.Execute()進行事務提交時,直接引發exception異常日誌如下:

"Multi-key operations must involve a single slot; keys can use 'hash tags' to help this, i.e. '{/users/12345}/account' and '{/users/12345}/contacts' will always be in the same slot"

2.問題分析

   現場Redis部署為叢集部署方式,3主3備,組成一個cluster,是不是由於叢集部署導致?Slot又是什麼概念?

2.1 Redis Slot概念

   在redis叢集內部,採用slot槽位的邏輯管理方式, 叢集內部共有16384(2的14次方)個Slot,叢集內每個Redis Instance負責其中一部分的Slot的讀寫。一個Key到底屬於哪個Slot,由分片演算法:

crc16(key) % 16384

決定。也正是通過此分片演算法,將不同的key以相對均勻的方式分配到不同的slot上。

2.2 多個鍵值需要落在一個slot上的問題如何破?

   當執行多鍵值事務操作時,Redis不僅要求這些鍵值需要落在同一個Redis例項上,還要求落在同一個slot上。如何實現?

          解決方法還是從分片技術的原理上找。

   為了實現將key分到相同槽位,就需要相同的hash值,即相同的key。key相同是不現實的,因為每個key都有不同的用途。例如user:user1:ids儲存使用者的ID,user:user1:contacts儲存聯絡人的具體內容,兩個key不可能同名。

仔細觀察user:user1:ids和user:user1:contacts,兩個key其實有相同的地方,即user1。能不能拿這一部分去計算hash呢?

   這就是 Hash Tag 。允許用key的部分子串來計算hash。

   下文是redis-cloud-cluster網站針對Hash Tag的一段說明(詳細內容請參考:https://redislabs.com/kb/redis-cloud-cluster/)

1. Keys with a hash tag: a key’s hash tag is any substring between ‘{‘ and ‘}’ in the key’s name. That means that when a key’s name includes the pattern ‘{…}’, the hash tag is used as input for the hashing function. For example, the following key names have the same hash tag and would therefore be mapped to the same slot: foo{bar}, {bar}baz & foo{bar}baz.

2. Keys without a hash tag: when a key doesn’t contain the ‘{…}’ pattern, the entire key’s name is used for hashing.

           大致意思:當一個key包含 “{ }” 的時候,就不對整個key做hash,而僅對 “{}” 包括的子串計算hash。假設hash演算法為sha1。對ufoo{bar}, {bar}bazfoo{bar}baz這三個字串計算hash值,等同於計算sha1(bar)。

        OK,搞清楚具體原委後,故障修復也就是小case了。

3.解決方案

        將剩餘令牌個數鍵remainTokenKey和上次訪問時間戳鍵oldTsKey,統一採用如下字首進行命名:{/AppId}/

即:

    string remainTokenKey = “{/AppId}/AppIdTokenBucketAvailable”
    string oldTsKey = “{/AppId}/AppIdTokenBucketTs”

 這樣便可確保這兩個key落在同一個slot內部。

4.延伸

   通過Hash Tag可以解決多鍵值落在一個slot上的問題,但也不要一味的使用Hash Tag,將本不需要放在一個slot上的鍵值對人為的加上Hash tag而導致分片在同一個slot上,這樣會導致redis叢集鍵值在各個slot上分配不均衡。

還是引用redis-cloud-cluster網站的原文加以說明吧:

You can use the ‘{…}’ pattern to direct related keys to the same hash slot, so that multi-key operations are supported on them. On the other hand, not using a hash tag in the key’s name results in a (statistically) even distribution of keys across the keyspace’s shards. If your application does not perform multi-key operations, you don’t need to construct key names with hash tags.