Innodb是mysql数据库中目前最流行的存储引擎,innodb相对其它存储引擎一个很大的特点是支持事务,并且支持行粒度的锁。今天我重点跟大家分享下innodb行锁实现的基础知识。由于篇幅比较大,文章会按如下的目录结构展开。

{
  innodb锁结构
  锁机制关键流程
  innodb行锁开销
  innodb锁同步机制
  innodb等待事件实现
}

先从一个简单的例子说起,如下表1

时间轴

A用户(T1)

B用户(T2)

t1

select * from t where id=1 for update

t2

select * from t where id=1 for update

t3

挂起状态

t4

commit

t5

执行成功

表1

t1时刻A用户获得表t中id为1这条记录的排它锁,那么当t2时刻B用户再请求该记录的排它锁时,则需要等待;t4时刻A用户提交事务后,则B用户立即也执行成功。这个简单例子的背后有几个问题需要我们思考,第一,innodb如何挂起B用户的执行线程的;第二,用户B又如何在A用户提交事务后,立即执行成功返回的。上面例子本质上是innodb使用锁达到了A用户和B用户有序操作id为1这条记录的目的,下文会详细介绍这个实现过程,同时会介绍锁相关的一些基础知识。

1. Innodb锁结构

Innodb锁结构通过lock_sys管理,所有的行锁lock_t对象都插入hash表中,通过维护hash表,来管理行锁对象,hash表的key值通过页号(space_id,page_no)计算得到。

1)  锁系统结构图

2) 重要数据结构

 lock_sys
{
hash_table_t* rec_hash; //行锁hash表
srv_slot_t* waiting_threads; //等待对象数组
} lock_rec_t
{
ulint space; //表空间编号
ulint page_no; //数据页编号
ulint n_bits; //数据页包含的记录
byte bitmap[+n_bits/] //bitmap数组
};

2.关键流程

1) 创建锁【lock_rec_create】

a)计算页面中的记录数目,

b)按每个记录一个bit存储,计算需要的存储空间

c)申请lock_t的存储空间

d)初始化bitmap,将heap_no对应的bit位置1,表示上锁

e)将锁对象指针插入hash链表

f)将锁对象插入到事务的锁链表

2) 查询某一个记录上锁情况:(是否上锁,锁类型)

a) 获取记录信息: (space_id,page_no),和heap_no

b) 根据(space_id,page_no)查找hash表,获取锁对象lock _t

c) 根据锁对象内容,判断是共享锁还是排它锁

d) 若存在,遍历锁对象的bitmap,确定heap_no对应的位是否为1。

e) 为1,表示已经加锁

3) 上行锁

a) 查找hash表,判断页面上是否有锁

b) 若不存在,则创建锁,将锁对象插入hash链表

c) 若存在,判断是否事务已有更强的锁存在 (lock_rec_has_expl)

d) 若是,跳转5,若不是,跳转6(lock_rec_lock_slow)

e) 根据页面的heap_no设置bit位,结束。

f) 判断请求锁是否有锁冲突

g)若是,创建锁(模式LOCK_WAIT),设置wait_lock (lock_rec_enqueue_waiting)

h)若不是,上锁成功,加入锁队列(lock_rec_add_to_queue)

i) 上层调用根据返回的错误码,调用锁等待逻辑(lock_wait_suspend_thread)

4) 锁等待【lock_wait_suspend_thread】

  a) 根据工作线程信息获取事务信息;

  b) 申请slot节点(lock_wait_table_reserve_slot),初始化等待事件;

  c) 设置等待事件(linux中通过条件变量实现),将线程挂起

调用堆栈
# pthread_cond_wait
# os_cond_wait(pthread_cond_t*, os_fast_mutex_t*) ()
# os_event_wait_low(os_event*, long) ()
# lock_wait_suspend_thread(que_thr_t*) ()
# row_mysql_handle_errors(dberr_t*, trx_t*, que_thr_t*, trx_savept_t*) ()

