浅析MySQL死锁检测

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
日志服务 SLS,月写入数据量 50GB 1个月
简介: MySQL发生死锁时,通过show engine innodb status;命令并不能看到事务中引起死锁的所有SQL语句。死锁排查起来就比较麻烦,需要查询events_statements_%表,来获取SQL,同时需要对业务也比较熟悉,这样能分析出造成死锁的语句

浅析MySQL死锁检测

MySQL发生死锁时,通过show engine innodb status;命令并不能看到事务中引起死锁的所有SQL语句。死锁排查起来就比较麻烦,需要查询events_statements_%表,来获取SQL,同时需要对业务也比较熟悉,这样能分析出造成死锁的语句。
本着探究的目的,来看下MySQL死锁检测实现及为何无法打印出触发死锁的所有SQL语句。

Lock bitmap

截取show engine innodb status;命令查询锁信息时一段内容:

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 124 page no 4 n bits 80 index idx_id of table `dhy`.`t` trx id 45909 lock_mode X waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
 0: len 4; hex 80000002; asc     ;;
 1: len 6; hex 000000000601; asc       ;;

space id、page no、n bits(lock_rec_t结构体) 通过这三个可以定位到某一条记录,这里对应的就是heap_no = 2这条。n_bits是一个bitmap,用来记录行记录上是否持有Lock。
lock_t 结构用来描述锁信息的,通过n_bits可以将lock(lock_t类型的变量,下文中都将这样描述)与行记录的对应起来。lock_rec_t结构如下:

struct lock_rec_t {
    ib_uint32_t    space;        /*!< space id */
    ib_uint32_t    page_no;    /*!< page number */
    ib_uint32_t    n_bits;        /*!< number of bits in the lock
                    bitmap; NOTE: the lock bitmap is
                    placed immediately after the
                    lock struct */ 

    /** Print the record lock into the given output stream
    @param[in,out]    out    the output stream
    @return the given output stream. */
    std::ostream& print(std::ostream& out) const;
};

n_bits 的大小分配为 : (1+ (记录锁+ 64) / 8) * 8


rec_lock.n_bits = static_cast<uint32_t>(8 * size);

size大小:
static size_t lock_size(const page_t* page)
    {
        ulint    n_recs = page_dir_get_n_heap(page);

        /* Make lock bitmap bigger by a safety margin */

        return(1 + ((n_recs + LOCK_PAGE_BITMAP_MARGIN) / 8));
    }

这么大的内存空间分配在lock内存之后的:

ulint        n_bytes = size + sizeof(*lock);
mem_heap_t*    heap = trx->lock.lock_heap;

lock = reinterpret_cast<lock_t*>(mem_heap_alloc(heap, n_bytes));

在lock_rec_set_nth_bit中,设置bitmap位图信息:

lock_rec_set_nth_bit(
/*=================*/
    lock_t*    lock,    /*!< in: record lock */
    ulint    i)    /*!< in: index of the bit */
{
    ulint    byte_index; 
    ulint    bit_index;

    byte_index = i / 8; //标识第几个字节 
    bit_index = i % 8; //标识字节中的第几位

    ((byte*) &lock[1])[byte_index] |= 1 << bit_index; //将对应byte上相应的bit位设置为1

    ++lock->trx->lock.n_rec_locks;
}

创建好的lock都会被添加到HASH表中,space_no与page_no相同的lock会被分配到同一个HASH桶中

ulint    key = m_rec_id.fold();

++lock->index->table->n_rec_locks;

HASH_INSERT(lock_t, hash, lock_hash_get(m_mode), key, lock);


/**
    @return the "folded" value of {space, page_no} */
ulint fold() const
{
    return(m_fold);
}
    
        

判断lock上bitmap对应的bit位是否存在锁:

UNIV_INLINE
ibool
lock_rec_get_nth_bit(
/*=================*/
    const lock_t*    lock,    /*!< in: record lock */
    ulint        i)    /*!< in: index of the bit */
{
    const byte*     b;


    if (i >= lock->un_member.rec_lock.n_bits) {

        return(FALSE);
    }

    b = ((const byte*) &lock[1]) + (i / 8);  //根据i(也就是heap_no)计算出是lock之后的第几个字节

    return(1 & *b >> (i % 8)); //判断对应的bit位上是否为1
}

死锁检测

先介绍几个重要点:

  • 变量:

const lock_t* m_wait_lock; // 想持有的锁
const trx_t* m_start // 直译过来是:正在以不兼容模式请求锁定的联接事务,举例说明下比较好理解:

session1 session2
begin; begin;
lock:a
lock:b
lock:b
lock:a

m_start 就是对应session2这个事务

ulint heap_no // 记录对应的物理位置号

  • 函数get_first_lock // 获取m_wait_lock对应记录上的第一个lock, 例如上面例子中,就是获取session1中对a这条记录持有的lock信息。

    • 精简后流程如下:
