防重复请求处理的实践与总结

##背景
在业务开发中,我们常会面对防止重复请求的问题。当服务端对于请求的响应涉及数据的修改,或状态的变更时,可能会造成极大的危害。重复请求的后果在交易系统、售后维权,以及支付系统中尤其严重。

前台操作的抖动,快速操作,网络通信或者后端响应慢,都会增加后端重复处理的概率。

前台操作去抖动和防快速操作的措施,我们首先会想到在前端做一层控制。当前端触发操作时,或弹出确认界面,或disable入口并倒计时等等,此处不细表。

但前端的限制仅能解决少部分问题,且不够彻底,后端自有的防重复处理措施必不可少,义不容辞。

在接口实现中,我们常要求接口要满足幂等性,来保证多次重复请求时只有一次有效。

查询类的接口几乎总是幂等的,但在包含诸如数据插入,多模块数据更新时,达到幂等性会比较难,尤其是高并发时的幂等性要求。比如第三方支付前台回调和后台回调,第三方支付批量回调,慢性能业务逻辑(如用户提交退款申请,商家同意退货/退款等)或慢网络环境时,是重复处理的高发场景。

##尝试

这里针对“用户提交退款申请”的例子,说明一下尝试过的防重复处理方法的效果。

后端防重复处理的方式,我们先后尝试了三种:

####1)基于DB中退款订单状态的验证

这种方式简单直观,从DB查询出来的退款详情(包括状态)往往还可以用在后续逻辑中,没有花额外的工作专门应对重复请求的问题。

这种查询状态后进行验证的逻辑,从代码上线后就一直存在于所有含状态的业务逻辑处理中,必不可少。但对于防重复处理效果并不好:在前端添加防重复提交前,每周平均在25笔;前端优化后,每周降到7笔。这个数量占总退款申请数的3%%,一个仍然无法接受的比例。

理论上,任意次请求只要在数据状态更新之前都完成了查询操作,则业务逻辑的重复处理就会发生。如下图所示。优化的方向是减少查询到更新之间业务处理时间,可降低空档期的并发影响。极致情况下如果查询和更新变成了原子操作,则就不存在我们当前的问题。



####2)基于缓存数据状态的验证
Redis存储查询轻量快速。在request进来的时候,可以先记录在缓存中。后续进来的request每次进行验证。整个流程处理完成,清除缓存。以退款为例子:

    I.  每次退款发起申请,读取缓存中是否有以orderId为key的值
    II. 没有,则往缓存中写入以orderId为key的value
    III.有,则说明有该订单的退款正在进行。
    IV. 操作完清缓存,或者缓存存值的时候设置生命周期

与1)的发放相比,数据库换成响应更快的缓存。但是仍然不是原子操作。插入和读取缓存还是有时间间隔。在极致的情况下还是存在重复操作的情况。
此方法优化后,每周1笔重复操作。



####3)利用唯一索引机制的验证

需要原子性操作,想到了数据库的唯一索引。
新建一个TradeLock表:

