MDEV-10962 Deadlock with 3 concurrent DELETEs by unique key
PROBLEM: A deadlock was possible when a transaction tried to "upgrade" an already held Record Lock to Next Key Lock. SOLUTION: This patch is based on observations that: (1) a Next Key Lock is equivalent to Record Lock combined with Gap Lock (2) a GAP Lock never has to wait for any other lock In case we request a Next Key Lock, we check if we already own a Record Lock of equal or stronger mode, and if so, then we change the requested lock type to GAP Lock, which we either already have, or can be granted immediately, as GAP locks don't conflict with any other lock types. (We don't consider Insert Intention Locks a Gap Lock in above statements). The reason of why we don't upgrage Record Lock to Next Key Lock is the following. Imagine a transaction which does something like this: for each row { request lock in LOCK_X|LOCK_REC_NOT_GAP mode request lock in LOCK_S mode } If we upgraded lock from Record Lock to Next Key lock, there would be created only two lock_t structs for each page, one for LOCK_X|LOCK_REC_NOT_GAP mode and one for LOCK_S mode, and then used their bitmaps to mark all records from the same page. The situation would look like this: request lock in LOCK_X|LOCK_REC_NOT_GAP mode on row 1: // -> creates new lock_t for LOCK_X|LOCK_REC_NOT_GAP mode and sets bit for // 1 request lock in LOCK_S mode on row 1: // -> notices that we already have LOCK_X|LOCK_REC_NOT_GAP on the row 1, // so it upgrades it to X request lock in LOCK_X|LOCK_REC_NOT_GAP mode on row 2: // -> creates a new lock_t for LOCK_X|LOCK_REC_NOT_GAP mode (because we // don't have any after we've upgraded!) and sets bit for 2 request lock in LOCK_S mode on row 2: // -> notices that we already have LOCK_X|LOCK_REC_NOT_GAP on the row 2, // so it upgrades it to X ...etc...etc.. Each iteration of the loop creates a new lock_t struct, and in the end we have a lot (one for each record!) of LOCK_X locks, each with single bit set in the bitmap. Soon we run out of space for lock_t structs. If we create LOCK_GAP instead of lock upgrading, the above scenario works like the following: // -> creates new lock_t for LOCK_X|LOCK_REC_NOT_GAP mode and sets bit for // 1 request lock in LOCK_S mode on row 1: // -> notices that we already have LOCK_X|LOCK_REC_NOT_GAP on the row 1, // so it creates LOCK_S|LOCK_GAP only and sets bit for 1 request lock in LOCK_X|LOCK_REC_NOT_GAP mode on row 2: // -> reuses the lock_t for LOCK_X|LOCK_REC_NOT_GAP by setting bit for 2 request lock in LOCK_S mode on row 2: // -> notices that we already have LOCK_X|LOCK_REC_NOT_GAP on the row 2, // so it reuses LOCK_S|LOCK_GAP setting bit for 2 In the end we have just two locks per page, one for each mode: LOCK_X|LOCK_REC_NOT_GAP and LOCK_S|LOCK_GAP. Another benefit of this solution is that it avoids not-entirely const-correct, (and otherwise looking risky) "upgrading". The fix was ported from mysql/mysql-server@bfba840dfa7794b988c59c94658920dbe556075d mysql/mysql-server@75cefdb1f73b8f8ac8e22b10dfb5073adbdfdfb0 Reviewed by: Marko Mäkelä
Showing
Please register or sign in to comment