MySQL select for update returns empty set even though a row exists

筅森魡賤 提交于 2019-12-10 19:52:36

问题


I'm seeing a strange issue with MySQL's "select for update". I am using version 5.1.45. I have two tables:

    mysql> show create table tag;
+-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                                                                                                                      |
+-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| tag   | CREATE TABLE `tag` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `message` varchar(255) NOT NULL,
  `created_at` bigint(20) unsigned NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8 |
+-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
mysql> show create table live_tag;
+----------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table    | Create Table                                                                                                                                                                                                           |
+----------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| live_tag | CREATE TABLE `live_tag` (
  `tag_id` int(10) unsigned NOT NULL,
  KEY `live_tag_tag_fk` (`tag_id`),
  CONSTRAINT `live_tag_tag_fk` FOREIGN KEY (`tag_id`) REFERENCES `tag` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+----------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

The first stores versions ("tags") a user has saved, along with a commit message. The second table contains the id of the version that is currently "live". There is a foreign key on live_tag.tag_id referencing tag(id). live_tag only ever contains a single row. This row is updated when a new version is committed. Before updating the live_tag row I execute this statement:

mysql> select tag_id from live_tag for update;

However, when I run this statement in two terminals, and update the tag_id in one of them, sometimes MySQL returns "empty set" in the second terminal instead of the new value:

-- TERMINAL ONE
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

-- TERMINAL TWO
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

-- TERMINAL ONE
mysql> select tag_id from live_tag for update;
+--------+
| tag_id |
+--------+
|      2 |
+--------+
1 row in set (0.00 sec)

-- TERMINAL TWO
mysql> select tag_id from live_tag for update;
-- hangs (waiting for lock)

-- TERMINAL ONE
mysql> update live_tag set tag_id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.01 sec)

-- TERMINAL TWO returns the following for previous "select tag_id from live_tag for update"
Empty set (8.54 sec) -- Why empty set?

I did not delete any rows, I simply updated the one row in live_tag, why isn't MySQL seeing the update?

What's more weird, is that I've noticed if I set live_tag to a higher value than it was previously, the second terminal correctly returns the new value:

-- TERMINAL ONE
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

-- TERMINAL TWO
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

-- TERMINAL ONE
mysql> select tag_id from live_tag for update;
+--------+
| tag_id |
+--------+
|      1 |
+--------+
1 row in set (0.00 sec)

-- TERMINAL TWO
mysql> select tag_id from live_tag for update;
-- hangs (waiting for lock)

-- TERMINAL ONE
mysql> update live_tag set tag_id = 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.01 sec)

-- TERMINAL TWO returns the following for previous "select tag_id from live_tag for update"
+--------+
| tag_id |
+--------+
|      2 |
+--------+
-- this is correct

The problem only occurs when I set tag_id to a LOWER value than it was previously.

Is this something due to the foreign key constraint on tag_id? Or because I'm selecting all rows in the table (no 'where' clause)?

What I've already tried:

  • After dropping the keys on live_tag.tag_id, it works correctly.

  • I added an id column to live_tag, and limited my 'select for update' with 'where id = 1'. This also works correctly.

  • I tried this with three terminals. After committing 1, 2 immediately returns empty set. A few seconds later, 3 also returns empty set (even though I haven't committed 2).

I'm fine with adding the id column to the table, but still curious about this odd behavior? I've tried googling and searching here, but haven't found an answer.

UPDATE

Barmar's theory seems correct, since I tried his suggested test, and got only 1 row in the response:

-- TERMINAL ONE
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

-- TERMINAL TWO
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

-- TERMINAL ONE
mysql> select tag_id from live_tag for update;
+--------+
| tag_id |
+--------+
|      2 |
|      3 |
+--------+
2 rows in set (0.00 sec)

-- TERMINAL TWO
mysql> select tag_id from live_tag for update;
-- hangs

-- TERMINAL ONE
mysql> update live_tag set tag_id=1 where tag_id=2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> update live_tag set tag_id=4 where tag_id=3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from live_tag;
+--------+
| tag_id |
+--------+
|      1 |
|      4 |
+--------+
2 rows in set (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

-- TERMINAL TWO returns
+--------+
| tag_id |
+--------+
|      4 |
+--------+
1 row in set (34.02 sec)

Anyone have a newer version of MySQL who wants to try this?


回答1:


From the dependency on setting the value of an indexed column higher or lower, it looks like the lock is actually being placed on the index entry. The database engine scans the index, and stops at the first locked entry, waiting for it to be released.

When the first transaction is committed, the index is unlocked, and the waiting transaction continues scanning the index. Because the value was lowered, it is now earlier in the index. So the resumed scan doesn't see it because it has already passed that point.

To confirm this, try the following test:

  1. Create two rows, with values 2 and 3.
  2. In both transactions, do the SELECT ... FOR UPDATE
  3. In transaction 1, change 2 to 1, 3 to 4.
  4. Commit transaction 1.

If my guess is correct, transaction 2 should return just the row with 4.

This seems like a bug to me, as I don't think you should ever get partial results like this. Unfortunately, it's difficult to search for this at bugs.mysql.com, because the word "for" is ignored when searching because it's too short or common. Even quoting "for update" doesn't seem to find bugs that only contain this phrase.



来源:https://stackoverflow.com/questions/26723981/mysql-select-for-update-returns-empty-set-even-though-a-row-exists

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!