'Lock table for retrieving MAX() id until the max id + 1 is inserted into the table as a new record

Triggered by a web-hook of an e-commerce platform after a new order, I have to update a table with the order's details in a local system. So for the customer's info insertion part, I need to determine whether the customer is an existing one, or should be inserted as a new customer...

So, I wrote this piece of code:

        sqlsrv_query($connection, sprintf("INSERT INTO z_web_users (UserID, OrderID, FirstName, LastName, Country, State, Region, Street, Zip, Phone, Email) VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s');",
            sqlsrv_fetch_array(sqlsrv_query($connection, "SELECT COALESCE((SELECT DISTINCT UserId FROM z_web_users WHERE Email = '{$order->billing->email}'), (SELECT MAX(UserId) + 1 FROM z_web_users), 1) AS ID;"), SQLSRV_FETCH_ASSOC)['ID'],
            $order->id,
            $order->billing->first_name,
            $order->billing->last_name,
            $order->billing->country,
            $states[$order->billing->state],
            $order->billing->city,
            $order->billing->address_1,
            $order->billing->postcode,
            $order->billing->phone,
            $order->billing->email,
        ));

The problem is a got many suggestions that I should add some mechanism to ensure the returned MAX(UserId) stays out of conflicts. Given that the table z_web_users doesn't take any measures by its design, nor do I have any control over its design, what can I do in my code to ensure that?



Solution 1:[1]

Well, the first thing I would say is that you should avoid rolling your own id sequence mechanism if at all possible. This is a case of reinventing an existing well oiled wheel.

So, if you really have to roll your own, then I would use a stored procedure with the following content which takes a complete table lock, therefore ensuring only 1 application can access this at a time.

This is a simple way to solve the concurrency issue, and not the most optimal, but it will solve the problem in the short term while you investigate a better solution.

BEGIN
    SET NOCOUNT, XACT_ABORT ON;

    BEGIN TRAN;

    DECLARE @Id INT;

    -- Once we get this lock, we have exclusive access to the table until the end of the transaction
    SELECT @Id = COALESCE(MAX(id), (SELECT COALESCE(MAX(id), 0) + 1 FROM Users))
    FROM Users WITH (TABLOCKX)
    WHERE Email = @Email;

    -- Put your original insert here, modified to use the @Id we have already obtained
    INSERT INTO Users (Id, OtherCols...)
        VALUES (@Id, OtherCols...);

    COMMIT;

    RETURN 0;
END;

Note: The Create Procedure Docs provide all you need to know to encapsulate this into a stored procedure.

And you'll need to research how to call a stored procedure from your environment.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1