flush table with write lock实现MDL事务锁

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

内容简介:对于分布式场景下的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)


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

查看所有标签

猜你喜欢:

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

Java性能权威指南

Java性能权威指南

奥克斯 (Scott Oaks) / 柳飞、陆明刚、臧秀涛 / 人民邮电出版社 / 2016-3-1 / CNY 79.00

市面上介绍Java的书有很多,但专注于Java性能的并不多,能游刃有余地展示Java性能优化难点的更是凤毛麟角,本书即是其中之一。通过使用JVM和Java平台,以及Java语言和应用程序接口,本书详尽讲解了Java性能调优的相关知识,帮助读者深入理解Java平台性能的各个方面,最终使程序如虎添翼。 通过阅读本书,你可以: 运用四个基本原则最大程度地提升性能测试的效果 使用JDK中......一起来看看 《Java性能权威指南》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

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

UNIX 时间戳转换