妥善的处理重试请求

前言

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

解决方案

  我们需要解决的核心问题,是让客户端可以安全的发送重试请求。服务端应该能够正确的区分哪些请求是重试请求,避免重复处理。但如何实现这一点呢?
  经过一些思考,我初步的实现了一个解决方案。

客户端发送请求唯一标识

  对于手机游戏项目,大部分请求是带有用户属性的。首先,我们可以将请求区分的范围,缩小到同一用户的请求中。比如,在我们的项目中,通过传递 token 参数实现对用户身份的认证。
  客户端在发送请求时,多传递一个 flag 参数,这是一个随机数。我们约定,客户端发送的每个新请求,都应该具有不同的 flag 值,而发送的重试请求,则使用失败的原请求的 flag 值。
  服务端通过应答数据缓存和接收到请求的 flag 值,就可以区分是新请求还是重试请求。

1
2
3
4
5
6
7
8
# 新请求
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 是检查请求是否为重试请求的方法。   

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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()  

   

  

作者

Zivn

发布于

2016-02-23

更新于

2020-10-30

许可协议

评论