MySQL 分布式事务锁恢复机制探究

Posted 数据库开发者

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL 分布式事务锁恢复机制探究相关的知识,希望对你有一定的参考价值。

XA PREPARE prepare了一个事务之后,这个事务的日志虽然已经写入了innodb redo log,但是事务锁并没有释放。只有在执行xa commit时候才会释放事务锁。那么如果mysql因为crash,被kill掉等原因而重启后,一个事务还能否像刚才mysqld退出之前那样持有当时它当时持有的事务锁呢? 这一点非常关键,如果不能把事务锁的状态正确地恢复的话,那么事务的隔离性就会在重启后被破坏,比如,另一个事务就可以更新正在被更新还未提交的一行。  说实话在亲自验证之前我感觉mysql 可能并不能做到这一点,不过结果非常意外,mysql-5.7.16mariadb-10.1.9都可以正确地恢复一个innodb 事务的事务锁。当然,由于mariadb-10.1.9xa事务的支持原本有其他问题,导致它的表现与mysql-5.7.16还是有点差距。另外,MySQL SERVER层面的表锁并不能正确恢复,我已经向Oracle MySQL官方报告了这个bug:  http://bugs.mysql.com/bug.php?id=84345

 

MySQL分布式事务锁恢复功能验证

首先我们的数据是这样的:

 

mysql> use test;

Reading table information for completion of table and column names

You can turn off this feature to get a quicker startup with -A

 

Database changed

mysql> show tables;

+----------------+

| Tables_in_test |

+----------------+

| t1             |

| t3             |

+----------------+

2 rows in set (0.00 sec)

 

mysql> show create table t1;

+-------+-------------------------------------------------------------------------------------------------------------------+

| Table | Create Table                                                                                                      |

+-------+-------------------------------------------------------------------------------------------------------------------+

| t1    | CREATE TABLE `t1` (

  `a` int(11) DEFAULT NULL,

  `b` int(11) DEFAULT NULL

) ENGINE=InnoDB DEFAULT CHARSET=latin1 |

+-------+-------------------------------------------------------------------------------------------------------------------+

1 row in set (0.00 sec)

 

mysql> select *from t1;

+------+------+

| a    | b    |

+------+------+

|    1 |    3 |

|    3 |    2 |

|    2 |    4 |

|  100 |  100 |

|  200 |  100 |

|    0 |  100 |

|  300 |  100 |

|  400 |  400 |

|  500 |  500 |

|  600 |  600 |

|   60 |   60 |

|  660 |  660 |

|    1 |    1 |

|   44 |   44 |

|   55 |   55 |

+------+------+

15 rows in set (0.00 sec)

 

 

 

mysql> show create table t3;

+-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

| Table | Create Table                                                                                                                                                            |

+-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

| t3    | CREATE TABLE `t3` (

  `a` tinyint(4) NOT NULL AUTO_INCREMENT,

  `b` int(11) DEFAULT NULL,

  PRIMARY KEY (`a`)

) ENGINE=InnoDB AUTO_INCREMENT=127 DEFAULT CHARSET=latin1 |

+-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

1 row in set (0.00 sec)

 

mysql> select*from t3;

+-----+------+

| a   | b    |

+-----+------+

|  -1 |    4 |

|  22 |    7 |

| 127 |    3 |

+-----+------+

3 rows in set (0.00 sec)

 

 

 

测例1. 验证在Xa commit之前,不释放事务锁。


测例2. Xa prepare之后退出会话,不会丢失事务锁。

MySQL 分布式事务锁恢复机制探究

测例3. Killmysqld 后 检查事务锁恢复是否正确 

MySQL 分布式事务锁恢复机制探究

再来一个insert的测例。


MySQL 分布式事务锁恢复机制探究

MySQL XA事务锁恢复的实现机制研究

那么这个事务锁恢复功能是如何实现的呢,我做了如下3中猜测。在深入到代码之前,我打算使用一些测例来验证和实验。

1. 通过innodb undo日志和/或 redo日志恢复

由于从innodbundo redo日志可以知道一个事务插入、删除、更新了那些行,那么对这些行获取行锁即可。为了验证这一个想法,特地做了测例4,结果分析见下文。

 

2. 直接锁住全表

 

这么做不太可能,因为如果有多个prepared事务更新了同一个表,那么就无法让每个事务获取互斥表锁。而如果不针对事务直接锁表(引用计数),则与通常的事务处理方法非常不同。

为了验证这个想法,特地做了测例5,结果分析见下文。

 

3. xa prepareredo log时,将这个innodb事务的所有事务锁也记录到redo log中;做恢复的时候,直接恢复这些事务锁。

经过几轮测试分析,我认为这正是innodb实现事务锁恢复的方法。

 

测例4. 未更新的行的行锁恢复 

 

由于t1表没有主键,所以执行更新语句会锁住每一行。左边事务T1执行一个分布式事务,更新t1的一行R1。在右边的事务T2更新t1不同行R2T1的更新语句做了全表扫导致阻塞其他行的更新。Killmysqld然后重启恢复后,看是否只恢复了被更新的行的行锁还是恢复了所有扫过的行的行锁。

MySQL 分布式事务锁恢复机制探究

从结果来看,右边的事务 T2 虽然更新的是另一行但是mysqld重启后仍然被左边的PREPARED状态的事务T1阻塞了,说明T1mysqldkill掉之前持有的所有事务锁都被恢复了,所以innodb应该不会是使用undo/redo log中记录的这个事务insert/update/delete的数据行来恢复事务锁的。不过,有没有可能是在恢复后,这个表t1完全被锁定了(T1持锁),果真如此的话也会是上述行为。下面这个测例可以回答这个问题。

 

测例5. 恢复的是表级锁还是行级锁 


左边的事务T1 更新有主键的表的一行R1,避免全表扫描锁住所有行。右边的事务T2更新另外一行R2,没有锁冲突直接返回;然后更新R1,发生锁等待。

然后在mysqldkill掉然后重启恢复后,在右边会话中首先更新不同于R1的一行R3,仍然没有锁冲突直接返回,说明innodb恢复出来的T1的事务锁是行锁而不是表锁。并且重新执行更新R1仍然会被阻塞,符合预期。

 

下面这个测例更加直接,同时也发现了MySQL-5.7.16的一个BUG,我已经向Oracle MySQL官方报告了这个bug: http://bugs.mysql.com/bug.php?id=84345


总结

Innodb事务锁的恢复是innodb自身完成的,只要执行了xa prepare,那么就可以正确地恢复事务锁。XA事务分支的事务锁在mysqld运行期间可以正确保持,如果在prepare之后因为各种原因导致mysqld重启恢复,那么Innodb里面的事务锁可以正确的恢复。但是MySQL SERVER层面的表锁不能正确恢复,导致了问题。


以上是关于MySQL 分布式事务锁恢复机制探究的主要内容,如果未能解决你的问题,请参考以下文章

Redis数据库系列Redis事务乐观锁和分布式锁

一文详解-MySQL 事务和锁

Spring一次线上@Transational事务注解未生效的原因探究

mysql锁分析

分布式锁分析:使用Redis实现分布式事务中的锁机制

分布式锁分析:使用Redis实现分布式事务中的锁机制