防止从MySQL和PHP发送重复记录

时间:2018-09-20 13:43:37

标签: php mysql yii2

我有一张表格ad_banner_queue,用于根据广告的权重生成队列。广告被插入到广告表中。如果所有在队列中的现有广告都交付给用户,则会生成队列。

现在的问题是,如果请求同时出现并且Rand()返回相同的记录,我应该如何防止发送重复的广告?

下面是代码:

<?php
/* To Get the random Ad */
public function getBanner($params) {
    /* Fetch the Random from table */
    $ads_queue = (new \yii\db\Query())
            ->select('ad_quque_id, banner_image, unique_code')
            ->from('ad_banner_queue')
            ->join('inner join', 'advertisement', 'ad_banner_queue.ad_id = advertisement.ad_id')
            ->where('is_sent=0')
            ->orderBy('RAND()')
            ->one();

    /* In case of queue is not there generate the new queue */
    if ($ads_queue === false) {
        $output = $this->generateAdQueue();
        //In case of something went wrong while generating the queue
        if ($output == false) {
            return array();
        }

        //Now fetch the record again
        $ads_queue = (new \yii\db\Query())
                ->select('ad_quque_id, banner_image, unique_code')
                ->from('ad_banner_queue')
                ->join('inner join', 'advertisement', 'ad_banner_queue.ad_id = advertisement.ad_id')
                ->where('is_sent=0')
                ->orderBy('RAND()')
                ->one();
    }

    /* Now, marked that one as is_sent */
    Yii::$app->db->createCommand()->update('ad_banner_queue', ['is_sent' => 1], 'ad_quque_id =:ad_quque_id', array(':ad_quque_id' => $ads_queue['ad_quque_id']))->execute();
    return $ads_queue;
}

/**
 * Below will Generate the Queue if not exist
 */
public function generateAdQueue() {
    /* First check thatt there is existing queue, if so don't generate it */
    $data_exist = (new \yii\db\Query())
            ->select('ad_quque_id')
            ->from('ad_banner_queue')
            ->where('is_sent=0')
            ->scalar();
    if ($data_exist === false) {
        /* Delete all other entries */
        (new \yii\db\Query())
                ->createCommand()
                ->delete('ad_banner_queue')
                ->execute();

        /* Fetch all banner */
        $ads = (new \yii\db\Query())
                ->select('ad_id, unique_code, ad_name, banner_image,ad_delivery_weightage')
                ->from('advertisement')
                ->where('status_id in (8)') //Means only fetch Approved ads
                ->all();
        if (!empty($ads)) {
            foreach ($ads as $ad) {
                /* Make entry as per that weightage, example, if weightage is 10 then make entry 10 times */
                $ins_fields = array();
                for ($i = 1; $i <= $ad['ad_delivery_weightage']; $i++) {
                    $ins_fields[] = array($ad['ad_id']);
                }
                Yii::$app->db->createCommand()->batchInsert('ad_banner_queue', ['ad_id'], $ins_fields)->execute();
            }
            return true;
        } else {
            return false;
        }
    } else {
        return false;
    }
}

7 个答案:

答案 0 :(得分:5)

我认为您的意思是,同时执行请求的不同“人员”不应获得相同的随机行?为了避免在两个正在运行的请求中两次选择同一条记录的微小机会,最可靠的方法(无需测试)可能是锁定表并在事务中执行读取和更新。您必须使用支持此功能的存储引擎,例如InnoDB。

使用事务表(例如InnoDB表)完成LOCK TABLESUNLOCK TABLES的方法是从SET autocommit = 0开始事务,不是 {{1 }},然后是START TRANSACTION。然后,除非明确提交事务,否则不应调用LOCK TABLES

例如,如果您需要一次性读写表,则可以执行以下操作:

UNLOCK TABLES

我们使用别名锁定的原因是您cannot refer to a locked table multiple times in a single query using the same name。因此,我们改用别名,并为表和每个别名获得一个单独的锁。

答案 1 :(得分:4)

尽管这似乎根本不是一个小问题,但有几种处理方法,每种方法都有其缺点,主要是您可以从三个不同的角度来面对这个问题:

与它同住