DeadlockChecker::get_first_lock(ulint* heap_no) const
{

    const lock_t*    lock = m_wait_lock;

    if (lock_get_type_low(lock) == LOCK_REC) {
        hash_table_t*    lock_hash;

        lock_hash = lock->type_mode & LOCK_PREDICATE
            ? lock_sys->prdt_hash
            : lock_sys->rec_hash;

        /* We are only interested in records that match the heap_no. */
        *heap_no = lock_rec_find_set_bit(lock); // 查找lock对应的heap_no


        /* Find the locks on the page. */
        lock = lock_rec_get_first_on_page_addr(
            lock_hash,
            lock->un_member.rec_lock.space,
            lock->un_member.rec_lock.page_no); //找出page上的第一个lock

        /* Position on the first lock on the physical record.*/
        if (!lock_rec_get_nth_bit(lock, *heap_no)) { //如果lock的bitmap对应bit位上存在锁,则返回lock,否则查找下一个lock,直到对应bit位上存在锁
            lock = lock_rec_get_next_const(*heap_no, lock);
        }

    } else {
        /* Table locks don't care about the heap_no. */
        *heap_no = ULINT_UNDEFINED;
        dict_table_t*    table = lock->un_member.tab_lock.table;
        lock = UT_LIST_GET_FIRST(table->locks);
    }

    return(lock);
}

死锁检测核心函数在DeadlockChecker::search()中,会做以下处理:

  1. 首先获取m_wait_lock对应记录上的第一个lock
const lock_t*    lock = get_first_lock(&heap_no);
  1. 进入一个循环,这里代码比较多,直接贴代码不太直观,放一张流程图

1.png

流程较长,举个例子对照上图看下:

session1 session2
begin; begin;
lock:a
lock:b
lock:b //blocking
lock:a

进入死锁检测时:

  • m_start:session2事务信息
  • lock:m_wait_lock对应记录上的第一个lock,对应session1对a记录持有的锁
  • m_wait_lock:session2中对a想要持有的锁

2.png

进入循环后,只满足lock->trx->lock.que_state == TRX_QUE_LOCK_WAIT这个条件(也代表m_wait_lock和lock是发生锁等待了),步骤7中将m_wait_lock被赋值后即为session2中对b想要持有的锁,lock变为session2对b持有的锁,再次进入循环。
这时lock->trx == m_start(都为session2),即检测出死锁。如下图所示:
3.png

死锁日志

死锁日志只能看到事务中最后一个SQL语句,因为每次执行完语句后m_query_string变量都会被reset_query(),要实现就需要一个SQL语句和lock的对应关系,将每次执行的SQL保留起来。
这块还涉及到死锁日志的一个参数:

  • innodb_print_all_deadlocks :会将死锁信息打印到errorlock中,最好将此参数设置下,能够保留死锁日志,方便查看因为show engine innodb status;只会保留最后一个死锁日志的信息,原因是mysql会在tmp目录下创建一个ib开头的临时文件,每次重启后都会重建。

结语

这里梳理了下死锁检测的流程,由于水平有限,文章可能存在不正确地方,望指正。

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
3月前
|
SQL 关系型数据库 MySQL
MySQL死锁及源码分析!
MySQL死锁及源码分析!
MySQL死锁及源码分析!
|
3月前
|
SQL 关系型数据库 MySQL
案例剖析:MySQL唯一索引并发插入导致死锁!
案例剖析:MySQL唯一索引并发插入导致死锁!
251 0
案例剖析:MySQL唯一索引并发插入导致死锁!
|
3月前
|
SQL 关系型数据库 MySQL
案例剖析,MySQL共享锁引发的死锁问题!
案例剖析,MySQL共享锁引发的死锁问题!
|
28天前
|
存储 关系型数据库 MySQL
从新手到高手:彻底掌握MySQL表死锁
通过本文的介绍,希望你能深入理解MySQL表死锁的概念、原因、检测方法及解决方案,并在实际开发中灵活应用这些知识,提升系统的稳定性和性能。
193 9
|
5月前
|
SQL 关系型数据库 MySQL
遇到mysql数据库死锁,你会怎么排查?
遇到mysql数据库死锁,你会怎么排查?
375 0
|
2月前
|
SQL 算法 关系型数据库
面试:什么是死锁,如何避免或解决死锁;MySQL中的死锁现象,MySQL死锁如何解决
面试:什么是死锁,死锁产生的四个必要条件,如何避免或解决死锁;数据库锁,锁分类,控制事务;MySQL中的死锁现象,MySQL死锁如何解决
|
3月前
|
关系型数据库 MySQL 数据库
一个 MySQL 数据库死锁的案例和解决方案
本文介绍了一个 MySQL 数据库死锁的案例和解决方案。
216 3
|
4月前
|
监控 关系型数据库 MySQL
MySQL锁机制与解决死锁问题
MySQL锁机制与解决死锁问题
369 5
|
3月前
|
监控 关系型数据库 MySQL
一次彻底讲清如何处理mysql 的死锁问题
【10月更文挑战第16天】本文详细介绍了如何处理 MySQL 中的死锁问题,涵盖死锁的概念、原因、检测方法及解决策略,强调通过优化事务设计、调整数据库参数、手动处理和预防措施等手段,有效减少死锁,提升数据库性能与稳定性。
595 0
|
5月前
|
存储 SQL 关系型数据库
深入MySQL锁机制:原理、死锁解决及Java防范技巧
深入MySQL锁机制:原理、死锁解决及Java防范技巧

推荐镜像

更多