5) 释放锁

innodb的行锁在事务提交或回滚后才释放。释放锁后,会检查是否有等待该锁的锁对象,若有,则将其释放,唤醒对应的线程。

a) 提取锁类型为LOCK_WAIT锁,判断是否需要继续等待。

b) 若不需要等待,则授权lock_grant

c) 根据锁对象找到找到对应的事务(lock_t->trx)信息,

d) 通过事务找到对应的工作线程(trx_lock_t->wait_thr)信息

e) 通过thr信息找到对应的slot(等待事件)

f) 调用os_event_set触发事件

调用堆栈
# os_event_set(thr->slot->event);
# lock_wait_release_thread_if_suspended
# lock_grant
# lock_rec_dequeue_from_page
# lock_trx_release_locks 

6) slot的管理

锁等待通过slot对象上的等待事件event实现(下文会讲),每个slot对象包含一个等待事件,slot个数与运行的线程相关。因为阻塞的主体是线程,因此只需要初始化与最大线程数目相同的slot节点即可。slot信息存储在lock_sys的waiting_threads中。需要slot时,从数组中获取。

slot初始化
lock_sys = static_cast<lock_sys_t*>(mem_zalloc(lock_sys_sz));
lock_stack = static_cast<lock_stack_t*>(
mem_zalloc(sizeof(*lock_stack) * LOCK_STACK_SIZE));
void* ptr = &lock_sys[];
lock_sys->waiting_threads = static_cast<srv_slot_t*>(ptr); 

3. innodb行锁开销

innodb行锁采用位图存储,理论上一个记录只需要一个bit位。锁的基本单位是行,但锁是通过事务和页来进行管理和组织,创建锁的实例是lock_t,一个lock_t实例对应于一个索引页面的所有记录。

1) 行锁代价计算

内存开销主要来源于指针和存储锁信息的bitmap。bitmap中的一个bit对应page的一条记录,一个200条记录的Page,一个行锁对象大小约为 100bytes。若页面只锁一行,代价为100byte/行,而如果所有记录公用一把锁,则代价为100byte/200=4bit/行。实际情况下,只有当同一个事务锁住了页面的所有记录,并且锁模式相同,才可能保证一个页面只有一把锁。

一个lock_t对象占用的内存空间
1 /* Make lock bitmap bigger by a safety margin */
n_bits = page_dir_get_n_heap(page) + LOCK_PAGE_BITMAP_MARGIN;
n_bytes = + n_bits / ;
lock = static_cast<lock_t*>(
mem_heap_alloc(trx->lock.lock_heap, sizeof(lock_t) + n_bytes));

2) 锁重用

innodb锁机制利用锁重用方式,保证锁的内存开销尽可能小。具体而言,同一个事务锁住同一个页面的记录,并且锁模式相同; 同一个事务,对于同一条记录,已有的锁强于请求的锁模式,这两种情况下都不需要重新创建锁对象。

4. Innodb锁同步机制(spinlock+mutex+条件变量)

innodb没有直接采用原生的同步方式比如spinlock,mutex或是条件变量实现,而是将几种方式进行融合,达到最优的目的。主要函数的实现在于mutex_enter_func和mutex_exit两个函数。

1) 数据结构

ib_mutex_t
{
os_event_t event; //等待事件
volatile lock_word_t lock_word; //锁变量
os_fast_mutex_t os_fast_mutex; //不支持原子锁系统,使用互斥量
ulint waiters; //是否有等待线程
}

2) 获取互斥量流程【mutex_enter_func(ib-mutex)】

a) 首先进行自旋,检查mutex->lock_word,判断是否可以获得该锁

b) 对于不支持spinlock的系统,采用pthread_mutex_trylock方式,利用os_fast_mutex保护mutex->lock_word,判断是否可以获得该锁