CREATE TABLE `TradeLock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`type` int(11) NOT NULL COMMENT ‘锁类型‘,
`lockId` int(11) NOT NULL DEFAULT ‘0‘ COMMENT ‘业务ID‘,
`status` int(11) NOT NULL DEFAULT ‘0‘ COMMENT ‘锁状态‘,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT=‘Trade锁机制‘;

每次request进来则往表里面插入数据:

——成功,则可以继续操作(相当于获取锁);
——失败,则说明有操作在进行。

操作完成后,删除此条记录。(相当于释放锁)
目前已经上线,等待下周的数据统计。



####4)基于缓存的计数器验证:

由于数据库的操作比较消耗性能,了解到redis的计数器也是原子性操作。果断采用计数器。既可以提高性能,还不用存储,而且能提升qps的峰值。

还是以订单退款为例子:

每次request进来则新建一个以orderId为key的计数器,然后+1。

如果>1(不能获得锁): 说明有操作在进行,删除。

如果=1(获得锁): 可以操作。

操作结束(删除锁):删除这个计数器。
要了解计数器,可以参考:

link



##总结:

PHP语言自身没有提供进程互斥和锁定机制。因此才有了我们上面的尝试。

网上也有文件锁机制,但是考虑到我们的分布式部署,建议还是用缓存。

在大并发的情况下,程序各种情况的发生。特别是涉及到金额操作,不能有一分一毫的差距。所以在大并发要互斥的情况下可以考虑3、4两种方案。

时间: 2024-10-18 22:22:58

防重复请求处理的实践与总结的相关文章

[转]Android ListView最佳处理方式,ListView拖动防重复数据显示,单击响应子控件

Android ListView最佳处理方式,ListView拖动防重复数据显示,单击响应子控件. 1.为了防止拖动ListView时,在列表末尾重复数据显示.需要加入 HashMap<Integer,View> lmap = new HashMap<Integer,View>();其中Integer为列表位置,View为子项视图,加入数据前首先if (lmap.get(position)==null) ,满足条件时,加入lmap.put(position, convertView

防止跨站请求伪造(CSRF)攻击 和 防重复提交 的方法的实现

CSRF的概率可以参考:http://netsecurity.51cto.com/art/200812/102951.htm 本文介绍的是基于spring拦截器的Spring MVC实现 首先配置拦截器: <mvc:interceptors> <mvc:interceptor> <!-- 匹配的是url路径, 如果不配置或/**,将拦截所有的Controller --> <mvc:mapping path="/xxx/**" /> <

(九)Struts2 防重复提交

所有的学习我们必须先搭建好Struts2的环境(1.导入对应的jar包,2.web.xml,3.struts.xml) 第一节:重复提交示例演示 第二节:使用<s:token/>标签防重复提交 <s:token></s:token> :加在form 里: 使用token 拦截器: <interceptor-ref name="token"></interceptor-ref> <interceptor-ref name=

struts2学习(15)struts2防重复提交

一.重复提交的例子: 模拟一种情况,存在延时啊,系统比较繁忙啊啥的. 模拟延迟5s钟,用户点了一次提交,又点了一次提交,例子中模拟这种情况: 这样会造成重复提交: com.cy.action.StudentAction.java: package com.cy.action; import java.io.File; import org.apache.commons.io.FileUtils; import com.cy.model.Student; import com.opensympho

防重复提交

// 防重复提交 定义全局变量 var checkingCorrespondflg = false; =============== 点击提交判断 if (!checkingCorrespondflg) { checkingCorrespondflg = true; } else { return false; } =============== 出错之后设置 checkingCorrespondflg = false;

springmvc的token防重复提交

一:首要创立一个号码大全token处置类  ,这儿的类名叫关键词挖掘工具  TokenHandler private static Logger logger = Logger.getLogger(TokenHandler.class); static Map springmvc_token http://www.3h5.cn = null; //生成一个仅有值的token @SuppressWarnings("unchecked") public synchronized stati

防重复功能的实现

1. 定义窗体1界面,如下图 2. 变量定义如下 3. 定义一个赋值事件,用来把条码清空 4. 定义一个文件,用来存放数据 5. 定义一个查找事件,用来实现防重复功能 6. 其中,用到了一个警告事件 7. 再定义一个保存事件,用来把数据保存到文件 8. 查找事件和保存事件关联到[字段1]上 10. 再定义一个笔数事件,用来获取文件的笔数 11. 把笔数事件和清空事件关联到窗体1的进入事件上. 这样,一个完整的防重复功能的程序就完成了. 下面是完成的程序 防重复.zgx

vue防重复点击(指令实现)

快速点击按钮会重复多次调用接口,防止出现这样的情况 全局定义,方便调用 新建plugins.js export default { install (Vue) { // 防重复点击(指令实现) Vue.directive('preventReClick', { inserted (el, binding) { el.addEventListener('click', () => { if (!el.disabled) { el.disabled = true setTimeout(() =>

防重复提交实现方案

在WEB系统操作中,往往会出现用户连续重复点击一个按钮导致重复提交,后台程序的同一个接口代码往往上一个请求还没执行完,下一个请求就到达了,而这两个请求又是请求和操作的同一条数据,就会出现业务上的逻辑错误,往往结果不可预料: 要解决重复提交带来的问题的解决方案有多种,不如网上有很多介绍怎么通过前端页面控制来解决重复提交,当然还有其他方式,这里我采用了通过后台程序代码利用redis做分布式锁的方式来防止重复提交,其思路就是在进入一个后端接口执行前先获取一个分布式锁,如果获取成功则上锁,然后执行业务代