MySQL Transaction: SELECT + INSERT

后端 未结 1 1705
梦毁少年i
梦毁少年i 2020-12-21 14:38

Im building web application - reservation system using php and mysql. System will allow users to make reservations of time intervals on some devices (user time working on th

相关标签:
1条回答
  • 2020-12-21 14:45

    What you need is locking. Transactions are "not strictly needed" indeed.

    You can choose between "pessimistic locking" and "optimistic locking". The decision about which one of this two possibilities is up to you and has to be evaluated basically considering:

    • the level of concurrency that you have
    • the duration of the has-to-be-atomic operations on the database
    • the complexity of the whole operation

    I will recommend to read this two to build up an idea of the involved things:

    • Optimistic vs. Pessimistic locking
    • Optimistic locking in MySQL (here some examples that shows how transaction are not strictly needed)

    An Example to explain better

    This maybe is not so elegant but is only an example that shows how it is possible to do all without transaction (and even without the UNIQUE constraints). What is needed to do is to use the following combined INSERT + SELECT statemet and after its execution to check the number of affected rows. If the number of affected rows is 1 then it has succeded otherways (if it is 0) there have been a collision and the other party have won.

    INSERT INTO `slot` (`start`, `end`, `uid`, `group`, `message`, `devices_id`)
    SELECT @startTime, @endTime, @uid, @group, @message, @deviceId
    FROM `slot`
    WHERE NOT EXISTS (
        SELECT `id` FROM `slot`
        WHERE `start` <= @endTime AND `end` >= @startTime
        AND `devices_id` = @deviceId)
    GROUP BY (1);
    

    This is an example of Optimistic Locking obtained without transactions and with a single SQL operation.

    As it is written it has the problem that there needs to be at least one row already in the slot table in order it to work (otherways the SELECT clause will always return an empty recordset and in that case nothing is inserted evei if there are no collisions. THere are two possibilities to make it actually working:

    • insert one dummy row in the table maybe with the date in the past
    • rewrite so the main FROM clause refers to any table that has at least one row or better create one little table (maybe named dummy) with only one column and only one record in it and rewrite as following (note that there is no longer need for the GROUP BY clause)

      INSERT INTO `slot` (`start`, `end`, `uid`, `group`, `message`, `devices_id`)
      SELECT @startTime, @endTime, @uid, @group, @message, @deviceId
      FROM `dummy`
      WHERE NOT EXISTS (
          SELECT `id` FROM `slot`
          WHERE `start` <= @endTime AND `end` >= @startTime
          AND `devices_id` = @deviceId);
      

    Here following a series of instruction that if you simply copy/paste shows the idea in action. I have assumed that you encode date/times on int fields as a number with the digits of date and time concatenated.

    INSERT INTO `slot` (`start`, `end`, `uid`, `group`, `message`, `devices_id`)
    VALUES (1008141200, 1008141210, 11, 2, 'Dummy Record', 14)
    
    INSERT INTO `slot` (`start`, `end`, `uid`, `group`, `message`, `devices_id`)
    SELECT 1408141206, 1408141210, 11, 2, 'Hello', 14
    FROM `slot`
    WHERE NOT EXISTS (
        SELECT `id` FROM `slot`
        WHERE `start` <= 1408141210 AND `end` >= 1408141206
        AND `devices_id` = 14)
    GROUP BY (1);
    
    INSERT INTO `slot` (`start`, `end`, `uid`, `group`, `message`, `devices_id`)
    SELECT 1408141208, 1408141214, 11, 2, 'Hello', 14
    FROM `slot`
    WHERE NOT EXISTS (
        SELECT `id` FROM `slot`
        WHERE `start` <= 1408141214 AND `end` >= 1408141208
        AND `devices_id` = 14)
    GROUP BY (1);
    
    INSERT INTO `slot` (`start`, `end`, `uid`, `group`, `message`, `devices_id`)
    SELECT 1408141216, 1408141220, 11, 2, 'Hello', 14
    FROM `slot`
    WHERE NOT EXISTS (
        SELECT `id` FROM `slot`
        WHERE `start` <= 1408141220 AND `end` >= 1408141216
        AND `devices_id` = 14)
    GROUP BY (1);
    
    SELECT * FROM `slot`;
    

    This is clearly an extreme example of Optimistic Locking but is very efficient in the end because all is done with only one SQL instruction and with low interaction (data exchange) between the database server and php code. Further there is practically no "real" locking.

    ...or with Pessimistic Locking

    The same code can become a good Pessimistc Locking implementation just surrounding with explicit table lock/unlock instructions:

    LOCK TABLE slot WRITE, dummy READ;
    
    INSERT INTO `slot` (`start`, `end`, `uid`, `group`, `message`, `devices_id`)
    SELECT @startTime, @endTime, @uid, @group, @message, @deviceId
    FROM `dummy`
    WHERE NOT EXISTS (
        SELECT `id` FROM `slot`
        WHERE `start` <= @endTime AND `end` >= @startTime
        AND `devices_id` = @deviceId);
    
    UNLOCK TABLES;
    

    Of course in this case (Pessimistic Locking) the SELECT and INSERT could be separated and some php code executed in-between. However this code remains very quick to execute (no data exchange with php, no intermediate php code) and so the duration of the Pessimistic Lock is the shortest possible. Keeping Pessimistic Lock as short as possible is a key point in order to avoid slowing down of the application.

    Anyway you need to check the number of affected records return value in order to know if it succeeded since the code is practically the same and so you get the success/failure information in the same way.

    Here http://dev.mysql.com/doc/refman/5.0/en/insert-select.html they say that "MySQL does not permit concurrent inserts for INSERT ... SELECT statements" so it should not be needed the Pessimistic Lock but anyway this can be a good option if you think that this will be changing in future versions of MySQL.

    I am "Optimistic" that this will not change ;-)

    0 讨论(0)
提交回复
热议问题