I have a users table with gifts_count and user_id fields
Should I add lock to the next statement(it may come from many threads)?
update users set gifts_count = gifts_count + 1 where users.user_id = user_id;
Your best bet would be to use transactions rather than explicit table locking.
An example session would look something like this:
START TRANSACTION;
UPDATE `users` SET `gifts_count` = `gifts_count` + 1 WHERE `users`.`user_id` = user_id;
COMMIT;
You can also do some integrity checks if you needed to (depending on what you're doing), between the query and the COMMIT, if something goes wrong, you can always use ROLLBACK.
If you want to prevent reads on the specific row when it's updated (if using InnoDB with row-level locking), you can run your SELECT query with LOCK IN SHARE MODE at the end, like so:
SELECT * FROM `users` `users`.`user_id` = user_id LOCK IN SHARE MODE;
That query will hang until your transaction is COMMITed or ROLLBACK'd.
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
I have the following table:
CREATE TABLE `accounts` (
`name` varchar(50) NOT NULL,
`balance` int NOT NULL,
PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
And it has two accounts in it. "Bob" has a balance of 100. "Jim" has a balance of 200.
I run this query to transfer 50 from Jim to Bob:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM accounts;
SELECT SLEEP(10);
SET #bobBalance = (SELECT balance FROM accounts WHERE name = 'bob' FOR UPDATE);
SET #jimBalance = (SELECT balance FROM accounts WHERE name = 'jim' FOR UPDATE);
UPDATE accounts SET balance = #bobBalance + 50 WHERE name = 'bob';
UPDATE accounts SET balance = #jimBalance - 50 WHERE name = 'jim';
COMMIT;
While that query is sleeping, I run the following query in a different session to set Jim's balance to 500:
UPDATE accounts SET balance = 500 WHERE name = 'jim';
What I thought would happen is that this would cause a bug. The transaction would set Jim's balance to 150, because the first read in the transaction (before the SLEEP) would establish a snapshot in which Jim's balance is 200, and that snapshot would be used in the later query to get Jim's balance. So we would subtract 50 from 200 even though Jim's balance has actually been changed to 500 by the other query.
But that's not what happens. Actually, the end result is correct. Bob has 150 and Jim has 450. But I don't understand why this is.
The MySQL documentation says about Repeatable Read:
This is the default isolation level for InnoDB. Consistent reads within the same transaction read the snapshot established by the first read. This means that if you issue several plain (nonlocking) SELECT statements within the same transaction, these SELECT statements are consistent also with respect to each other. See Section 15.7.2.3, “Consistent Nonlocking Reads”.
So what am I missing here? Why does it seem like the SELECT statements in the transaction are not all using a snapshot established by the first SELECT statement?
The repeatable-read behavior only works for non-locking SELECT queries. It reads from the snapshot established by the first query in the transaction.
But any locking SELECT query reads the latest committed version of the row, as if you had started your transaction in READ-COMMITTED isolation level.
A SELECT is implicitly a locking read if it's involved in any kind of SQL statement that modifies data.
For example:
INSERT INTO table2 SELECT * FROM table1 WHERE ...;
The above locks examined rows in table1, even though the statement is just copying them to table2.
SET #myvar = (SELECT ... FROM table1 WHERE ...);
This is also copying a value from table1, into a variable. It locks the examined row in table1.
Likewise SELECT statements that are invoked in a trigger, or as part of a multi-table UPDATE or DELETE, and so on. Anytime the SELECT is part of a larger statement that modifies any data (in a table or in a variable), it locks the rows examined by the SELECT.
And therefore it's a locking read, and behaves like an UPDATE with respect to which row version it reads.
1:
I was trying this and it was working fine:
start transaction;
select * from orders where id = 21548 LOCK IN SHARE MODE;
update orders set amount = 1500 where id = 21548;
commit;
According to the definition of LOCK IN SHARE MODE , it locks the table with IS lock and lock the selected rows with S lock.
When a row is locked with S lock.How can it be modified without releasing lock?
It needs X lock to modify it.Right? Or is it valid only for different connection transaction?
2:
//session1
start transaction;
select * from orders where id = 21548 FOR UPDATE;
Keep this session1 same and try this in the different session:
//session2
select * from orders where id = 21548; //working
update orders set amount = 2000 where id = 21548; //waiting
FOR UPDATE locks the entire table into IX mode and selected row into X mode.
As X mode is incompatible with S mode then how come select query in second session is getting executed?
One answer might be that select query is not asking for S lock that's why it's running successfully.But update query in the second session is also not asking for X lock , but as you execute it , it starts waiting for the lock held by session1.
I have read a lot of stuff regarding this but not able to clear my doubts.Please help.
Bill Karwin answered this question through email.He said:
The same transaction that holds an S lock can promote the lock to an X lock. This is not a conflict.
The SELECT in session 1 with FOR UPDATE acquires an X lock. A simple SELECT query with no locking clause specified does not need to acquire an S lock.
Any UPDATE or DELETE needs to acquire an X lock. That's implicit. Those statements don't have any special locking clause for that.
For more details on IS/IX locks and FOR UPDATE/LOCK IN SHARE MODE please visit
shared-and-exclusive-locks .
I want to implement parallel processing of multiple DB transactions which lock only a few rows for short periods of time. For Example we have this query executed every time an user opens the page:
START TRANSACTION;
SELECT * FROM table_1 WHERE worktime < UNIX_TIMESTAMP() FOR UPDATE;
...WORK...
...UPDATE...
COMMIT;
In a multiuser environment, this kind of row locking would lead to Deadlocks every time the select statement would be executed. Currently I would solve the problem using a second table to store the locked IDs:
START TRANSACTION;
LOCK TABLE table_1 WRITE, table_locks WRITE;
SELECT id FROM table_1 WHERE worktime < UNIX_TIMESTAMP() AND id NOT IN table_locks;
...insert locked Ids into Table "table_locks"...
...this prevents other calls to read from this table...
UNLOCK TABLES;
COMMIT;
...Perform calculations and Updates...
DELETE FROM table_locks WHERE id = ...
The problem of this method is, that if something goes wrong after "locking" a row by storing its ID in the table_locks table, this Row would never be updated anymore. Of course I can set a timeout to release such locks automatically after some time, but this doesen't seem properly done to me. But is there something possible like:
SELECT * FROM table_1 WHERE worktime < UNIX_TIMESTAMP() AND NOT LOCKED BY OTHER TRANSACTION FOR UPDATE
?
You could mark rows to be done by your session:
UPDATE table_1
SET marked_by_connection_id = CONNECTION_ID(),
marked_time = NOW()
WHERE worktime < UNIX_TIMESTAMP() AND marked_by_connection_id IS NULL;
Then you can feel free to work on any row that has your connection id, knowing that another session will not try to claim them:
SELECT * FROM table_1 WHERE marked_by_connection_id = CONNECTION_ID();
. . .
No locking or non-autocommit transaction is needed.
At the end of your session, unmark any rows you had marked:
UPDATE table_1 SET marked_by_connection_id = NULL
WHERE marked_by_connection_id = CONNECTION_ID();
Or alternatively your app could unmark individual rows as it processes them.
But perhaps your session dies before it can unmark those rows. So some rows were marked, but never processed. Run a cron job that clears such abandoned marked rows, allowing them to get re-processed by another worker, although a bit late.
UPDATE table_1 SET marked_by_connection_id = NULL
WHERE marked_time < NOW() - INTERVAL 30 MINUTE;
I have an issue where I'm running a web service which is performing a transaction involving two tables Users and Transactions. The trouble is that when I select from Transactions it sometimes is unable to find the latest row, which I'm able to see exists in the database.
I'm using Perl/Dancer as the web framework, although I think the issue I'm having is at the database level (I am using MySQL/InnoDB). The pseudocode looks like this:
my $dbh = DBI->connect_cached(("DBI:mysql:Database;host=localhost;mysql_ssl=1",
"user", "password",
{RaiseError => 0, AutoCommit => 0});
my $sth_transact = $dbh->prepare("select balance from Users ".
"where id=? for update");
... store result in local variable ...
my $sth_inner = $dbh->prepare("select deposit from Transactions ".
"where id=?");
$sth_inner->execute();
if (my $deposit = $sth_inner->fetch_row()) {
$deposit *= $bonus;
$sth_inner = $dbh->prepare("update Transactions set deposit=?".
"where id=?");
$sth_inner->bind_param(1, $deposit);
$sth_inner->bind_param(2, $transaction_id);
$sth_inner->execute();
... update balance in Users table ...
}
On the first few requests the select on the Transactions table returns the most recent row. However, on the third request, it can no longer find the most recent row. And when I do:
my $sth_inner = $dbh->prepare("select id from Transactions where id=(SELECT max(id) from Transactions)");
It returns the id of 3 rows older than the most recent row. E.g., if there were 87 rows in the Transactions table, it would return 84.
I'm not sure if I'm handling the locking incorrectly. I'm protecting the Users table with a select ... for update lock, but I'm not doing that for the Transactions table. I'm not sure if that's important. Since I nested the update to the Transactions table in the select ... for update of the Users table I thought it would be protected in the transaction.
Any help on why the select on the Transactions table isn't returning the most recent rows is appreciated.
This problem is usually caused by the default isolation level for InnoDB, which is "repeatable read." This means a connection can't see anything added to the database after the current transaction was started.
You can avoid this problem by changing the isolation level (possibly to "read committed" which is the default for Oracle) or by issuing a commit just before your select to begin a new transaction.