c) 若不能获得,则从全局变量 sync_wait_array分配一个cell,并将cell的wait_object设置为ib-mutex

d) 将ib-mutex的waiters设为1

e) 调用os_event_wait_low(ib-mutex->event),将线程挂起

f) 获得信号量后,线程跳转步骤a)重新开始执行。

3) 释放互斥量流程【mutex_exit_func(ib-mutex)】

a) 重置mutex->lock_word,

b) 对于自旋锁,通过os_atomic_test_and_set_byte设置

c) 对于不支持自旋锁的系统,释放os_fast_mutex,将lock_word设置为0

d) 判断ib-mutex对象waiters是否为1(是否有线程挂起)

e) 调用mutex_signal_object(ib-mutex->event)

f) 调用pthread_cond_broadcast(event->cond)唤醒所有等待的线程

5. innodb等待事件实现

1) event的结构

os_event
{
os_cond_t cond_var; //条件变量
ibool is_set; //为ture时,线程不会阻塞在事件上
os_fast_mutex_t os_mutex; //保护条件变量的互斥量
}

2) os_event_set 流程

a) 获取互斥量os_mutex

b) 若is_set为true,什么也不做,释放os_mutex

c) 若is_set为false,设置is_set为true

d) 调用pthread_cond_broadcast广播条件变量,唤醒所有等待线程

3) os_event_wait 流程

a) 获取互斥量os_mutex

b) 判断is_set为true,则什么也不做,释放os_mutex

c) 若is_set为false,调用pthread_cond_wait,将自己挂起等待

d) 被唤醒后,释放互斥量os_mutex

回到文章开始提到的问题,假设表t,id=1的记录所在的页面为(1,20),如图2所示,则锁节点可以红色的框表示,一个节点表示一个锁对象。另外,事务T2和T3已经在页面(0,200)上了2把锁,这里解释下,为啥同一个页面有2把锁。这是因为,锁对象的拥有者不同。不同事务即使是对同一条记录上同样模式的锁,也需要分别创建一个锁对象,所谓的锁重用是针对同一个事务锁同一个页面的多个记录而言。若T1也需要对(0,200)上锁,若上锁的记录与已有锁冲突,则创建锁,并挂起等待;否则,创建锁,返回成功。

Innodb行锁源码学习(一)的更多相关文章

  1. 从源码学习Java并发的锁是怎么维护内部线程队列的

    从源码学习Java并发的锁是怎么维护内部线程队列的 在上一篇文章中,凯哥对同步组件基础框架- AbstractQueuedSynchronizer(AQS)做了大概的介绍.我们知道AQS能够通过内置的 ...

  2. Java并发包源码学习系列:ReentrantLock可重入独占锁详解

    目录 基本用法介绍 继承体系 构造方法 state状态表示 获取锁 void lock()方法 NonfairSync FairSync 公平与非公平策略的差异 void lockInterrupti ...

  3. Redisson分布式锁学习总结:可重入锁 RedissonLock#lock 获取锁源码分析

    原文:Redisson分布式锁学习总结:可重入锁 RedissonLock#lock 获取锁源码分析 一.RedissonLock#lock 源码分析 1.根据锁key计算出 slot,一个slot对 ...

  4. muduo网络库源码学习————互斥锁

    muduo源码的互斥锁源码位于muduo/base,Mutex.h,进行了两个类的封装,在实际的使用中更常使用MutexLockGuard类,因为该类可以在析构函数中自动解锁,避免了某些情况忘记解锁. ...

  5. Java并发包源码学习之AQS框架(四)AbstractQueuedSynchronizer源码分析

    经过前面几篇文章的铺垫,今天我们终于要看看AQS的庐山真面目了,建议第一次看AbstractQueuedSynchronizer 类源码的朋友可以先看下我前面几篇文章: <Java并发包源码学习 ...

  6. 源码学习之ASP.NET MVC Application Using Entity Framework

    源码学习的重要性,再一次让人信服. ASP.NET MVC Application Using Entity Framework Code First 做MVC已经有段时间了,但看了一些CodePle ...

  7. Spring源码学习-容器BeanFactory(三) BeanDefinition的创建-解析Spring的默认标签

    写在前面 上文Spring源码学习-容器BeanFactory(二) BeanDefinition的创建-解析前BeanDefinition的前置操作中Spring对XML解析后创建了对应的Docum ...

  8. Netty源码学习系列之4-ServerBootstrap的bind方法

    前言 今天研究ServerBootstrap的bind方法,该方法可以说是netty的重中之重.核心中的核心.前两节的NioEventLoopGroup和ServerBootstrap的初始化就是为b ...

  9. Spring5.0源码学习系列之事务管理概述

    Spring5.0源码学习系列之事务管理概述(十一),在学习事务管理的源码之前,需要对事务的基本理论比较熟悉,所以本章节会对事务管理的基本理论进行描述 1.什么是事务? 事务就是一组原子性的SQL操作 ...

