Question

I have the following 2 tables, with sample values:

producer_tbl:
id (auto-inc, PK)  producer_id   item_id   item_added
       2               5            3          20

products_available_tbl:
item_id (PK)   avail_cnt   blocked_cnt
  3               9             2

Here is the method how I access them:

When a manufacturer provides me with an item, I insert appropriate data in producer_tbl. I simultaneously increment the avail_cnt for the respective item in products_available_tbl.

When a consumer wants that given item, I first use (avail_cnt - blocked_cnt) to check if the quantity asked for is available. If so, I increment the blocked_cnt with the quantity, but don't update avail_cnt. When the consumer commits to his request, I decrement the blocked_cnt and avail_cnt, both with the same quantity.

Now, when there are multiple producers and consumers touching the same item simultaneously, I need atomicity for the above operations.

I would like to know if can I solve this with triggers? (I don't want to use external mutexes) Anyone can point me to sample examples of how to do this?

Was it helpful?

Solution

Per your request, below are comments that focus on the performance issues in your code:

Optimizing add_item()

Assuming that the products_available_tbl has a unique index on item_id, then

CREATE PROCEDURE add_item(IN in_producer_id INT, IN in_item_id INT,
                                                IN in_item_cnt INT)
BEGIN
    DECLARE item INT DEFAULT NULL;
    START TRANSACTION;

    INSERT INTO producer_tbl (producer_id, item_id, item_cnt)
        VALUES (in_producer_id, in_item_id, in_item_cnt);

    SELECT item_id FROM products_available_tbl 
        WHERE item_id=in_item_id INTO item FOR UPDATE;

    IF item IS NOT NULL THEN
        UPDATE products_available_tbl 
            SET avail_cnt=avail_cnt + in_item_cnt
            WHERE item_id=in_item_id;
    ELSE
        INSERT INTO products_available_tbl 
            (item_id, avail_cnt, blocked_cnt)
            VALUES (in_item_id, in_item_cnt, 0);
    END IF;

    COMMIT;
END //

could be rewritten as:

CREATE PROCEDURE add_item(IN in_producer_id INT, IN in_item_id INT,
                                                IN in_item_cnt INT)
BEGIN
    START TRANSACTION;

    INSERT INTO producer_tbl (producer_id, item_id, item_cnt)
        VALUES (in_producer_id, in_item_id, in_item_cnt);

    INSERT INTO products_available_tbl SET
        item_id = in_item_id,
        avail_cnt = in_item_cnt,
        blocked_cnt = 0
    ON DUPLICATE KEY UPDATE
        avail_cnt = avail_cnt + in_item_cnt;

    COMMIT;
END //

Optimizing block_item()

The optimization is significant, so let's proceed in stages:

First, let's rewrite

SET out_cnt = var_avail_cnt - var_blocked_cnt;
IF out_cnt >= cnt THEN
    SET out_cnt = cnt;
END IF;

as

SET out_cnt = LEAST(var_avail_cnt - var_blocked_cnt, cnt);

Next, let's rewrite

SELECT avail_cnt, blocked_cnt FROM products_available_tbl
    WHERE item_id=in_item_id INTO var_avail_cnt, var_blocked_cnt
    FOR UPDATE;

SET out_cnt = LEAST(var_avail_cnt - var_blocked_cnt, cnt);

as

SELECT LEAST(avail_cnt - blocked_cnt, cnt) FROM products_available_tbl
    WHERE item_id=in_item_id INTO out_cnt
    FOR UPDATE;

Finally, let's rewrite

SELECT LEAST(avail_cnt - blocked_cnt, cnt) FROM products_available_tbl
    WHERE item_id=in_item_id INTO out_cnt
    FOR UPDATE;

UPDATE products_available_tbl
    SET blocked_cnt = var_blocked_cnt + out_cnt
    WHERE item_id = in_item_id;

as

UPDATE products_available_tbl
    SET 
    blocked_cnt = blocked_cnt + (@out_cnt := LEAST(avail_cnt - blocked_cnt, cnt))
    WHERE item_id = in_item_id;

so

CREATE PROCEDURE block_item(IN in_item_id INT, INOUT cnt INT)
BEGIN
    DECLARE out_cnt INT DEFAULT cnt;
    DECLARE var_avail_cnt, var_blocked_cnt INT DEFAULT 0;
    START TRANSACTION;

    SELECT avail_cnt, blocked_cnt FROM products_available_tbl
        WHERE item_id=in_item_id INTO var_avail_cnt, var_blocked_cnt
        FOR UPDATE;

    SET out_cnt = var_avail_cnt - var_blocked_cnt;
    IF out_cnt >= cnt THEN
        SET out_cnt = cnt;
    END IF;

    UPDATE products_available_tbl
        SET blocked_cnt = var_blocked_cnt + out_cnt
        WHERE item_id = in_item_id;

    SET cnt = out_cnt;
    COMMIT;
END //

becomes

CREATE PROCEDURE block_item(IN in_item_id INT, INOUT cnt INT)
BEGIN
    UPDATE products_available_tbl
        SET 
        blocked_cnt = blocked_cnt + (@out_cnt := LEAST(avail_cnt - blocked_cnt, cnt))
        WHERE item_id = in_item_id;

    SET cnt = @out_cnt;
END //

Optimizing commit_item():

Let's rewrite

CREATE PROCEDURE commit_item(IN in_item_id INT, INOUT cnt INT)
BEGIN
    DECLARE out_cnt INT DEFAULT cnt;
    DECLARE var_avail_cnt, var_blocked_cnt INT DEFAULT 0;
    START TRANSACTION;

    SELECT avail_cnt, blocked_cnt FROM products_available_tbl
        WHERE item_id=in_item_id INTO var_avail_cnt, var_blocked_cnt
        FOR UPDATE;

    IF cnt > var_blocked_cnt THEN
        SET out_cnt = -1;  /* Error case: Caller supplied wrong value. */
    ELSEIF var_blocked_cnt > var_avail_cnt THEN
        SET out_cnt = -2;  /* Error case: Bug in block_item proc. */
    ELSE
        SET out_cnt = cnt;

        UPDATE products_available_tbl
            SET blocked_cnt = var_blocked_cnt - out_cnt,
                avail_cnt = var_avail_cnt - out_cnt
            WHERE item_id = in_item_id;
    END IF;

    SET cnt = out_cnt;
    COMMIT;
END //

as

CREATE PROCEDURE commit_item(IN in_item_id INT, INOUT cnt INT)
proc: BEGIN
    DECLARE var_avail_cnt, var_blocked_cnt INT DEFAULT 0;

    UPDATE products_available_tbl
        SET blocked_cnt   = blocked_cnt - cnt,
            avail_cnt     = avail_cnt - cnt
        WHERE item_id     = in_item_id
        AND   cnt         <= blocked_cnt
        AND   blocked_cnt <= avail_cnt;

    IF ROW_COUNT() > 0 THEN
        LEAVE proc;
    END IF;

    SELECT avail_cnt, blocked_cnt FROM products_available_tbl
        WHERE item_id=in_item_id INTO var_avail_cnt, var_blocked_cnt;

    IF cnt > var_blocked_cnt THEN
        SET cnt = -1;  /* Error case: Caller supplied wrong value. */
    ELSEIF var_blocked_cnt > var_avail_cnt THEN
        SET cnt = -2;  /* Error case: Bug in block_item proc. */
    ELSE
        SET cnt = -3; /* UPDATE failed, reasons unknown. */
    END IF;
END //

I hope these help. Let me know what you think!

OTHER TIPS

You can always make sure you are updating the record you just read, by adding a uuid VARCHAR(32) column to any table. You read the record you want to update, then you update the record with a check that the uuid field didn't change.

For example, you can increment blocked_cnt via:

UPDATE products_available_tbl
   SET blocked_cnt = blocked_cnt + 1,
       uuid = UUID()
 WHERE blocked_cnt = 2
   AND uuid = '21EC2020-3AEA-1069-A2DD-08002B30309D';

SELECT ROW_COUNT(); -- a 1 indicates the UPDATE was successful, 0 or -1 failure

To decrement the blocked_cnt and avail_cnt fields:

UPDATE products_available_tbl
   SET blocked_cnt = blocked_cnt - 1,
       avail_cnt = avail_cnt - 1,
       uuid = UUID()
 WHERE blocked_cnt = 3
   AND uuid = '3F2504E0-4F89-11D3-9A0C-0305E82C3301';

SELECT ROW_COUNT();

To save 24 bytes per record, you could use a uuid_short BIGINT field instead, and replace the UUID()s above with UUID_SHORT()s.

If you want to make sure no one can change the record between when you read it and update it, you either have to use SELECT ... FOR UPDATE or SELECT ... LOCK IN SHARE MODE inside a START TRANSACTION ... COMMIT, which requires an ENGINE that supports transactions, such as InnoDB, or LOCK TABLES READ [LOCAL] / UNLOCK TABLES which work on all database engines.

I found that using stored procedures is a good way to solve the problem in this scenario with multiple people accessing/modifying multiple tables here, which also need atomicity. Here are the 3 procedures to add an item, block an item and commit an item. The add_item operation doesn't really need atomicity since it always adds to the item's avail_cnt in products_available_tbl, so the add_item procedure is not really necessary.

DELIMITER //
DROP PROCEDURE IF EXISTS `add_item` //
CREATE PROCEDURE add_item(IN in_producer_id INT, IN in_item_id INT,
                                                IN in_item_cnt INT)
BEGIN
    DECLARE item INT DEFAULT NULL;
    START TRANSACTION;

    INSERT INTO producer_tbl (producer_id, item_id, item_cnt)
        VALUES (in_producer_id, in_item_id, in_item_cnt);

    SELECT item_id FROM products_available_tbl 
        WHERE item_id=in_item_id INTO item FOR UPDATE;

    IF item IS NOT NULL THEN
        UPDATE products_available_tbl 
            SET avail_cnt=avail_cnt + in_item_cnt
            WHERE item_id=in_item_id;
    ELSE
        INSERT INTO products_available_tbl 
            (item_id, avail_cnt, blocked_cnt)
            VALUES (in_item_id, in_item_cnt, 0);
    END IF;

    COMMIT;
END //
DELIMITER ;

This 'block_item' gets called when a consumer places a request for an item, but haven't committed to a buy.

DELIMITER //
DROP PROCEDURE IF EXISTS `block_item` //
CREATE PROCEDURE block_item(IN in_item_id INT, INOUT cnt INT)
BEGIN
    DECLARE out_cnt INT DEFAULT cnt;
    DECLARE var_avail_cnt, var_blocked_cnt INT DEFAULT 0;
    START TRANSACTION;

    SELECT avail_cnt, blocked_cnt FROM products_available_tbl
        WHERE item_id=in_item_id INTO var_avail_cnt, var_blocked_cnt
        FOR UPDATE;

    SET out_cnt = var_avail_cnt - var_blocked_cnt;
    IF out_cnt >= cnt THEN
        SET out_cnt = cnt;
    END IF;

    UPDATE products_available_tbl
        SET blocked_cnt = var_blocked_cnt + out_cnt
        WHERE item_id = in_item_id;

    SET cnt = out_cnt;
    COMMIT;
END //
DELIMITER ;

This 'commit_item' gets called when the customer confirms the order.

DELIMITER //
DROP PROCEDURE IF EXISTS `commit_item` //
CREATE PROCEDURE commit_item(IN in_item_id INT, INOUT cnt INT)
BEGIN
    DECLARE out_cnt INT DEFAULT cnt;
    DECLARE var_avail_cnt, var_blocked_cnt INT DEFAULT 0;
    START TRANSACTION;

    SELECT avail_cnt, blocked_cnt FROM products_available_tbl
        WHERE item_id=in_item_id INTO var_avail_cnt, var_blocked_cnt
        FOR UPDATE;

    IF cnt > var_blocked_cnt THEN
        SET out_cnt = -1;  /* Error case: Caller supplied wrong value. */
    ELSEIF var_blocked_cnt > var_avail_cnt THEN
        SET out_cnt = -2;  /* Error case: Bug in block_item proc. */
    ELSE
        SET out_cnt = cnt;

        UPDATE products_available_tbl
            SET blocked_cnt = var_blocked_cnt - out_cnt,
                avail_cnt = var_avail_cnt - out_cnt
            WHERE item_id = in_item_id;
    END IF;

    SET cnt = out_cnt;
    COMMIT;
END //
DELIMITER ;

These have been tested to work fine.

Side note: Since I use python, I need to use cursor.fetchone() after calling these procedures.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top