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

标签 mysql transactions innodb commit database-concurrency

假设我有一个名为tasks 的表。每个任务都有一个状态。我将处于“待管理”状态的任务之一置于“管理中”状态,然后运行为其创建任务的过程(这可能需要几秒钟即可完成)。

执行结束时,任务可能会返回到待管理已完成状态,具体取决于该过程是否必须再次运行。

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

我想确保两个进程不会同时管理同一任务。为了实现这一点,上述事件应该在事务中执行:

$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

我们说只有一项任务处于待管理状态。如果两个进程(P1 和 P2)同时执行并且事务 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 完成后释放它?
  • 我还应该采取什么措施来防止出现意外结果?

数据库结构比上面描述的要复杂得多。当我更改任务状态时,我也会在另一个表上写入日志。这是由代码(模型类)本身完成的。我有任务表、带有任务外键的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

谢谢

最佳答案

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

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

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

对每个状态转换执行类似的操作。

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

这听起来像是一种“排队”机制。我有一句口头禅:“不要排队,只管去做”。这意味着每当您有任务要做时,生成一个工作进程可能会更容易/更快/更简单,而不是对其进行排队等。

关于mysql - 事务并发性以防止读取旧版本的记录,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44422043/

相关文章:

oracle - Oracle和PostgreSQL中Write Skew异常不回滚事务

java - 在单个 Hibernate session 中使用多个事务

c# - EF数据库事务可以循环使用吗?

mysql - 优化卡在 "Creating sort index"上的简单 MySQL GROUP BY 查询

Mysql匹配函数

mysql - 我的程序的数据库设计是否存在循环引用问题?

php - 如何从 php 脚本中获取结果

mysql select sql 很慢,哪个索引丢了

mysql - Amazon RDS 到自定义 MySQL 服务器 InnoDB key 大小

java - 将 mysql 连接到 Java netbeans 时出错