InnoDB锁专题

锁是实现事务隔离性最广泛使用的技术。本文主要分享InnoDB中锁的设计与实现。

锁的定义

下面列举innodb支持的锁。

行级锁
共享锁:S锁,允许事务读一行数据
排他锁:X锁,允许事务删除或更新一行数据

XS
X不兼容不兼容
S不兼容兼容

X锁与任何的锁都不兼容,而S锁仅和S锁兼容。

注意:行锁实际上是索引记录锁,对索引记录的锁定。即使表没有建立索引,InnoDB也会创建一个隐藏的聚簇索引,并使用此索引进行记录锁定。

意图锁
意图锁定是表级锁定,标识事务稍后对表中的行做哪种类型的锁定(共享或独占)

意向共享锁(IS):事务想要获得一张表中某几行的共享锁
意向排他锁(IX):事务想要获得一张表中某几行的排他锁

意图锁遵循如下协议:
在事务获取表中某行的共享锁之前,它必须首先在表上获取IS锁或更强的锁。
在事务获取表中某行的独占锁之前,它必须首先在表上获取IX锁。

ISIXSX
IS兼容兼容兼容不兼容
IX兼容兼容不兼容不兼容
S兼容不兼容兼容不兼容
X不兼容不兼容不兼容不兼容

X锁与任何的锁都不兼容,S锁与IX锁不兼容,其他情况都是兼容的。

注意:意向锁只会阻塞表级别的锁(如LOCK TABLES请求的表锁),并不会阻塞行级锁(如行级X锁)。

间隙锁(Gap Lock)
行锁的以下三种算法
Record Lock:单个行记录上的锁
Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
Next-Key Lock:锁定记录以及记录前一个间隙

插入意向锁(Insert Intention Lock)
插入意图锁是在行插入之前通过INSERT操作设置的一种特殊间隙锁。
注意:多个事务插入同一个间隙的不同位置,他们并不会冲突。假设存在索引记录,其值分别为4和7。单独的事务分别尝试插入值5和6,在获得插入行的排他锁之前,每个事务都使用插入意图锁来锁定4和7之间的间隙, 但他们不会互相阻塞。
同样,不同事务请求同一个间隙的Gap锁并不会阻塞,但如果一个事务请求了Gap锁,另一个事务再请求插入意向锁,则会阻塞。

Gap锁和Next-Key锁只存在RR隔离级别下,RC隔离级别下并不使用这些锁。

Gap锁意义是什么?是为了解决幻读问题。
什么是幻读问题?一个事务中同一个SQL多次执行,结果集不同,就是多了一些记录。这违反了事务的隔离性,即当前事务能够看到其他事务的结果。
Gap锁的目的就是解决这个问题。它阻塞插入意向锁,阻止不适当的记录插入,避免幻读问题。
文章下面说到的加Gap锁或Next-Key锁的场景,大家思考一下通过这些锁是否可以解决幻读问题,就知道为什么要加Gap锁和Next-Key锁了。

自增锁
自增锁是事务插入到具有AUTO_INCREMENT列的表时的一种特殊表级锁。当一个事务将值插入表时,必须获取自增锁,以便获取自增列的值。
innodb_autoinc_lock_mode参数可以控制 auto-increment 锁定的算法。有兴趣的同学可以深入了解。

表锁
innodb还支持表锁,LOCK TABLES … WRITE可以获取指定表的X锁。LOCK TABLES … READ可以获取指定表的S锁。

锁的实现

行锁在InnoDB中的数据结构如下

typedef struct lock_rec_struct        lock_rec_t
struct lock_rec_struct{
    ulint space;    /*space id*/
    ulint page_no;  /*page number*/
    unint n_bits;   /*number of bits in the lock bitmap*/
}