随机推荐

  1. 用T4 Template生成代码

    1 T4语法 T4的语法与ASP.NET的方式比较类似.主要包括指令.文本块.控制块. 1.1    指令 指令主要包括template, output, assembly, import, incl ...

  2. Adaptive Backgrounds – jQuery 自适应背景插件

    Adaptive Backgrounds 是一款很特别的 jQuery 插件,可以从图像中提取主导颜色并将它应用到它的父元素.这个插件利用 Canvas 元素和 ImageData 对象.需要注意的是 ...

  3. HTML5 Canvas 实现的9个 Loading 效果

    Sonic.js 是一个很小的 JavaScript 类,用于创建基于 HTML5 画布的加载图像.更强大的是 Sonic.js 还提供了基于现成的例子的创建工具,可以帮助你实现更多自定义的(Load ...

  4. 类似 Dribbble 下载按钮的 SVG 弹性动画进度条

    Codrops 发布了一个如何创建一个基于弹性效果的 SVG 加载进度条教程,基于 SVG 和 TweenMax 实现.按钮开始的时候是一个带有箭头的图标,一旦它被点击,动画成一个有趣的小金属丝和一个 ...

  5. Quill – 可以灵活自定义的开源的富文本编辑器

    Quill 的建立是为了解决现有的所见即所得(WYSIWYG)的编辑器本身就是所见即所得(指不能再扩张)的问题.如果编辑器不正是你想要的方式,这是很难或不可能对其进行自定义以满足您的需求. Quill ...

  6. Captain Icon – 350+ 有趣的矢量图标免费下载

    Captain Icon 是一套一个惊人的免费图标集,包含350+有趣的矢量图标,可以缩放到任意大小而不会降低质量.图标的类别很丰富,有设计,体育,社会,天气等很多类别.提供 EPS.PSD.PNG. ...

  7. C#仿google日历asp.net简单三层版本

    网上搜了很多xgcalendar的例子都是Php开发的,而且官方站上的asp.net/MVC版 在vs10 08 都报错. 所以自己重新用三层写了一下希望对大家有帮助 废话不多说了 先看看它都有些什么 ...

  8. Request.MapPath和ServerMapPath

    一.路径 / 念 反斜杠,/ 是超文本协议的路径分隔符号,所有的网站在浏览器中显示的路径分隔都是以"/"表示.它一般代表虚拟路径. \ 念 斜杠,在普通程序代码中则以"\ ...

  9. 一次失败的APP业务渗透测试

    作者:whoamiecho 来源:ichunqiu 本文参加i春秋社区原创文章奖励计划,未经许可禁止转载! 一.  过程 1.1.事情起因:暴力破解 测试给了个普通用户账号,可以登录.APP一来就要登 ...

  10. SharePoint Error - The SharePoint server was moved to a different location.

    错误 The SharePoint server was moved to a different location. ( Error from SharePoint site: HttpStatus ...