I am writing a web application and came across the problem to keep a value in column that is identical in two rows only and both goes in single batch of execution. One way I came up with solution to read the MAX value in the column and increment by 1. Thus, I end-up writing the procedure to lock the table so that other user should not get the dirty read of MAX value.
Create table D(Id int , Name varchar(100))
Begin Tran
DECLARE #i int = (SELECT MAX(ID) FROM D with (tablockx, holdlock))
Print #i ;
Insert into D values ((#i + 1), 'ANAS')
SELECT * FROM D
--COMMIT
Rollback
This code lock the table until query commits or rollback. I have two question from this 1) Is this code guarantee to have exclusive lock on table? 2) In my quick read tablockx can help perform lock for read also whereas holdlock help to prevent the changes in row I am working in locked session, but is this rightly used because I think holdlock may actually not required
Not sure you are asking the right question. You need the current value not to be changed and no insert. With SERIALIZABLE you may not need (updlock).
I am not positive about this answer. You should do your own testing.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
Begin Tran
DECLARE #i int = (SELECT MAX(ID) FROM D with (updlock))
Print #i ;
Insert into D values ((#i + 1), 'ANAS')
SELECT * FROM D
--COMMIT
Rollback
identity or sequence number are better approaches
Related
I've encountered an undocumented behavior of "SET #my_var = (SELECT ..)" inside a transaction:
The first one is that it locks rows ( depends whether it is a unique index or not ).
Example -
START TRANSACTION;
SET #my_var = (SELECT id from table_name where id = 1);
select trx_rows_locked from information_schema.innodb_trx;
ROLLBACKL;
The output is 1 row locked, which is strange, it shouldn't gain a reading lock.
Also, the equivalent statement SELECT id INTO #my_var won't produce a lock.
It can lead to a deadlock in case of an UPDATED after the SET statement ( for 2 concurrent requests )
In REPEATABLE READ -
The SELECT inside the SET statement gets a new snapshot of the data, instead of using the original SNAPSHOT.
SESSION 1:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START transaction;
SELECT data FROM my_table where id = 2; # Output : 2
SESSION 2:
UPDATE my_table set data = 3 where id = 2 ;
SESSION 1:
SET #data = (SELECT data FROM my_table where id = 2);
SELECT #data; # Output : 3, instead of 2
ROLLBACK;
However, I would expect that #data will contain the original value from the first snapshot ( 2 ).
If I use SELECT data into #data from my_table where id = 2 then I will get the expected value - 2;
Do you have an idea what is the source of the different behavior of SET = (SELECT ..) compared to SELECT data INTO #var FROM .. ?
Thanks.
Correct — when you SELECT in a context where you're copying the results into a variable or a table, it implicitly works as if you had used a locking read SELECT ... FOR SHARE.
This means it places a shared lock on the rows examined, and it also means that the statement reads only the most recently committed version of rows, as if your transaction were in READ-COMMITTED isolation level.
I'm not sure why SELECT ... INTO #var does not do the same kind of implicit locking in MySQL 8.0. My memory is that in older versions of MySQL it did do locking in that query form. I've searched the manual for an explanation but I can't find one yet.
Other cases that implicitly lock the rows examined by SELECT, and therefore reads data as if you transaction is READ-COMMITTED:
INSERT INTO <table> SELECT ...
UPDATE or DELETE multi-table, even if you don't update or delete a given table, the rows joined become locked.
SELECT inside a trigger
Lets suppose I've a row with ID equals to 1 in table t.
If I run the following sql, I get a result equals to 1 (when column c is 0):
SELECT NEXT_ID(1)
But, if run this, I get 1 as a result, instead of 0 (as there is no row with ID = 2 in table t):
SELECT NEXT_ID(2)
NEXT_ID function:
CREATE FUNCTION NEXT_ID(id INT)
RETURNS VARCHAR(15)
BEGIN
DECLARE counter BIGINT DEFAULT 0;
UPDATE t SET c = (#counter := c +1) WHERE ID = id;
return #counter;
END;
My intent here is to create a counter that increment a value as an atomic operation.
So, why do I get a value greater than 0 on NEXT_ID(2)? It seems like the counter variable is been stored in the session...
Is this safe to use in a multithreaded application?
If you DECLARE counter then you should NOT use #counter to refer to the variable. In a MySQL stored function, declared variables don't have the # sigil. Variables with the # sigil are user-defined variables. #counter is a different variable than counter, even though they have the same spelling.
Is it safe to do in a multi-threaded app? Certainly if the threads are incrementing different rows by using different id values, they will not conflict (assuming id is a unique key of the t table).
Even if multiple threads use the same id value, and therefore need to increment the same row in the t table, what will happen is that one will get there first, get a lock on the row, and increment it. The second thread will wait to get its own lock until the first thread commits its transaction. Then the second thread will proceed, and will then see the incremented value of c.
So it's safe, but it will not allow a high rate of throughput. Threads that are in contention for the same id will have to queue up and wait for the current holder of the lock to commit their transaction. If you're expecting multiple threads to do their work in parallel, you'll find this becomes a bottleneck.
This is why AUTO_INCREMENT exists, because it locks the counter briefly to generate a new value, but it releases the lock immediately, not waiting for the caller's transaction to finish. This allows concurrent threads to keep working and not wait for one another.
You can't simulate the behavior of AUTO_INCREMENT with UPDATE operations, which are necessarily in transactions.
Re your comments:
Sorry, I forgot that := doesn't work with local declared variables. I tested it and I found two alternatives:
Alternative 1: don't bother to declare a local variable, just use the user-defined variable.
CREATE FUNCTION NEXT_ID(id INT)
RETURNS VARCHAR(15)
READS SQL DATA
BEGIN
UPDATE t SET c = (#counter := c +1) WHERE ID = id;
RETURN #counter;
END
Alternative 2: use a local variable, but set the LAST_INSERT_ID() to the incremented value. Then SET the counter local variable to that value.
CREATE FUNCTION NEXT_ID(id INT)
RETURNS VARCHAR(15)
READS SQL DATA
BEGIN
DECLARE counter BIGINT DEFAULT 0;
UPDATE t SET c = LAST_INSERT_ID(c +1) WHERE ID = id;
SET counter = LAST_INSERT_ID();
RETURN counter;
END;
I tested both alternatives and they work.
I am building a "poor man's queuing system" using MySQL. It's a single table containing jobs that need to be executed (the table name is queue). I have several processes on multiple machines whose job it is to call the fetch_next2 sproc to get an item off of the queue.
The whole point of this procedure is to make sure that we never let 2 clients get the same job. I thought that by using the SELECT .. LIMIT 1 FOR UPDATE would allow me to lock a single row so that I could be sure it was only updated by 1 caller (updated such that it no longer fit the criteria of the SELECT being used to filter jobs that are "READY" to be processed).
Can anyone tell me what I'm doing wrong? I just had some instances where the same job was given to 2 different processes so I know it doesn't work properly. :)
CREATE DEFINER=`masteruser`#`%` PROCEDURE `fetch_next2`()
BEGIN
SET #id = (SELECT q.Id FROM queue q WHERE q.State = 'READY' LIMIT 1 FOR UPDATE);
UPDATE queue
SET State = 'PROCESSING', Attempts = Attempts + 1
WHERE Id = #id;
SELECT Id, Payload
FROM queue
WHERE Id = #id;
END
Code for the answer:
CREATE DEFINER=`masteruser`#`%` PROCEDURE `fetch_next2`()
BEGIN
SET #id := 0;
UPDATE queue SET State='PROCESSING', Id=(SELECT #id := Id) WHERE State='READY' LIMIT 1;
#You can do an if #id!=0 here
SELECT Id, Payload
FROM queue
WHERE Id = #id;
END
The problem with what you are doing is that there is no atomic grouping for the operations. You are using the SELECT ... FOR UPDATE syntax. The Docs say that it blocks "from reading the data in certain transaction isolation levels". But not all levels (I think). Between your first SELECT and UPDATE, another SELECT can occur from another thread. Are you using MyISAM or InnoDB? MyISAM might not support it.
The easiest way to make sure this works properly is to lock the table.
[Edit] The method I describe right here is more time consuming than using the Id=(SELECT #id := Id) method in the above code.
Another method would be to do the following:
Have a column that is normally set to 0.
Do an "UPDATE ... SET ColName=UNIQ_ID WHERE ColName=0 LIMIT 1. That will make sure only 1 process can update that row, and then get it via a SELECT afterwards. (UNIQ_ID is not a MySQL feature, just a variable)
If you need a unique ID, you can use a table with auto_increment just for that.
You can also kind of do this with transactions. If you start a transaction on a table, run UPDATE foobar SET LockVar=19 WHERE LockVar=0 LIMIT 1; from one thread, and do the exact same thing on another thread, the second thread will wait for the first thread to commit before it gets its row. That may end up being a complete table blocking operation though.
I have a stored procedure with a select and an update. I would like to prevent multiple users, from executing it, at the same time, so I don't update, based on an incorrect select.
How do I lock it?
I've read various solutions (Transaction isolation, xlock), but I haven't been able to figure what I really want, and how to do it.
The easiest way is to forget about data locks but look at sp_getapplock to control access through the code
BEGIN TRY
EXEC sp_getapplock ...
SELECT ...
UPDATE ...
EXEC sp_releaseapplock
END TRY
...
Saying that, with thing like the OUTPUT clause and judicious use of ROWLOCK, UPDLOCK there is a good chance the UPDATE and SELECT can be one statement
Using the XLOCK table hint in the SELECT query:
CREATE TABLE [X]([x] INT NOT NULL)
GO
INSERT [X]([x]) SELECT 0
GO
CREATE PROCEDURE [ATOMIC]
AS
BEGIN
BEGIN TRAN
DECLARE #x INT = (
SELECT [x]
FROM [X] (XLOCK)
) + 1
UPDATE [X] SET [x] = #x
COMMIT TRAN
END
GO
You can then test this by running
EXEC [ATOMIC]
GO 10000
simultaneously from different sessions. You can test using
SELECT [x] FROM [X]
The value should be exactly 10 000 times the number of sessions you ran. If the number is less than expected you don't have atomic read + write, or some SPIDs may have been killed due to dead locking.
Hey guys, here is one I am not able to figure out. We have a table in database, where PHP inserts records. I created a trigger to compute a value to be inserted as well. The computed value should be unique. However it happens from time to time that I have exact same number for few rows in the table. The number is combination of year, month and day and a number of the order for that day. I thought that single operation of insert is atomic and table is locked while transaction is in progress. I need the computed value to be unique...The server is version 5.0.88. Server is Linux CentOS 5 with dual core processor.
Here is the trigger:
CREATE TRIGGER bi_order_data BEFORE INSERT ON order_data
FOR EACH ROW BEGIN
SET NEW.auth_code = get_auth_code();
END;
Corresponding routine looks like this:
CREATE FUNCTION `get_auth_code`() RETURNS bigint(20)
BEGIN
DECLARE my_auth_code, acode BIGINT;
SELECT MAX(d.auth_code) INTO my_auth_code
FROM orders_data d
JOIN orders o ON (o.order_id = d.order_id)
WHERE DATE(NOW()) = DATE(o.date);
IF my_auth_code IS NULL THEN
SET acode = ((DATE_FORMAT(NOW(), "%y%m%d")) + 100000) * 10000 + 1;
ELSE
SET acode = my_auth_code + 1;
END IF;
RETURN acode;
END
I thought that single operation of
insert is atomic and table is locked
while transaction is in progress
Either table is locked (MyISAM is used) or records may be locked (InnoDB is used), not both.
Since you mentioned "transaction", I assume that InnoDB is in use.
One of InnoDB advantages is absence of table locks, so nothing will prevent many triggers' bodies to be executed simultaneously and produce the same result.