大家好,提这个问题还是因为避免重复提交。
比如,咱们站的发布帖子部分,假设 表中有字段,user_id,created_at,content 那么我们建立唯一规则时 一般情况下会进行 user_id,created_at联合唯一。 但是当某些情况下, 我们在点击发布的时候 如果按钮没有通过js进行限定或客户端禁用js,那么我在一个比较短的时间内(点击按钮到收到服务器响应进行跳转这段时间内)有多次进行点击了。或者在提交的时候 每个1秒点一次提交,一共点了10次。 经过个人测试,是会产生10条 逻辑上重复的数据的。 但是表内存储的内容确实是唯一的。
比如
user_id | created_at | content |
---|---|---|
1 | 100 | 内容 |
1 | 101 | 内容 |
1 | 102 | 内容 |
1 | 103 | 内容 |
1 | 104 | 内容 |
1 | 105 | 内容 |
1 | 106 | 内容 |
1 | 107 | 内容 |
1 | 108 | 内容 |
1 | 109 | 内容 |
这就出现了 逻辑上的重复数据了。
想到不行就联合验证3个字段的内容[['user_id', 'created_at'], 'unique', 'targetAttribute' => ['user_id','created_at', 'content']],
于是问题是:
1.因为content
是varchar并且内容可能会比较多,那么yii在进行 unique 的时候会不会消耗比较大?
2.对于我说的这种情况,各位有什么好的办法吗?
3.在一个应用中 需要防止重复提交的地方应该是比较多的。如果通过php后台方式进行避免重复,难道每个action中都要进行相应代码的编写吗?那不一堆一堆的重复了?
欢迎大家指导,讨论,谢谢。
其实这个问题我之前也有发现,一直想找一个简单的解决方案,刚开始用的 JS 来控制,但是会有一个 bug,点击提交的时候按钮 disabled 掉,但是如果提交的时候表单验证失败,按钮就变成 disabled 状态了,即使你纠正表单,提交按钮也不能恢复,所以我后来就索性去掉了那个 JS,然后就没有然后了……
user_id
和 create_at
联合唯一,如果 create_at
两次提交的不一样,这个索引是不生效的。
只有当两者完全相同时,才会拒绝重复,所以看你 created_at
的最小单位了,如果是分钟或者秒,点的慢点,可能都会重复提交。
还有我觉得不该用数据库去约束, 应该用 session 之类的,全站统一一个类似过滤器的东西,在用户所有创建内容的地方,判断 session 去确定是否符合最小间隔时间。
if (time() - session('comment_on_article_1') < 15) {
return 0;
}
return 1
session 的命名,应该能通过你的内容栏目名字逆向出来,这样就有迹可循了,可以指定类似“每篇文章允许15秒内评论一次” 的规则。
@abrahamgreyson
因为才开始web不久,所以下面的内容可能让你见笑了。还请指导,如果应用有100个需要发布的地方,那么session中的key就有100个,我的问题是,如果有100个用户同时在线,那么session中的存储是不是100*100个?
恩,可以这么说,可是我面对的用户年龄都大一点,以前c/s的时候就会有 不小心手一抖就2下鼠标点下去了。所以必须得避免。
以下是我暂时不成熟的解决办法。如果有好的方式,还请大家多多分享指导。
<?php \common\widgets\JsBlock::begin() ?>
<script>
$('form#<?=$form->id?>').on('beforeValidate', function (e) {
$(':submit').attr('disabled', true).addClass('disabled');
});
$('form#<?=$form->id?>').on('afterValidate', function (e) {
if (cheched = $(this).data('yiiActiveForm').validated == false) {
$(':submit').removeAttr('disabled').removeClass('disabled');
}
});
$('form#<?=$form->id?>').on('beforeSubmit', function (e) {
$(':submit').attr('disabled', true).addClass('disabled');
});
</script>
<?php \common\widgets\JsBlock::end() ?>
public function actionCreate()
{
if (Yii::$app->request->isPost) {
if (yii::$app->session->getFlash('sending_' . yii::$app->user->id, false)) {
//返回发布成功时应该返回的页面,但是也有问题,就是如果出现js判断不了的错误,那因为第二次或n次提交造成,之前的model中的error信息无法显示了。但暂时我还是使用了这个方法.
return $this->redirect(['client/view', 'id' => $model->client_id]);
}
//...
//...
}
yii::$app->session->setFlash('sending_' . yii::$app->user->id);
return $this->render('create', [
'model' => $model,
]);
}
虽然看起来貌似解决了,但是我依然还是有第一点疑问。如果应用有100个需要发布的地方,那么session中的key就有100个,我的问题是,如果有100个用户同时在线,那么session中的存储是不是100*100个?
单独为某个 Session 设置过期时间好像有点麻烦。其实你也可以用 cookies 来实现这个功能,同时还能解决服务器压力。
关于 cookies 的使用,可以查看:
$repeatCheckKey = $this->action->id ;
if($cache->get($repeatCheckKey) !== false ){
// 提示用户提交过快
}
if($model->save()){
// 60 秒内防止重复提交
$cache->set($repeatCheckKey ,time(),60) ;
}
上面这个只是示意做法 如果真的可行 那么可以利用事件机制 无侵入实现
只需要监听ActiveRecord::EVENT_BEFORE_INSERT 以及 ActiveRecord::EVENT_AFTER_INSERT 就可以无侵入实现
还有一种方法 直接跟ar无关 直接监听控制器的beforeAction 跟afterAction 利用这种思路也可以实现
经过思考 觉得还是监听控制器吧
还有一种方案
就是先查询下 提交前查询下 内容是否存在 如果存在提示重复提交
还有就是用MD5 或者哈希下 前一次提交的内容 第二次提交时 对比签名 这样 md5的值可以放在session中 因为资源较小 相比再次查询db能稍好些
关于重复提交:
接 @forecho的回复。
思路就是 哈希POST (注意下 两次提交有没有不一样的参数混进来 比如xxxToken)中的值 提交成功后存一个md5 (位数比较少哈! ) 下次处理提交前对比哈希 如果一样 就是内容重复 。 session key 可以命名为 $lastActionParamsHash , 值:md5(\Yii::$app->request->post()) ; 这样一定程度内可以防止 “手抖”多次点击提交按钮
对于手抖 其实在js端也可以做限制 比如点击按钮后 就灰显(disable)之。
这个方案 跟防止快速提交性质稍微不一样了 但也好像有些关联 :d:
@xjdata 是的,如果有 100 个用户同时在线,那么每个用户维护 100 个, 就是一万个。 session 要设置过期时间,过期后,会被 PHP 的垃圾回收自动处理,详情见:PHP session 现在 PHP session 很少有用默认的文件存储了, 因为会存在文件锁引起的 bug,大多数用内存缓存或数据库缓存。所以关于性能和空间占用完全可以不用考虑,很多时候,开发就是用冗余去换用户体验,在两者中间找平衡。 过早的优化,是 bug 产生的根源。 用 hash 也是一个办法,但是比这个办法更不简单。
@abrahamgreyson #12楼 非常赞成 过早的优化,是 bug 产生的根源。 刚刚接触yii看了很多资料,知道了缓存,于是自己尝试写了一些,可是现在自己联手的项目越来越多的问题,归根结底还是因为自己对这些技术没有经验并且掌握的不够透彻,哎。。。慢慢来。
这个貌似可以用yii的csrftoken来解决吧,将csrftoken的有效次数设置为一次,进入页面后会生成一个csrftoken,提交一次后失效了,后面提交的数据就不能成功了,虽然csrftoken是防止恶意注入的,但是重复插入本身也算是一种注入了
@c376685128 #15楼 不行的,不一样的,csrftoken 不是用来解决这个问题的。
@forecho [#2楼](#comment2) 可以设置延时,200ms 之后,将按钮disabled 属性删除掉.
关于cacheKey的设计 最好加上用户或者浏览器特定的因素在里面 如果不考虑这些因素 所有用户的操作可能就被串行化了(不同的人 浏览器 都在做这个action 动作时 如果key不是用户客户端特定的 他们将被同一个key给锁住)
针对对不同的人 要用不同的缓存键 所以可以用某种前后缀方法 保证键对客户端的唯一性:
php 早期有个 ip2long 方法可以把客户端的ip转换为整数 但后来ipv6出现后 这个方法好像有些失效 网上有人提出了ip2bin方法(可以搜索下)
还有一个方法是 cacheKey
里面添加sessionId 做前缀
$repeatCheckKey = {$sessionId}.$this->action->id