防止读取旧版本记录的事务并发
Transaction concurrency to prevent read of old version of a record
假设我有一个名为 tasks
的 table。每个任务都有一个status
。我将其中一项处于 To Manage
状态的任务置于 In Management
状态,并 运行 为其创建任务的过程(可能需要几秒钟才能完成).
在执行结束时,任务可能会return到To Manage
或Completed
状态,这取决于程序是否必须再次运行。
现在假设有几个进程同时运行这个activity,以便一起完成或以其他方式处理多个不同的任务。
我想确保两个进程不会同时管理同一个任务。为此,上述 activity 应在事务中执行:
$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 数据库,table 是 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
读取任务table并在操作2完成后释放它?
- 我还应该做些什么来防止出现意外结果吗?
数据库结构比上面描述的要复杂得多。当我更改任务状态时,我也会在另一个 table 上写日志。这是由代码 (Model 类) 本身完成的。我有任务 table,task_status table 带有任务外键和 task_status_change(即日志 table)。每个 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;
根据您的 table 结构,可以在单个原子 UPDATE
语句中完成交易。 (您没有提供太多细节。)
为每个状态转换做类似的事情。
这为 您的 代码的长 运行 集合提供事务语义,只使用数据库中的一个 status
。
听起来像是"queuing"机制。我有一个口头禅:"Don't queue it, just do it." 这意味着它可能是 easier/faster/simpler 在您有任务要做时生成一个工作进程,而不是排队等
假设我有一个名为 tasks
的 table。每个任务都有一个status
。我将其中一项处于 To Manage
状态的任务置于 In Management
状态,并 运行 为其创建任务的过程(可能需要几秒钟才能完成).
在执行结束时,任务可能会return到To Manage
或Completed
状态,这取决于程序是否必须再次运行。
现在假设有几个进程同时运行这个activity,以便一起完成或以其他方式处理多个不同的任务。
我想确保两个进程不会同时管理同一个任务。为此,上述 activity 应在事务中执行:
$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 数据库,table 是 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
读取任务table并在操作2完成后释放它? - 我还应该做些什么来防止出现意外结果吗?
数据库结构比上面描述的要复杂得多。当我更改任务状态时,我也会在另一个 table 上写日志。这是由代码 (Model 类) 本身完成的。我有任务 table,task_status table 带有任务外键和 task_status_change(即日志 table)。每个 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;
根据您的 table 结构,可以在单个原子 UPDATE
语句中完成交易。 (您没有提供太多细节。)
为每个状态转换做类似的事情。
这为 您的 代码的长 运行 集合提供事务语义,只使用数据库中的一个 status
。
听起来像是"queuing"机制。我有一个口头禅:"Don't queue it, just do it." 这意味着它可能是 easier/faster/simpler 在您有任务要做时生成一个工作进程,而不是排队等