flush table with write lock实现MDL事务锁

栏目: 数据库 · 发布时间: 7年前

内容简介:对于分布式场景下的spider集群主备切换 , flush tables with read lock已经不能保证数据的一致性了,为此需要实现一种事务级的锁,保证分布式场景下的主备安全切换。本文依次通过三种思路实现事务锁,不断优(踩)化(坑)。最终根据选型3的思路,实现MDL事务锁。spider是IEG DBA提供的MySQL分布式关系型数据库解决方案,主要特点包括:透明分库分表、高可用的MySQL集群服务,透明及在线的扩容及缩容;使得开发者可以仅专注于业务逻辑的开发及运营,无需编写数据分片逻辑,在海量用户

对于分布式场景下的spider集群主备切换 , flush tables with read lock已经不能保证数据的一致性了,为此需要实现一种事务级的锁,保证分布式场景下的主备安全切换。本文依次通过三种思路实现事务锁,不断优(踩)化(坑)。最终根据选型3的思路,实现MDL事务锁。

一.  痛点

1. spider集群简介

spider是IEG DBA提供的 MySQL 分布式关系型数据库解决方案,主要特点包括:透明分库分表、高可用的MySQL集群服务,透明及在线的扩容及缩容;使得开发者可以仅专注于业务逻辑的开发及运营,无需编写数据分片逻辑,在海量用户并发情况下,也无须关心DB存储层的负载压力。spider集群架构图如下:

flush table with write lock实现MDL事务锁

2. spider集群主备切换

目前业务主要使用 flush table with read lock(ftwrl) 来实现 spider 集群的主备切换。具体流程为

1.update mysql.server
2.ftwrl后flush privileges
3.unlock tables

对于不开启事务的场景,上述步骤是没有问题的。如果是事务场景呢?

session A:
begin;
insert into table_1(a)  values(1);
update_1 : update table_1 set a=a+1;

session B:
update mysql.servers set Port=Port_dr;
flush table with read lock;
flush privileges;

此时A如果进行DML操作,也不能commit。

session B:
unlock tables;

session A:
update_2 : update table_1 set a=a+1;
commit;

上述场景 update_1 在 gamedb_1 上执行, update_2 在 dr_1 上执行。

对于不开启事务, update_1 执行完, gamedb_1 已经将 binlog 数据同步给 dr_1 ,没有问题,最后 a=2

对于普通事务, update_1 执行完,没有提交,数据不会同步到 dr_1 ,最后 a=1

对于分布式事务, 无法正常的提交

二. 技术选型

在上述事务切换场景中, ftwrl 的作用是阻止事务的 提交 。而我们的需求是实现一种操作: flush table with write lock ,阻止事务的 开启 。即:保证其他事务结束后,才能加锁成功。加锁成功后,阻止新的事务开启。经过实践与比较,最终采用选型3的思路,实现了事务锁。

选型1--基于互斥锁实现事务锁

a.所有事务在开始,都需要获得S锁;
b.在主备切换前,执行flush table  with write lock,尝试加X锁。如果没加成功,通过条件变量和互斥锁维护一种状态,阻止其他线程获得S锁,避免X锁饿死
c.主备互切结束,执行unlock tables,释放写锁。

不足:

S锁释放后,通知各个等待的线程是广播行为,并不能保证先到先得。

改进:

可以借鉴MDL的实现原理,保证锁资源先到先得

选型2--基于MDL锁,改变DML select语义

关于MDL锁的基础知识可以了解上一篇文章: 浅析MYSQL MDL锁 。 首先思考一个问题:为什么 update  执行后可以 ftwrl 呢? 跟踪源码发现, update 操作刚开始加的是 语句级 的 MDL IX锁 ,语句执行完就释放,后面加的事务级的锁并不会阻塞 ftwrl 的 MDL S锁 。如果将 update 的 MDL IX锁 ,改为 事物级的 ,那是不是就可以阻塞 ftwrl 了。

基于这一点考虑,可以参考了 ftwrl 的实现原理,

但是如何让事务中的 select 操作阻塞 ftwrl 呢?

是不是通过 flush table with write lock  , 改变 DML select 的语义呢?

