3

我有一个简单的表格,它是一个电子邮件队列。

CREATE TABLE `emails_queue_batch` (
  `eq_log_id` int(11) NOT NULL DEFAULT '0',
  `eq_to` varchar(120) CHARACTER SET utf8 DEFAULT NULL,
  `eq_bcc` varchar(80) CHARACTER SET utf8 DEFAULT '',
  `eq_from` varchar(80) CHARACTER SET utf8 DEFAULT NULL,
  `eq_title` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL,
  `eq_headers` varchar(80) CHARACTER SET utf8 DEFAULT NULL,
  `eq_content` longtext CHARACTER SET utf8,
  `eq_sid` int(11) DEFAULT '0',
  `eq_type` int(11) DEFAULT '0' COMMENT 'email type',
  `eq_esp` int(11) DEFAULT '0',
  PRIMARY KEY (`eq_log_id`),
  KEY `email` (`eq_to`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 

多个线程一次重复读取 50 行并删除这些行。

为了避免重复阅读我使用的同一行:

    $db->query(" LOCK TABLE $table WRITE ");

    $query= "SELECT * FROM $table LIMIT  ".CHUNK_SIZE. " " ; 

    $emails2send=$db->get_results ($query);

    if (!empty ($emails2send)){

        // DELETE EMAIL 
        $eq_log_ids = array();
        foreach ($emails2send as $email) $eq_log_ids[]= $email->eq_log_id ;

        $query= "DELETE FROM $table WHERE eq_log_id IN ( ".implode(',', $eq_log_ids)." ) ";
        $db->query ($query);

        $db->query (" UNLOCK TABLES "); // unlock the table so other sessions can read next rows
        ........ code processing the read rows here .............
    } else { // if !empty emails2send
        // $emails2send is empty 
        $db->query (" UNLOCK TABLES; ");    
        $stop_running=true; // stop running
    }

另一个线程同时写入表。出于某种原因,我不明白这个配置在读取和写入时都被锁定的表死锁了。

我的问题是:这是锁定正确的解决方案,以确保我读取每一行一次且仅一次(并删除它)。

还是将其作为交易更好地处理,如果是,是哪种?我没有交易经验。

4

2 回答 2

1

计划A:

这假设您可以在不到 2 秒的时间内处理 N 行。你有 N=50——这可能太大了。

BEGIN;
SELECT ... LIMIT 50  FOR UPDATE;
... process ...
... gather a list of ids to delete ...
DELETE ... WHERE id IN (...)
COMMIT;

抓得越多,走得越快,但也越容易陷入僵局。当发生死锁时,只需重新启动事务。还要跟踪死锁发生的频率,以调整“50”。

B计划:

当一个项目的处理对于一个事务来说“太长”时,这很有用。我说 2 秒可能“太长”了。

Grab a row to process:
with autocommit=ON ...
UPDATE ... SET who_is_processing = $me,
               when_grabbed = NOW()
               id = LAST_INSERT_ID(id),
           WHERE when_grabbed IS NULL
             AND any-other-criteria
           LIMIT 1;
$id = SELECT LAST_INSERT_ID();

... process $id ...  (This may or may not involve transactions)

Release the row (or, in your case, delete it):
again, autocommit=ON suffices...
DELETE ... WHERE id = $id;

“从不”在 InnoDB 中使用表锁。(可能有用例,但这不是一个。)

于 2017-11-26T16:14:18.210 回答
1

使用事务可能会更好,尤其是在死锁的情况下。

首先,尝试将批量大小从 50 减少到 1,看看情况是否有所改善。他们可能。这很容易。如果您使用交易,这就是您想要做的。

其次,尝试这种查询序列。

  START TRANSACTION;
  SELECT @id := table.eq_log_id, table.* FROM table LIMIT 1 FOR UPDATE;
  /* handle the item here */
  DELETE FROM table WHERE eq_log_id = @id;
  COMMIT;

这仅适用eq_log_id于唯一(或主)键。在您的 php 程序中循环运行它,直到该SELECT操作不返回任何行。然后睡一会,再试一次。

更好的方法是processed向您的表添加一个调用的 TIMESTAMP,默认值为空。然后,您可以更新它们的时间戳,而不是删除行。这将为您提供一种解决问题的方法。

START TRANSACTION;
SELECT @id:=eq_log_id, * FROM table WHERE processed IS NULL LIMIT 1 FOR UPDATE;
/* handle the item here */
UPDATE table SET processed=NOW() WHERE eq_log_id = @id;
COMMIT;

您可以运行一夜之间的批处理来清除所有像这样的陈旧记录

DELETE FROM table WHERE processed < CURDATE() - INTERVAL 1 DAY;

我建议这样做是因为在生产中查看已发送消息的时间历史非常有帮助。

于 2017-11-26T15:16:24.293 回答