记一次Mysql线上事故之metadata锁详解

背景

在项目的一次需求中,须要对一个表增长字段,然而在执行增长字段的sql语句时,卡住了好久都没提交到Mysql完成,而此时对外接口服务请求也卡住了,这时中断卡住的alter table 语句,服务慢慢恢复正常,若是不搞清楚这个问题的根源,不敢增长字段,由于会直接影响到服务mysql

排查

经过show processlist 查看到在alter table语句执行卡住过程当中,累计了大量状态为 Waiting for table metadata lock 的记录算法

而后查看当前的事务状态 执行 select * from information_schema.innodb_trxGsql

mysql> select * from information_schema.innodb_trx\G
*************************** 1. row ***************************
                    trx_id: 421408771164000
                 trx_state: RUNNING
               trx_started: 2019-07-02 14:27:09
     trx_requested_lock_id: NULL
          trx_wait_started: NULL
                trx_weight: 0
       trx_mysql_thread_id: 11688
    ....复制代码

发现了其中一条已经运行了好久的事务,我怀疑跟这个运行好久的并且没有提交的事务有关。flask

测试还原

在本地mysql开多个终端测试session

session 1: 开启事务,执行select 语句,但不提交事务app

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

mysql> select * from t1;
+------+
| c1   |
+------+
|    1 |
+------+
1 row in set (0.00 sec)复制代码

session 2:执行增长字段sql测试

mysql> alter table t1 add c2 int;

复制代码

执行被阻塞了ui

mysql> show processlist;
+----+------+-----------+------+---------+------+---------------------------------+---------------------------+
| Id | User | Host      | db   | Command | Time | State                           | Info                      |
+----+------+-----------+------+---------+------+---------------------------------+---------------------------+
| 27 | root | localhost | test | Query   |  141 | Waiting for table metadata lock | alter table t1 add c2 int |
| 29 | root | localhost | test | Query   |    0 | starting                        | show processlist          |
| 30 | root | localhost | test | Sleep   |  210 |                                 | NULL                |
+----+------+-----------+------+---------+------+---------------------------------+---------------------------+复制代码

能够看到alter table语句的状态为Waiting for table metadata lockthis

session 3 : 再次查询t1表spa

mysql> select * from t1;复制代码

也被阻塞了

mysql> show processlist;
+----+------+-----------+------+---------+------+---------------------------------+---------------------------+
| Id | User | Host      | db   | Command | Time | State                           | Info                      |
+----+------+-----------+------+---------+------+---------------------------------+---------------------------+
| 27 | root | localhost | test | Query   |  141 | Waiting for table metadata lock | alter table t1 add c2 int |
| 28 | root | localhost | test | Query   |    8 | Waiting for table metadata lock | select * from t1          |
| 29 | root | localhost | test | Query   |    0 | starting                        | show processlist          |
| 30 | root | localhost | test | Sleep   |  210 |                                 | NULL                      |
+----+------+-----------+------+---------+------+---------------------------------+-------------------复制代码

select * from t1 再次查询t1表也是 Waiting for table metadata lock状态,说明因为 metadata lock的存在,会致使后面正常的查询都会由于等待锁而阻塞

再查看当前事务运行状态:

mysql> select * from information_schema.innodb_trx\G
*************************** 1. row ***************************
               trx_id: 421408771166760
               trx_state: RUNNING
               trx_started: 2019-08-02 15:34:41
               trx_mysql_thread_id: 30复制代码

能够看到,session1的事务因为还没提交,因此这里能看到它的状态仍是running

这时咱们commit session1的事务,看看效果

session 1:

mysql> select * from t1;
+------+
| c1   |
+------+
|    1 |
+------+
1 row in set (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)复制代码

session 2:

mysql> alter table t1 add c2 int;
Query OK, 0 rows affected (30.51 sec)
Records: 0  Duplicates: 0  Warnings: 0复制代码

session 3:

mysql> select * from t1;
+------+
| c1   |
+------+
|    1 |
+------+
1 row in set (7.56 sec)复制代码

能够看到session1的事务提交后,session2 和session3 都正常执行了, 他们完成的时间分别是30秒和7秒

项目 autocommit 的设置

经过上面的还原测试,能够知道是因为事务没有提交而给表加了锁,致使后面alter语句由于等待锁而阻塞,从而影响后面的正常请求。那说明咱们的项目是默认开启了事务吗?继续排查,项目是使用flask-sqlchemy的插件来管理mysql接入,而后查了下文档在实例化sqlchemy的时候,会建立一个用于跟Mysql交互的session对象,看看源码

# db是这样使用的
db = SQLAlchemy()
db.__init__(app)
.... 

# 看看SQLAlchemy里面的session是怎么建立的
class SQLAlchemy(object):
    def __init__(self, app=None, use_native_unicode=True, session_options=None,
                 metadata=None, query_class=BaseQuery, model_class=Model,
                 engine_options=None):
        ...
        self.session = self.create_session(session_options)
        ... 

def create_session(self, options):
    ...
    return orm.sessionmaker(class_=SignallingSession, db=self, **options)

# session 使用到是SignallingSession 这个类 
class SignallingSession(SessionBase):
    ...
    def __init__(self, db, autocommit=False, autoflush=True, **options):
     ...复制代码

从 SignallingSession类的定义看来,autocommit=False,说明默认都给全部的sql执行开启事务,也就是说,哪怕是纯select语句,不须要加锁的select,咱们的项目默认也须要开启事务,这对于Mysql MVCC的版本控制来讲,是不必的。

解决办法:就是在实例化SQLAlchemy的时候,给一个参数,修改的session的autocommit=True:

db = SQLAlchemy(session_options={"autocommit": True})
db.__init__(app)复制代码

关于 table metadata lock

来自官网的介绍:

To ensure transaction serializability, the server must not permit one session to perform a data definition language (DDL) statement on a table that is used in an uncompleted explicitly or implicitly started transaction in another session. The server achieves this by acquiring metadata locks on tables used within a transaction and deferring release of those locks until the transaction ends.

意思就是为了保证事务的串行执行,而启用的一个锁,这个锁只会在事务结束的时候释放,所以在事务提交或回滚钱,任何对这个表作的DDL操做,都是会阻塞的

If the server acquires metadata locks for a statement that is syntactically valid but fails during execution, it does not release the locks early. Lock release is still deferred to the end of the transaction because the failed statement is written to the binary log and the locks protect log consistency.

这个 Metadata lock 是MySQL在5.5.3版本后引入了,为的是防止5.5.3之前的一个bug的出现:

当一个会话在主库执行DML操做还没提交时,另外一个会话对同一个对象执行了DDL操做如drop table,而因为MySQL的binlog是基于事务提交的前后顺序进行记录的,所以在从库上应用时,就出现Q了先drop table,而后再向table中insert的状况,致使从库应用出错。

总结

  • 为了事务的串行话,和数据一致性, Mysql会对打开事务进行DML的表加上table metadata lock, 在事务提交前,其余的DDL操做会阻塞
  • 对于主要是查询数据的项目来讲,默认不开启事务便可,若是确实须要,程序上手动开启事务
  • 须要使用到事务时,也要尽可能缩小事务的运行时间,一个事务中不要包含太多的语句
  • 程序上对任何错误异常情况必定要捕捉后,回滚事务,不然事务脱离程序,只能等事务本身超时,手动关闭事务或者重启服务释放锁了

关于我

若是文章对你有收获,能够收藏转发,这会给我一个大大鼓励哟!另外能够关注我公众号【码农富哥】 (coder2025),我会持续输出原创的算法,计算机基础文章!

相关文章
相关标签/搜索