1.让flush table with write lock,获得global EXPILICT MDL X锁。
2.将DML操作的语句级 MDL IX锁,改为事务级的
3.select操作开始时,加一种只于MDL X锁互斥的global锁:SWITCH_IS

SWITCH_IS 的兼容性如下:

object 上已持有锁和请求锁的兼容性矩阵, grant_matrix :


 

| Type of active |

Request | scoped lock |

type | IS(*) IX S X SWITCH_IS |

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

IS | + + + + + |

IX | + + - - + |

S | + - + - + |

X | + - - - - |

SWITCH_IS | + + + - + |

object 上等待锁和请求锁的优先级矩阵 , wait_matrix :


 

| Pending |

Request | scoped lock |

type | IS(*) IX S X SWITCH_IS |

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

IS | + + + + + |

IX | + + - - + |

S | + + + - + |

X | + + + + - |

SWITCH_IS | + + + - + |

Here: "+" -- means that request can be satisfied
          "-" -- means that request can't be satisfied and should wait

不足:

这样设计,似乎可以实现需求。但是考虑下面的场景:

session A:
begin;
select *  from test;

session B:
flush table with write lock;------------hang住

此时 session B 会 hang 住,因为 session A 获得了 SWITCH_IS ,与 X 互斥, X 锁进入等待队列。如果此时session A执行 update test set c2=1 where id=1 , 会发生 死锁 。原因如下:

1.flush table with write lock的X锁等待session A释放SWITCH_IS
2.DML操作需要获得 global IX锁.
3.首先判断grant队列,发现只存在SWITCH_IS锁,gant矩阵中并不互斥
4.于是在判断wait队列,存在X锁。在wait矩阵中IX与X互斥,等待session B释放X,循环等待,导致死锁。

改进

虽然可以在上述场景中加条件判断避免死锁,但改变 DML SELECT 语义风险较大,可能引入其他问题,可以参考方案一:事物开启的时候加锁,事物结束释放锁。

选型3—基于MDL,实现事务锁

综合选型1和选型2的经验,增加两种彼此互斥的 MDL 锁: SWITCH_S 和 SWITCH_X

开始事务,加SWITCH_S,事务结束释放SWITCH_S
执行flush table with write lock加SWITCH_X,unlock tables释放SWITCH_X

3.1 锁粒度

为了将风险降到最低,选择粒度最小目标锁: USER_LOCK ,用法可以参考

https://dev.mysql.com/doc/internals/en/user-level-locks.html

3.2 兼容性

为了不影响其他锁, SWITCH_S 与 SWITCH_X 之间排斥,兼容其他锁

object 上已持有锁和请求锁的兼容性矩阵, grant_matrix :


 

Request | Granted requests for lock |

type | S SH SR SW SU SRO SNW SNRW X SWITCH_S SWITCH_X |

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

S | + + + + + + + + - + + |

SH | + + + + + + + + - + + |

SR | + + + + + + + - - + + |

SW | + + + + + - - - - + + |

SU | + + + + - + - - - + + |

SRO | + + + - + + + - - + + |

SNW | + + + - - + - - - + + |

SNRW | + + - - - - - - - + + |

X | - - - - - - - - - + + |

SU -> X | - - - - 0 - 0 0 0 + + |

SNW -> X | - - - 0 0 - 0 0 0 + + |

SNRW -> X | - - 0 0 0 0 0 0 0 + + |

SWITCH_S | + + + + + + + + + + - |

SWITCH_X | + + + + + + + + + - - |

object 上等待锁和请求锁的优先级矩阵 , wait_matrix :


 

Request | Pending requests for lock |

type | S SH SR SW SU SRO SNW SNRW X SWITCH_S SWITCH_X |

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

S | + + + + + + + + - + + |

SH | + + + + + + + + + + + |

SR | + + + + + + + - - + + |

SW | + + + + + + - - - + + |

SU | + + + + + + + + - + + |

SRO | + + + - + + + - - + + |

SNW | + + + + + + + + - + + |

SNRW | + + + + + + + + - + + |

X | + + + + + + + + + + + |

SU -> X | + + + + + + + + + + + |

SNW -> X | + + + + + + + + + + + |

SNRW -> X | + + + + + + + + + + + |

SWITCH_S | + + + + + + + + + + - |