在现实生活中,您反复受到拉扯的机会很低,您需要真正考虑一下,如果您愿意面对额外的工作,只是要确保广告不会连续显示两次,那么您还需要考虑缓存存在,您可能会中断大脑,使广告变得原子化,只是为了找出浏览器/代理/缓存是否在重复投放广告:(

在数据库上处理它

您可以处理此问题,使数据库有责任保持数据安全和一致(实际上,这是数据库的主要任务),可以通过以下几种方法来实现:

  • 锁和表(如先前所建议),我个人类似于在PHP和MySQL中使用锁的方法,虽然会遭受性能损失和死锁的风险,但是无论如何,这仍然是一个解决方案,在队列表上进行一次更新选择,以确保在更新之前不会有人再次读取。这里的问题是,完成此操作后,您将锁定整个表,并且需要小心使用DB Driver和自动提交。
  • 游标游标是一种基本为您愿意承担的职责而创建的数据库结构,您可以创建游标并使用its functions安全地对其进行遍历。由于事务的原因,在PHP中使用游标可能会非常棘手,您需要非常清楚如何避免错误。
  • 游标和存储过程将其处理到数据库中的最佳方法是管理数据库本身内部的游标,这就是为什么存在存储过程的原因,只需创建过程以从游标中拉出一个新项目并全部消耗完后,再次填充。

在PHP端处理它

在这种情况下,您需要在PHP上实现自己的队列,并且可能有几种方法可以实现,但是主要的问题可能是在您的应用程序上实现多进程安全的原子操作,我个人不喜欢使用任何类型的锁无法100%确定应用程序的执行流程,否则您可能最终将其全部锁定。无论如何,这里有三种机会:

  • 使用同时包含在php或第三方中的sems或互斥锁,超时和锁定可能会变成地狱,而且不易检测,因此如上所述,我会避免使用它。 / p>

  • Use PHP MSG Queue ,我认为这是最安全的方法,只要您在* nix系统上运行应用程序,只需将所有可用广告发送到消息队列即可,而不是在数据库上创建表之后,一旦所有广告都被消耗掉,您可以再次重新生成队列,此系统的缺点是您的服务器无法分发,如果在重新启动前不保存它,则可能会丢失当前队列状态。

  • 第三方队列系统,具体取决于您的应用程序工作负载或交互,您可能需要使用队列管理系统,如果您要使用分布式系统,这是必须的,这听起来可能太严重了使用msg队列系统来解决此问题,但这种方法可能会挽救生命。

摘要

如果您不能忍受它并且对我将要用于存储过程和游标的数据库足够熟练,则不需要并发决定,只要您使用数据库就可以处理它。符合ACID的数据库(不是MyISAM)

如果您想避免进入数据库并且您的系统是* nix并且不会被分发,则可以尝试使用msg_queues

如果您认为您的系统有时可能是分布式的,或者不依赖于旧的SysV机制,则可以尝试使用RabbitMQ之类的消息代理,这很有趣,一旦您开始使用它们,就会开始发现它们的新用途每天。

答案 2 :(得分:3)

您可以使用mutex组件来确保只有一个尝试从队列中弹出广告的过程。

config.xml

但是请注意,这可能会大大降低性能,特别是如果您想同时处理多个请求。您可能会考虑针对此类队列使用不同的技术,而关系数据库并不真正适合此类任务。使用基于Redis的队列和SPOP可能是更好的选择。

答案 3 :(得分:3)

大概是在不同的页面上展示广告。 HTML是“无状态的”,因此您不能期望一页知道以前显示的广告。因此,您必须逐页传递此信息,或将其存储在与单个用户相关联的数据库中的某个位置。

您还想要一些随机化吗?让我们同时做这两件事。

什么是“状态”?存在“初始状态”,此时您会随机选择要显示的第一个广告。然后,您将该信息传递到下一页(URL,Cookie或数据库中)。

另一个“状态”查看上一个状态并计算下一个要显示的广告。 (最终,您需要担心广告用完了-您会重新开始吗?是否会重新随机化?等等。)

但是如何避免连续显示相同的“随机”广告两次?

  • 您有N个广告-SELECT COUNT(*) ...
  • 您选择添加数字J作为第一个要显示的广告-RAND()的简单应用,无论是在SQL中还是在应用中。
  • 选择一个数字M,以使M和N为“相对质数”。
  • “下一个”广告是数字(J:=(J + M)mod N)。这将循环浏览所有广告,而不会重复显示所有广告。 -同样,这可以在SQL或ap中完成。
  • 将J和M从一页传递到下一页。

要获取第J行:要么对行进行唯一编号连续编号;或使用ORDER BY ... LIMIT 1 OFFSET J。 (注意:将J填充到SQL中可能很棘手。)

没有表锁,没有互斥锁,只是将信息从一页传递到下一页。

答案 4 :(得分:1)

您应该创建一个单独的数据库表,并标记用户在其帮助下收到的广告。在向用户发送广告之前,请检查他是否已经收到广告。

答案 5 :(得分:1)

您可以将事务和SELECT FOR UPDATE构造用于锁定数据和一致地执行查询。例如:

public function getAds()
{
    $db = Yii::$app->db;
    $transaction = $db->beginTransaction(Transaction::REPEATABLE_READ);
    try {
        $ads_queue = (new \yii\db\Query())
            ->select('ad_quque_id, banner_image, unique_code')
            ->from('ad_banner_queue')
            ->join('inner join', 'advertisement', 'ad_banner_queue.ad_id = advertisement.ad_id')
            ->where(new Expression('is_sent=0 FOR UPDATE'))
            ->orderBy('RAND()')
            ->one();
        if ($ads_queue === false) {
            $transaction->commit();
            return null;
        }
        $db->createCommand()->update('ad_banner_queue', ['is_sent' => 1], 'ad_quque_id =:ad_quque_id', array(':ad_quque_id' => $ads_queue['ad_quque_id']))->execute();      
        $transaction->commit();
        return $ads_queue;
    } catch(Exception $e) {
        $transaction->rollBack();
        throw $e;
    }
}

public function getBanner($params)
{
    $ads_queue = $this->getAds();
    if (is_null($ads_queue)) {
        $output = $this->generateAdQueue();        
        if ($output == false) {
            return array();
        }
        $ads_queue = $this->getAds();
    }
    return $ads_queue;
}

答案 6 :(得分:0)

使索引唯一,或进行检查以检查数据,看看是否重复。

希望这会有所帮助。祝你好运