妥善的處理重試請求

前言

  手機遊戲項目中,因爲用戶在不少時間使用的是移動網絡,和服務器鏈接不穩定在所不免。客戶端發送給服務端的請求沒接收到應答,也是常常碰到的狀況。
  一樣是沒有接收到應答,是由於服務端未接收到請求,仍是發送應答給客戶端失敗,客戶端很難區分。對客戶端來講,這兩種狀況幾乎沒有什麼分別。
  這會帶來一個問題:客戶端在沒法接收到應答的時候,是否發送重試請求?
  若是是由於服務端沒收到請求形成的無應答,那麼發送重試請求並無什麼問題。但若是是由於服務端發送應答給客戶端失敗形成的無應答,那麼發送重試請求,會讓服務端重複處理已處理過的請求。
  若是隻是強化、升級這種請求,重複處理請求也許問題也不是太大。但若是是購買、消費這種請求,重複消費恐怕會引發玩家的重度不適,收到不少吐槽和投訴。mysql

解決方案

  咱們須要解決的核心問題,是讓客戶端能夠安全的發送重試請求。服務端應該可以正確的區分哪些請求是重試請求,避免重複處理。但如何實現這一點呢?
  通過一些思考,我初步的實現了一個解決方案。redis

客戶端發送請求惟一標識

  對於手機遊戲項目,大部分請求是帶有用戶屬性的。首先,咱們能夠將請求區分的範圍,縮小到同一用戶的請求中。好比,在咱們的項目中,經過傳遞 token 參數實現對用戶身份的認證。
  客戶端在發送請求時,多傳遞一個 flag 參數,這是一個隨機數。咱們約定,客戶端發送的每一個新請求,都應該具備不一樣的 flag 值,而發送的重試請求,則使用失敗的原請求的 flag 值。
  服務端經過應答數據緩存和接收到請求的 flag 值,就能夠區分是新請求仍是重試請求。sql

# 新請求
curl -v "http://lua.zivn.me/?token=4bcf03eaf46ae3976a5774f06fdc415e&action=equip.buy&equipId=1&flag=0.927991823060438"

# 新請求
curl -v "http://lua.zivn.me/?token=4bcf03eaf46ae3976a5774f06fdc415e&action=equip.sell&sellIds=25&flag=0.14721225947141647"

# 重試請求
curl -v "http://lua.zivn.me/?token=4bcf03eaf46ae3976a5774f06fdc415e&action=equip.sell&sellIds=27&flag=0.14721225947141647"

服務端緩存應答

  服務端將緩存每一個用戶最後一個請求的應答數據,緩存數據的鍵名使用 token 參數構造,存儲請求的動做 action、應答數據 reply 和惟一標識 flag 值,如圖:數組

應答緩存

服務端區分請求類型 

  服務端收到客戶端的請求後,首先使用 token 參數組織鍵名,並從緩存中獲取用戶上一個請求的應答數據。緩存

  1. 若是請求的動做 action 和惟一標識 flag 與緩存數據一致
    斷定爲重試請求,直接將緩存的應答數據 reply 發送給客戶端。安全

  2. 若是請求的動做 action 和惟一標識 flag 與緩存數據不一致
    斷定爲新請求,根據動做 action 將請求數據分發給對應的業務處理邏輯,並將處理結果組織成應答後發送給客戶端。服務器

分析和實例

  經過緩存的應答數據和請求惟一標識,咱們可以區分請求是新請求仍是重試請求,從而肯定對應的處理策略,避免請求被重複處理。網絡

  如下是目前線上項目使用的代碼實例,其中 Response:send 是發送應答的方法,Response:checkRetry 是檢查請求是否爲重試請求的方法。   併發

local xxtea = loadMod("xxtea")
local util = loadMod("core.util")
local exception = loadMod("core.exception")
local request = loadMod("core.request")
local counter = loadMod("core.counter")
local sysConf = loadMod("config.system")
local changeLogger = loadMod("core.changes")
local redis = loadMod("core.driver.redis")
local cacheConf = loadMod("config.cache")
local shmDict = loadMod("core.driver.shm")
local shmConf = loadMod("config.shm")

--- Response模塊
local Response = {
    --- 請求緩存鍵名前綴
    CACHE_KEY_PREFIX = "lastRes",

    --- Response存儲處理器實例
    cacheHelper = nil,
}

--- 生成重試緩存鍵名
--
-- @param number userId 用戶ID
-- @return string 重試緩存鍵名
function Response:getCacheKey(userId)
    return util:getCacheKey(self.CACHE_KEY_PREFIX, userId)
end

--- Response模塊初始化
--
-- @return table Response模塊
function Response:init()
    if sysConf.PRIORITY_USE_SHM then
        self.cacheHelper = shmDict:getInstance(shmConf.DICT_DATA)
    else
        self.cacheHelper = redis:getInstance(cacheConf.INDEX_CACHE)
    end

    return self
end

--- 發送應答
--
-- @param string message 應答數據
-- @param table headers 頭設置
function Response:say(message, headers)
    ngx.status = ngx.HTTP_OK

    for k, v in pairs(headers) do
        ngx.header[k] = v
    end

    ngx.print(message)
    ngx.eof()
end

--- 構造併發送應答數據
--
-- @param table|string message 消息
-- @param boolean noCache 不緩存消息
function Response:send(message, noCache)
    local headers = {
        charset = sysConf.DEFAULT_CHARSET,
        content_type = request:getCoder():getHeader()
    }

    if sysConf.DEBUG_MODE then
        ngx.update_time()

        headers.mysqlQuery = counter:get(counter.COUNTER_MYSQL_QUERY)
        headers.redisCommand = counter:get(counter.COUNTER_REDIS_COMMAND)
        headers.execTime = ngx.now() - request:getTime()
    end

    if sysConf.ENCRYPT_RESPONSE then
        message = xxtea.encrypt(message, sysConf.ENCRYPT_KEY)
    end

    self:say(message, headers)

    if not noCache then
        local action = request:getAction()
        local token = request:getToken(false)
        local flag = request:getRandom()

        if token ~= "" and flag ~= "" then
            local cacheKey = self:getCacheKey(token)
            local cacheData = { action = action, flag = flag, headers = headers, reply = message }

            self.cacheHelper:set(cacheKey, cacheData, sysConf.REQUEST_RETRY_EXPTIME)
        end
    end
end

--- 檢查重試請求,若是存在緩存則返回緩存
--
-- @return boolean
function Response:checkRetry()
    local action = request:getAction()
    local token = request:getToken(false)
    local flag = request:getRandom()

    if token ~= "" and flag ~= "" then
        local cacheKey = self:getCacheKey(token)
        local cacheData = self.cacheHelper:get(cacheKey)

        if cacheData and cacheData.action == action and cacheData.flag == flag then
            self:say(cacheData.reply, cacheData.headers)
            return true
        end
    end

    return false
end

return Response:init()  
相關文章
相關標籤/搜索