InnoDB中根据页的组织形式进行锁管理,并使用位图记录锁信息。
n_bits变量表示位图占用的字节数,它后面紧跟着一个bitmap,bitmap占用的字节为:1 + (nbits-1)/8,bitmap中的每一位标识对应的行记录是否加锁。
因此,lock_rec_struct占用的实际存储空间为:sizeof(lock_rec_struct) + 1 + (nbits-1)/8。

思考:如何锁定一个间隙呢?
InnoDB通过在间隙的下一个记录添加Gap锁实现锁定一个间隙

表级锁的数据结构(用于表的意向锁和自增锁)

typedef struct lock_table_struct lock_table_t;
struct lock_table_struct {
    dict_table_t*          table;   /*database table in dictionary cache*/
    UT_LIST_NODE_T(lock_t) locks;   /*list of locks on the same table*/
}

而事务中关联如下锁结构

typedef struct lock_struct      lock_t;
struct lock_struct{
    trx_t*                  trx;        /* transaction owning the lock */
    UT_LIST_NODE_T(lock_t)  trx_locks;  /* list of the locks of the transaction */
    ulint                   type_mode;  /* lock type, mode, gap flag, and wait flag, ORed */
    hash_node_t             hash;       /* hash chain node for a record lock */
    dict_index_t*           index;      /* index for a record lock */
    union {
        lock_table_t    tab_lock; /* table lock */
        lock_rec_t      rec_lock; /* record lock */
    } un_member;
};

index变量指向一个索引,行锁本质是索引记录锁。
lock_struct是根据一个事务的每个页(或每个表)进行定义的。但一个事务可能在不同页上有多个行锁,trx_locks变量将一个事务所有的锁信息进行链接,这样就可以快速查询一个事务所有锁信息。
UT_LIST_NODE_T定义如下,典型的链表结构

#define UT_LIST_NODE_T(TYPE)
struct {
       TYPE *   prev;       /* pointer to the previous node,NULL if start of list */
       TYPE *   next;       /* pointer to next node, NULL if end of list */
}

lock_struct中type_mode变量是一个无符号的32位整型,从低位排列,第1字节为lock_mode,定义如下;

/* Basic lock modes */
enum lock_mode {
    LOCK_IS = 0,    /* intention shared */
    LOCK_IX,    /* intention exclusive */
    LOCK_S,     /* shared */
    LOCK_X,     /* exclusive */
    LOCK_AUTO_INC,  /* locks the auto-inc counter of a table
            in an exclusive mode */
    LOCK_NONE,  /* this is used elsewhere to note consistent read */
    LOCK_NUM = LOCK_NONE, /* number of lock modes */
    LOCK_NONE_UNSET = 255
};

第2字节为lock_type,目前只用前两位,大小为 16 和 32 ,表示 LOCK_TABLE 和 LOCK_REC,

#define LOCK_TABLE      16   
#define LOCK_REC        32     

剩下的高位 bit 表示行锁的类型record_lock_type

#define LOCK_WAIT   256        /* 表示正在等待锁 */
#define LOCK_ORDINARY 0     /* 表示 Next-Key Lock ,锁住记录本身和记录之前的 Gap*/
#define LOCK_GAP    512        /* 表示锁住记录之前 Gap(不锁记录本身) */
#define LOCK_REC_NOT_GAP 1024    /* 表示锁住记录本身,不锁记录前面的 gap */
#define LOCK_INSERT_INTENTION 2048    /* 插入意向锁 */
#define LOCK_CONV_BY_OTHER 4096        /* 表示锁是由其它事务创建的(比如隐式锁转换) */

另外,除了查询某个事务所有锁信息,系统还需要查询某个具体记录的锁信息。如记录id=3是否有锁?
而InnoDB使用哈希表映射行数据和锁信息

struct lock_sys_struct{
    hash_table_t* rec_hash;
}

