事务并发以防止读取旧版本的记录

时间:2017-06-07 20:17:07

标签: mysql transactions innodb commit database-concurrency

假设我有一张名为tasks的表格。每项任务都有一个status。我执行处于To Manage状态的任务之一,将其置于In Management状态,并运行创建任务的过程(可能需要几秒钟才能完成)。

在执行结束时,任务可能会返回To ManageCompleted状态,具体取决于是否必须再次运行该过程。

现在假设有几个进程同时运行此活动,以便完成或以其他方式同时处理多个不同的任务。

我想确保两个进程不会同时管理同一个任务。为实现此目的,应在交易中执行上述活动:

$db->beginTransaction(); /* transaction A */

/* Reads one task from the database (SELECT query with LIMIT 1) which is in the `To Manage` status and returns it */
$task = $tasks->getNextTask(); /* operation 1 */

/* Changes the status into the `In Management` status (UPDATE query) */
$task->changeStatusToManage(); /* operation 2 */

$db->commit();

$task->execute(); /* operation 3 */

我正在使用MySql数据库,该表是InnoDB,具有READ COMMITTED隔离级别:https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html

我们说To Manage状态只有一个任务。如果同时执行两个进程(P1和P2)并且transaction A不存在,则可能发生以下情况:

Instant 1: (operation 1) P1 reads the task id 100 in `To Manage` status
Instant 2: (operation 1) P2 reads the task id 100 in `To Manage` status
Instant 3: (operation 2) P1 puts the task id 100 in the `In Management` status
Instant 4: (operation 2) P2 puts the task id 100 in the `In Management` status
Instant 5: (operation 3) P1 performs the task id 100
Instant 6: (operation 3) P2 performs the task id 100

但是,操作1-2-3实际上是在事务中执行的事实,这种情况应该是不可能的。

  • 你能确认实际上是这样吗?
  • 在执行操作1之前是否需要执行显式LOCK来读取任务表,并在操作2完成后释放它?
  • 我还有什么办法可以防止意外结果吗?

DB结构比上述结构复杂得多。当我更改任务状态时,我也会在另一个表上写一个日志。这是由代码(模型类)本身完成的。我有任务表,task_status表和任务上的外键以及task_status_change(这是日志表)。每个txn执行1次读取(获取任务),2次写入(更改状态和写入日志)。所以我需要执行与此类似的操作(伪代码):

BEGIN;
$id = SELECT task_id FROM task WHERE task_status_id = 1 LIMIT 1;
UPDATE task SET task_status_id = 2 WHERE task_id = $id;
INSERT INTO task_status_change SET task_id = $id, task_status_id = 2;
COMMIT;

正如我上面提到的,我正在使用READ COMMITED隔离级别。我试图同时启动两个进程,在同一个任务池上一起运行。

第一个进程选择的任务ID(ID和时间戳):

55 1496925510
274 1496925512
384 1496925512
589 1496925513
648 1496925513
1088 1496925513
1990 1496925513

第二个进程选择的任务ID(ID和时间戳):

55 1496925510
274 1496925512
589 1496925512
648 1496925513
810 1496925513
1088 1496925513
2049 1496925514

谢谢

1 个答案:

答案 0 :(得分:1)

getNextTask应该修改status并获取它在单个事务中修改的任务的ID。一种方式(伪代码):

BEGIN;
$id = SELECT id ... 
        WHERE status = 'idle'
        LIMIT 1 ... FOR UPDATE;
UPDATE ...  SET status = 'management' WHERE id = $id
COMMIT;

根据您的表结构,可以在单个原子UPDATE语句中执行事务。 (你没有提供太多细节。)

为每个状态转换做类似的事情。

这为长期运行的您的代码集提供了事务语义,只使用数据库中的一个status

这听起来像是一个“排队”机制。我有一句口头禅:“不要排队,只要这样做。”这意味着,无论何时执行任务,都可以更容易/更快/更简单地生成工作进程,而不是排队等等。