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