妥善的处理重试请求

前言

  手机游戏项目中,因为用户在不少时间使用的是移动网络,和服务器链接不稳定在所不免。客户端发送给服务端的请求没接收到应答,也是常常碰到的状况。
  一样是没有接收到应答,是由于服务端未接收到请求,仍是发送应答给客户端失败,客户端很难区分。对客户端来讲,这两种状况几乎没有什么分别。
  这会带来一个问题:客户端在没法接收到应答的时候,是否发送重试请求?
  若是是由于服务端没收到请求形成的无应答,那么发送重试请求并无什么问题。但若是是由于服务端发送应答给客户端失败形成的无应答,那么发送重试请求,会让服务端重复处理已处理过的请求。
  若是只是强化、升级这种请求,重复处理请求也许问题也不是太大。但若是是购买、消费这种请求,重复消费恐怕会引发玩家的重度不适,收到不少吐槽和投诉。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()  
相关文章
相关标签/搜索