手機遊戲項目中,因爲用戶在不少時間使用的是移動網絡,和服務器鏈接不穩定在所不免。客戶端發送給服務端的請求沒接收到應答,也是常常碰到的狀況。
一樣是沒有接收到應答,是由於服務端未接收到請求,仍是發送應答給客戶端失敗,客戶端很難區分。對客戶端來講,這兩種狀況幾乎沒有什麼分別。
這會帶來一個問題:客戶端在沒法接收到應答的時候,是否發送重試請求?
若是是由於服務端沒收到請求形成的無應答,那麼發送重試請求並無什麼問題。但若是是由於服務端發送應答給客戶端失敗形成的無應答,那麼發送重試請求,會讓服務端重複處理已處理過的請求。
若是隻是強化、升級這種請求,重複處理請求也許問題也不是太大。但若是是購買、消費這種請求,重複消費恐怕會引發玩家的重度不適,收到不少吐槽和投訴。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
參數組織鍵名,並從緩存中獲取用戶上一個請求的應答數據。緩存
若是請求的動做 action
和惟一標識 flag
與緩存數據一致
斷定爲重試請求,直接將緩存的應答數據 reply
發送給客戶端。安全
若是請求的動做 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()