SWITCH_X | + + + + + + + + + + + |

  Here: "+" -- means that request can be satisfied
           "-" -- means that request can't be satisfied and should wait

细心的你可能已经发现两个矩阵中 SWITCH_S 与 SWITCH_X 之间的相对关系和S和X的一致。

对于 grant 矩阵很好理解,一般的互斥锁都是这么设计的。对于 wait 矩阵:

a. 为什么 SWITCH_X 兼容 SWITCH_S ?

如果不兼容,考虑以下场景:

session A:begin;
session B:flush table with write lock;------hang住
session C:begin;----------------------------hang住

此时 session B 与 grant 矩阵中的 SWITCH_S 互斥,进入等待队列。 session C 与 wait 矩阵中的 SWITCH_X 互斥,进入等待队列。 session A 执行完 commit ,会尝试着唤醒等待队列的 SWITCH_X ,但是 SWITCH_X 与 wait 矩阵中的 SWITCH_S 互斥,无法得得到锁。同理 session C 也无法得到锁,会一直 hang 住。

b. 为什么 SWITCH_X 兼容 SWITCH_X ?为什么 SWITCH_S 兼容 SWITCH_S ?

如果不兼容,考虑如下场景:

session A:begin;
session B:flush table with write lock;------hang住
session C:flush table with write lock;------hang住

此时 session B 和 session C 都与 grant  矩阵中的 SWITCH_S 互斥,进入等待队列。 session A 执行完 commit ,会尝试着唤醒等待队列的 SWITCH_X ,但是 SWITCH_X 与 wait 矩阵中的 SWITCH_X 互斥,无法得得到锁。 session B 和 session C 无法得到锁,会一直 hang 住。仔细观察 wait 矩阵可以发现,所有的锁都与自己兼容。

3.3 锁持有时间

3.3.1 对于SWITCH_X:

只需一种类型: EXPLICIT

执行 flush table with write lock 加 EXPLICIT USER_LOCK
执行 unlock tables 释放锁

3.3.2 对于SWITCH_S:

分为 事务级 和 EXPLICIT

a.  需要事务级锁的场景:

  • 当 autocommit=1

    • 对于不显示指定 begin 的普通 sql

      在 sql_parse.cc 的 switch_commad() 前,增加 事务级锁 ,这里的事务级锁声明周期等同于   statement

    • 对于会隐式提交的 sql  如 DDL , flush tables

      会在隐式提交完,释放之前事务的锁后,加 事务级 锁

  • 当 autocommit=0 ,在隐式开始事务的函数,增加 事务级 锁

    void trans_register_ha(THD *thd, bool all, handlerton *ht_arg)

  • 显示指定 begin , 在开启事务的函数,增加 事务级 锁

    bool trans_begin(THD *thd, uint flags)

  • 对于 lock table test read/write :

    由于 lock 操作自身加的事务级的锁,但是执行 commit 并不会释放锁。只有执行 unlock 或者 begin 才会释放。所以给 lock 操作增加 事务级SWITCH_S锁

b. 需要EXPLICIT锁的场景:

  • 对于 sql:select get_lock("func","time") ;select release_lock("func")

    由于需要显示释放锁,所以需要 加EXPLICIT锁 ,对应函数:

    longlong Item_func_get_lock::val_int()

    longlong Item_func_release_lock::val_int()

  • 对于 sql:handler handler_table open; handler handler_table close;

    由于需要显示释放锁,所以需要 加EXPLICIT锁 ,对应函数:

    bool mysql_ha_open(THD *thd, TABLE_LIST *tables, SQL_HANDLER *reopen)

    static void mysql_ha_close_table(SQL_HANDLER *handler)


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。

编程之道

编程之道

杰弗雷﹒詹姆斯 / 清华大学出版社 / 1999-05 / 18.00元

本书出自美国一位善于进行哲学思考、有十多年工作经验的程序设计师——杰弗雷·詹姆斯之手,他以一种敏锐的眼光审视着发生在程序设计室里的各种各样的小故事,并利用古老的道家思想对其进行分析。简单的故事蕴含深奥的道理,是本书的最大特色。本书语言优美,比喻生动,可读性极强。一起来看看 《编程之道》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具