在MySQL存储过程中处理非简单情况下的并发性


Handling concurrency in MySQL stored procedures for non-simple cases

我有一个表,其中插入的有效性是表中所有其他行的函数。该表管理资源的日期期间,只有当没有其他行与同一资源重叠时,新行才有效。

所以表是

resource_id INT
start_date DATE
end_date DATE

从概念上讲,如果以下查询有0个结果,就可以插入一行(其中newResourceId、newStartDate、newEndDate是要插入的元组)

select count(entity_id) from mytable where resource_id = newResourceId and start_date < newEndDate and end_date > newStartDate;

问题是如何确保正确处理并发插入?我需要一些方法来确保在执行上面的select和随后的insert之间不会插入任何行。因此,并发操作应该阻止上面的select,直到第一个操作提交。为了实现这一点,我使用以下存储过程:

DELIMITER //
drop procedure if exists rp_insert //
create procedure rp_insert
(
    IN p_resource_id INT(10), 
    IN p_start_date DATE, 
    IN p_end_date DATE,
    OUT result INT
) 
BEGIN   
    DECLARE num_conflicts INT;
    DECLARE conflicts CURSOR FOR SELECT entity_id from mytable WHERE start_date < p_end_date and end_date > p_start_date and resource_id = p_resource_id FOR UPDATE; 
    START TRANSACTION;
    OPEN conflicts;
    SELECT FOUND_ROWS() into num_conflicts;
    if num_conflicts = 0 then
        insert ignore into mytable (resource_id, start_date, end_date) values (p_resource_id, p_start_date, p_end_date); 
        set result = LAST_INSERT_ID();      
        COMMIT;
    else
        set result = -2;
        ROLLBACK;
    end if; 
END //
DELIMITER ;

因此,如果有人能告诉我这是否是处理并发的正确途径,我将不胜感激,特别是:

  • select游标上的"For update"是否意味着具有重叠where条件的并发操作将阻塞select,直到第一个事务提交/回滚?

  • 在相同的并发场景中,第二个操作SELECT会看到第一个操作插入的行吗(假设插入将在第二个事务开始后提交,但在第二次选择执行之前)?

  • 智能数据库引擎是否会意识到我从不更新select返回的任何行,并决定不强制执行select进行更新(例如,很容易看到事务中没有更新语句)

  • 还有其他关于表现的评论吗?有更好的方法吗?

关于信息,我决定反对:

  • 将表锁定为过杀(例如,如果选择结果中存在重叠,则第二次操作只需要阻止)。mysql存储过程中似乎也不允许使用锁

  • 用PHP而不是存储过程进行编码(没有理由进行数据库往返,因为我不需要知道冲突是什么,只需要知道插入是否成功)。

好吧,冒着独白的风险,我做了以下事情:

以下解决方案似乎有效;我用额外的参数测试了比赛条件下感兴趣的3个点的睡眠陈述:

  • 在选择更新之前
  • 选择更新之后但之前
  • 冲突测试在冲突测试之后但在插入之前

然后,我从两个不同的mysql会话中运行存储过程,其中一个将休眠,另一个将正常运行,并有机会领先于第一个。在每种情况下,预期的存储过程都插入了一行,而另一行则失败。因此,此实现对于并发调用是安全的

如果有人知道比这更好的方法或有其他评论,我仍然很感兴趣。我想只有当普通的数据库约束不能强制表的完整性时,才需要这样的东西。

DELIMITER //
create procedure rp_insert(
IN p_resource_id INT(10), IN p_start_date DATE, IN p_end_date DATE, OUT result INT)
BEGIN
    DECLARE num_conflicts INT DEFAULT 0;
    DECLARE num_locked_resources INT DEFAULT 0;
    DECLARE lock_resources CURSOR FOR SELECT resource_id from myresources WHERE resource_id = p_resource_id FOR UPDATE;
    DECLARE conflicts CURSOR FOR SELECT entity_id from mytable WHERE start_date < p_end_date and end_date > p_start_date and resource_id = p_resource_id;
    SET result = 0;
    START TRANSACTION;      
    OPEN lock_resources;
    SELECT FOUND_ROWS() into num_locked_resources;
    IF 0 < num_locked_resources THEN
        OPEN conflicts;
        SELECT FOUND_ROWS() into num_conflicts;
        if num_conflicts = 0 then
            insert ignore into mytable (resource_id, start_date, end_date) values (p_resource_id, p_start_date, p_end_date);
            SET result = LAST_INSERT_ID();
            COMMIT;
        else
            SET result = -2;
            ROLLBACK;
        end if; 
    ELSE
        set result = -3;
    END IF;
END //
DELIMITER ;