每次新建一个锁对象,都要插入到lock_sys->rec_hash中。lock_sys_struct中的key通过页的space和page_no计算得到,而value则是锁对象lock_rec_struct。
因此若需查询某一行记录是否有锁,首先根据行所在页进行哈希查询,然后根据查询得到的lock_rec_struct,查找lock bitmap,最终得到该行记录是否有锁。
可以看出,根据页进行对行锁的查询并不是高效设计,但这种方式的资源开销非常小。某一事务对一个页任意行加锁开销都是一样的(不管锁住多少行)。因此也不需要支持锁升级的功能。
如果根据每一行记录进行锁信息管理,所需的开销会非常巨大。当一个事务占用太多的锁资源时,需要进行锁升级,将行锁升级为更粗粒度的锁,如页锁或表锁。
而现在InnoDB设计的方案并不需要锁升级。

加锁操作

下面列举几种常见场景下的加锁逻辑

插入
  1. 首先对表加上IX锁

  2. 唯一索引冲突检查:如果唯一索引上存在相同项,进行S锁当前读,读到数据则唯一索引冲突,返回异常,否则检查通过。

  3. 判断插入位置是否存在Gap锁或Next-Key锁,没有的话直接插入,有的话等待锁释放,并产生插入意向锁。

  4. 对插入记录的所有索引项加X锁

为了降低锁的开销,innodb采用了延迟加锁机制,即隐式锁(implicit lock)。
当有事务对某条记录进行修改时,需要先判断该行记录是否有隐式锁(原记录的事务id是否是活动的事务),如果有则为其真正创建锁并等待(隐式锁转换为显示锁),否则直接更新数据并写入自己的事务id(可以理解为加了隐式锁)。
二级索引虽然存储上没有记录事务id,但同样可以存在隐式锁,只不过判断逻辑复杂一些。有兴趣的同学可以深入了解。

插入操作第3步添加的插入意向锁和第4步添加的X锁都是先添加隐式锁(就是没有加锁),当发生锁冲突时,再转化为显示锁。

一致性锁定读,修改

一致性非锁定读:如果读取的行正在执行DELETE或UPDATE操作,读取操作不等待行上锁的释放,而去读行的一个快照数据。在之前事务篇已经分享过相关内容。
这里看一下一致性锁定读(就是当前读)和修改操作的加锁逻辑
(1) 查询命中结果

  • SELECT … FROM … LOCK IN SHARE MODE(S锁),SELECT … FROM … FOR UPDATE(X锁),UPDATE … WHERE … (X锁)语句在扫描命中的索引记录上加上next key锁。如果是唯一索引,只需要在相应记录上加index record lock。

  • 在辅助索引记录上加锁的语句,首先对辅助索引记录加next key锁,然后还要对聚集索引记录进行加锁record lock

  • 在辅助索引记录上加锁的语句,可能还需要对下一个记录进行加Gap锁,解决幻读问题。

(2) 查询未命中结果
如果sql查询没有命中结果,则对命中的间隙加Gap锁。

(3) 查询未使用索引
如果sql没有使用索引,只能走聚簇索引,对表中的记录进行全表扫描。

在RC隔离级别下会给所有记录加Record锁,在RR隔离级别下,对所有记录加Next-Key锁。

删除

删除操作需要和更新操作一样加锁,并且当purge真正删除记录操作完成后,如果删除记录上有Gap锁,则由下一个记录继承该锁,同时释放并重置删除记录上等待锁的信息。

死锁

死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种相互等待的现象。若无外力作用,他们都将无法推进下去。

解决死锁常用的两个方案:

  1. 超时机制,即两个事务互相等待时,当一个等待时间超过设置的某一阀值时,其中一个事务回滚,另一个事务继续执行。
    MySQL4.0版本开始,提供innodb_lock_wait_time用于设置等待超时时间。

  2. 等待图(wait-for graph)
    InnoDB通过锁的信息链表和事务等待链表,判断是否存在等待回路。如有,则存在死锁。
    每次加锁操作需要等待时都判断是否产生死锁,若有则回滚事务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值