后端接口如何实现防重复提交? | php 技术论坛-大发黄金版app下载

在平时工作中,我们经常会碰到一些应用场景,需要我们去做防重复提交或防重复点击处理,比如像投票、抽奖等等。正常情况下,前后端的都要去做防重复点击处理。今天我们则重点介绍一下服务器端如何实现对接口的防重复提交。

比较常见的一种方案是,我们首先对请求生成一个唯一的标识,然后针对该标识去做防重复提交判断。

废话不多说,直接上代码:

// 示例代码,仅供参考
$request_signature = sha1(
    $request->method() .
    '|' . $request->server('server_name') .
    '|' . $request->path() .
    '|' . $request->ip() .
    '|' . $authinfo['user_id']
);
$redis = app('redis')->connection('default');
if ($redis->get($request_signature)) {
    throw new exception('请勿重复提交。');
}
$expire_time = 10; // 10 秒内只能发起一次请求
$redis->set($request_signature, 1, 'ex', $expire_time);
echo "请求成功" . php_eol;

上面这种情况可能是大家比较常见的吧,但是这里面有一个很严重的问题,正常情况下,我们的接口运行良好,也不存在重复提交的问题。但是当遇到并发请求的时候,我们就会发现,这种方法根本起不到防止重复提交的作用。

下面这张截图是我在本地模拟并发请求的测试结果,可以看到,我们在 8 秒内发起了 100 次请求,结果居然有 3 次请求成功:

那如何解决这种情况呢?大家可能还记得我们前面在写互斥锁的时候用到了 redis 的 setnx 命令。下面我们给出改进版的方案:

// 示例代码,仅供参考
$request_signature = sha1(
    $request->method() .
    '|' . $request->server('server_name') .
    '|' . $request->path() .
    '|' . $request->ip() .
    '|' . $authinfo['user_id']
);
$redis = app('redis')->connection('default');
if ($redis->setnx($request_signature, 1) !== 1) {
    throw new exception('请勿重复提交。');
}
$expire_time = 10; // 10 秒内只能发起一次请求
$redis->expire($request_signature, $expire_time);
echo "请求成功" . php_eol;

同样,我们在本地模拟下并发请求测试,结果如下:

可以看到,同样是在 8 秒内发起了 100 次请求,结果只有 1 次请求成功。

写到这里,我们就已经能够实现后端接口的防重复提交了。

我们知道 setnx 之所以能成功做到在高并发下限制接口的重复提交,完全是基于 redis 本身的原子性操作。想要在 redis 中实现原子性操作,还有一种办法,那就是利用 lua 脚本。

下面我再贴出另外一种实现方案:

// 示例代码,仅供参考
$request_signature = sha1(
    $request->method() .
    '|' . $request->server('server_name') .
    '|' . $request->path() .
    '|' . $request->ip() .
    '|' . $authinfo['user_id']
);
$lua_script = <<<lua
local cmd = redis.call
local req_uuid = keys[1]
local expire = argv[1]
if (cmd('exists', req_uuid) == 1) then
    return cmd('incr', req_uuid)
else
    cmd('setex', req_uuid, expire, 1)
    return 1
end
lua;
$expire_time = 10; // 10 秒内只能发起一次请求
$req_count = app('redis')->eval($lua_script, 1, $request_signature, $expire_time);
if ($req_count > 1) {
    throw new exception('请勿重复提交。');
} else {
    echo "请求成功-->{$req_count}" . php_eol;
}

同样,我们拿到并发下进行测试,结果如下:

效果同 setnx 一样,同样在 8 秒内发起了 100 次请求,只有 1 次请求成功。但是,这种方案还有另外一个好处,那就是我不仅能做到接口的防重复提交,还能做到指定时间窗口内接口访问频率的限制。关于接口限流,我们这里就不展开说了,后面我会单独写一篇文章来专门介绍一些常见的接口限流的方案。

好了,到这里关于后端接口防重复提交的办法我们就介绍完了。如果您有什么其他更好的方案,欢迎评论区留言,大家相互学习~

写在最后

如果本文对您有所帮助或者有所启发,请帮忙扫描下方二维码或微信搜索 「自在牛马」 关注一下我的公众号,您的支持是我最大的写作动力。感谢~

拒绝白嫖,转载请注明出处。

本作品采用《cc 协议》,转载必须注明作者和本文链接
讨论数量: 2
/**
 * 锁使用方式:
 *      redisutil::lock('my_lock', 10, 5); // 参数:锁的名称、锁超时时间、获取锁等待时间
 *      var_dump('执行需要锁保护的代码块:' . datetime());
 *      sleep(3); // 模拟程序执行3秒
 *
 *      redisutil::release('my_lock'); // 参数:之前获取锁时使用的锁的名称
 *      var_dump('释放锁:' . datetime());
 */
    // 添加锁
    public static function lock(string $lockkey, int $locktimeout, int $acquiretimeout = 5): void
    {
        $acquirestarttime = microtime(true);
        while (true) {
            $lockacquired = redis()->set($lockkey, context::get('request_id'), ['nx', 'ex' => $locktimeout]);
            // 抢到锁
            if ($lockacquired) {
                break;
            }
            // 等待锁时间
            if ((microtime(true) - $acquirestarttime) > $acquiretimeout) {
                throw new businessexception('程序异常,请稍后再试'); // 获取redis锁失败
            }
            usleep(1000 * 200); // 间隔200ms重试
        }
    }
    // 释放锁
    public static function release(string $lockkey): bool
    {
        /**
         * eval($script, $args = [], $numkeys = 0):方法第三个参数表示第二个参数中key值的数量。本脚本对应参数为1,即第二个参数中只有第一个为keys值
         * keys[1]:传入的key参数,要操作的键名。本脚本对应参数为 $lockkey
         * argv[1]:传入的key在redis对应的键值。本脚本对应参数为 context::get('request_id')
         */
        $script = <<<lua
if redis.call("get",keys[1]) == argv[1] then
    return redis.call("del",keys[1])
else
    return 0
end
lua;
        return (bool)redis()->eval($script, [$lockkey, context::get('request_id')], 1);
    }
1个月前

redis lua 维护都异常复杂, 不宜这样搭桥实现, 不为跳坑而要埋坑. 如果我没猜错的话, 可以结合mysql来实现.

1个月前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
4
粉丝
1
喜欢
2
收藏
10
排名:2003
访问:1021
博客标签
社区赞助商
网站地图