为什么要做重复请求的过滤呢?不过滤不行吗?
过滤重复请求很难吗?加一个请求ID不就好了吗?
每个技术难点的话题,肯定是由一个产品需求引发的,俗话说:如果没有产品经理,程序员将不需要听诊器,但是会失业!!
产生背景
重复请求能够对系统造成伤害是架构中很难避免的一个设计问题,一般情况下,读请求很少会造成致命性的故障,主要是系统的写请求,很多时候一个重复写的动作,会是我们程序员加班的缘由。比如:用户使用积分兑换物品,重复的请求会造成用户积分的重复扣减,而作为线上系统,如果日志等辅助打的不好的话,排查原因其实需要很多时间。
一般的产品经理设计系统的时候并不会涉及到这类异常情况,但是一旦出现问题,产品经理就会找到程序员骂娘,多么悲哀的故事,人家付出5分精力设计的系统,我们却要花费10分的精力去编码和维护。
重复的业务请求,有的时候对系统造成的影响很大,所以程序员在设计的时候尤其要注意,产生的原因有很多:
- 黑客进行了拦截,人为的重放了请求
- 客户端因为某些原因,用户在很短的时间内重放了请求
- 一些中间件(比如网关)重放了请求
- 未知的其他情况
道理很简单,用一张图表达的会更清爽一些
image
抽象出来是不是很简单?但是落地却并非像这张图一样简单!!
从这张图上一眼就可以看到,整个过程的重点难点在于过滤器这个逻辑设计部分,这部分可以和业务代码融合在一起,有的时候也可以相分离,比如:有的网关可以内嵌脚本(比如:lua),就完全可以做到和业务无关,但是通常情况下,落地的代码却和业务息息相关。
客户端处理
客户端处理重复请求是一种可以有效过滤正常请求的手段,为什么这么说呢?当一个用户正常操作的时候,客户端完全可以利用loading的方式或者其他过滤重复手段来达到目的,比如:当用户点击一个按钮的时候,弹出loading窗口方式用户再次操作。
再比如:客户端可以设置一个类似于布隆过滤的数据结构,配合对应的过滤算法也可以达到过滤重复请求的效果。
不过,客户端的任何解决方案也只是治标不治本,毕竟,客户端在整个系统架构中,是最不可靠的终端。
请求标识
重复请求过滤的关键在于过滤器的逻辑设计,目前最常用,落地最多当属使用请求ID的方式。大体流程如下:
- 客户端发送请求的时候,会生成随机的请求ID,随着业务参数一起传送到服务端
- 服务端会根据传送上来的请求ID做是否重复的判断
服务器的判断逻辑其实有很多落地方案了,比如最常见的利用redis来存储请求ID,以下是伪代码(NetCore):
代码语言:javascript复制public class Para
{
public string ReqId{get ;set ;}
//其他业务参数
}
public bool IsExsit(Para p)
{
//利用redis来判断当前的key是否存在
bool isExsit=redisMethond(p.ReqId);
//如果存在,则说明是重复请求,如果不存在说明不是重复请求,并且添加到redis
if(!isExsit){
AddRedis(p.ReqId);
}
return isExsit;
}
一般网上的文章都到此为止了,这种方案有没有问题呢?答案:有
问题1
正常的客户端重复请求,一般情况下真的会根据我们写的代码过滤掉重复请求,为什么说一般情况呢?那是因为分布式的原因,极限情况下也会导致重复的请求到业务处理端,比如以下情况:
- 请求被路由到了A服务器,A服务器会去请求Redis,判断是否有相同的请求ID存在,如果是第一次请求,Redis会返回不存在
- 同样的时间,客户端或者黑客重放了同样的请求,这个请求被路由到了B服务器,B服务器同样会请求Redis来判断是否存在,这个时候由于A服务器还没回写Redis,所以B服务器得到的结果也是不存在该请求
- 这样就导致了业务端收到了两次同样的请求,会导致业务不可预期的结果
可见,一个小小重复过滤请求,可能还需要分布式锁的出场才可以
问题2
即便请求中加了唯一的请求ID,但是这个ID并没有安全保证,或者说,这个ID是可以篡改的。当黑客拦截到请求,随便改一下请求ID,在重放就搞定你了。所以,加的请求ID,还需要一个安全机制来保证安全,不然这个参数其实意义不大。
业务签名
由于单纯添加请求ID,并不能解决问题,所以我们需要一种保证请求ID的机制,目前来看,普遍的落地方案是根据业务参数生成摘要,也就是所谓的加签操作。加签操作可以有效的防止参数被篡改。如果你做过微信相关的开发,你会发现和微信服务器的交互也是基于加签操作的。而生成的签名可以作为请求ID,以下是伪代码:
代码语言:javascript复制 //客户端生成签名
string sigh=MD5($"参数1=值1&参数2=值2&time=当前时间戳")
以上只是例子,虽然MD5算法有产生重复数据的可能性,但是对于当前这个业务场景来说足够了。细心的同学会发现,参数当中加了一个时间戳的参数,这个是我故意加的,这个时间戳在这个场景下会出现问题,什么问题呢?
时间戳问题
当前的请求场景是要过滤重复的请求,什么样的请求算是重复请求呢?关键是这个定义要明确,我看了很多重复过滤请求的文章,重复请求这个概念其实定义的不好,这个是和具体业务场景相关的。举个栗子:当用户一秒内重复点击某个按钮算是重复请求,那10秒内重复点击呢?用户一秒之内对同一个商品下单算重复请求,那10秒内呢?
这个定义就涉及到了上面所说的时间戳参数的问题,时间戳是否要参与生成签名,要根据具体的业务场景来定义,不过,我还是要建议,请求的参数中带上时间戳,无论它参不参与签名,至于为什么这么做,当时间长了你就知道了
写在最后
过滤重复请求这个需求,并没有像想象中那么容易,并非只要加上一个请求ID就完事了,它涉及到安全以及分布式的问题,在某些场景下(比如:秒杀)还会涉及到性能以及高可用等非功能性问题,所以那些说:只需要一个请求ID就能过滤的同学,请不要再误导别人了,技术是神圣不可侵犯的。
还是那句话:具体的业务影响到具体的代码实现,脱离业务讲架构其实就是耍流氓