您所在的位置:首页 / 知识分享

如何防止接口重复请求

2020.03.05

1807

wng_png

问题
最近遇到一个接口重复请求导致数据异常的bug,场景是这样的: 在一个通知模块,老师发送通知给家长,家长点开通知后客户端需要调用一个ack接口告诉服务器该家长已经读取该通知,并在notify_read表中插入该家长的信息,内容大概是(notify_id, member_id),并把通知表notify的read_num字段+1。当遇到网络问题时,客户端会多次请求该接口。
int count = queryRead(notifyId, memberId);
if( count == 0){
    insertNotify(notifyId, memberId);
    incrementNotifyReadNum(notify, memberId);
}

当出现并发请求时,可能一个请求执行到insertNotify时,另外一个请求也刚好执行到insertNotify。导致incrementNotifyReadNum方法被执行两次,数据就出错了。
解决方案
由于我们的服务是分布式的,所以在解决问题的时候需要考虑两个请求落在不同的两台服务器的情况。 当时想到存在两种方案解决这个问题。
使用数据库
新建一张中间表
CREATE TABLE request(rkey varchar(32)PRIMARY KEY (`rkey`))
复制代码
每次请求在进行业务处理前都往该表插入一条记录,请求完成后删除该记录。
try{
    insertRequest(String.format("NOTIFY_%s_%s", notify, memberId))
 
    int count = queryRead(notifyId, memberId);
    if( count == 0){
        insertNotify(notifyId, memberId);
        incrementNotifyReadNum(notify, memberId);
    }
}catch(Exception e){
    //doSomething();
}finally{
 deleteRequest(String.format("NOTIFY_%s_%s", notify, memberId));
}
 
通过使用数据库中的唯一键保证在同一时刻只有一个请求可以正常插入request表,插入失败的请求将不做业务处理。方案很简单,但如果请求量比较大的话会损耗DB的性能且不容易扩展。
使用Redis的counter
使用Redis跟使用数据库的实现流程其实是差不多。每次请求时,对key进行setnx操作,setnx操作返回1时,表明只有当前请求在处理这项业务;当setnx操作返回等于0时,表明还有其他请求在处理这项业务。
**SETNX key value **
将 key 的值设为 value ,当且仅当 key 不存在。 若给定的 key 已经存在,则 SETNX 不做任何动作。 SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。 时间复杂度: O(1) 返回值: 设置成功,返回 1 。 设置失败,返回 0 。
String key = String.format("NOTIFY_%s_%s", notify, memberId);
try{
    int requestCount =  redisServer.setnx(key);
    
    if(requestCount == 1){
        int count = queryRead(notifyId, memberId);
        if( count == 0){
            insertNotify(notifyId, memberId);
            incrementNotifyReadNum(notify, memberId);
        }
    }
    
}catch(Exception e){
    //doSomething();
}finally{
    //记得释放
    redisServer.delete(key);
}
Redis的setnx命令常用来实现分布式锁,这种做法就是利用了这一特性来实现防止接口重复请求的。相对于数据库的实现,Redis的实现性能更好且更容易扩展。