锁的基本属性
InnoDB 的 lock_t
结构体定义了锁的基本属性,无论是在表锁还是行锁的情况下都适用。
lock_t
结构体
部分源码
/**
* 锁结构;受 lock_sys 互斥锁保护
*/
struct lock_t {
/** 拥有锁的事务 */
trx_t *trx;
/** 事务拥有的锁的链表节点 */
UT_LIST_NODE_T(lock_t) trx_locks;
/** 对于记录锁来说,索引对象指针 */
dict_index_t *index;
/** 用于记录锁的哈希链表节点。
* 哈希表中使用的单向链表中的节点。 */
lock_t *hash;
/** 联合体,用于存储表锁或记录锁的不同属性 */
union {
/** 表锁 */
lock_table_t tab_lock;
/** 记录锁 */
lock_rec_t rec_lock;
};
#ifdef HAVE_PSI_THREAD_INTERFACE
#ifdef HAVE_PSI_DATA_LOCK_INTERFACE
/** 创建锁的性能模式线程 */
ulonglong m_psi_internal_thread_id;
/** 创建锁的性能模式事件 */
ulonglong m_psi_event_id;
#endif /* HAVE_PSI_DATA_LOCK_INTERFACE */
#endif /* HAVE_PSI_THREAD_INTERFACE */
/** 锁的类型和模式位标志。
* LOCK_GAP 或 LOCK_REC_NOT_GAP, LOCK_INSERT_INTENTION, 等待标志,按位或运算。
*/
uint32_t type_mode;
#if defined(UNIV_DEBUG)
/** 创建锁的时间戳 */
uint64_t m_seq;
#endif /* UNIV_DEBUG */
/** 解锁 Next-Key Lock 中的间隙锁部分
*
* 该方法仅在 Next-Key Lock 上调用,用于解锁其中的间隙锁部分,
* 保持对记录本身的锁定。
*/
void unlock_gap_lock() {
ut_ad(!is_gap()); // 断言不是间隙锁
ut_ad(!is_insert_intention()); // 断言不是插入意向锁
ut_ad(is_next_key_lock()); // 断言是 Next-Key Lock
// 设置非间隙记录锁标志位
type_mode |= LOCK_REC_NOT_GAP;
}
/** 判断锁对象是否为记录锁
* @return 如果是记录锁返回 true,否则返回 false */
bool is_record_lock() const { return (type() == LOCK_REC); }
// ...
}
成员变量
trx
: 指向持有锁的事务对象。在MySQL中,无论是DDL(数据定义语言)还是DML(数据操纵语言)操作,都是在事务的上下文中进行的。因此,每个锁结构都关联着一个事务对象,表示加锁操作是由该事务发起的。trx_locks
: 指向该事务持有的所有锁的链表。这样设计可以方便地遍历和管理一个事务持有的所有锁。index
: 对于记录锁,指向索引对象。hash
: 对于记录锁,指向哈希链表中的节点。union
: 包含表锁和行锁的不同属性。tab_lock
: 表锁属性。rec_lock
: 行锁属性。
m_psi_internal_thread_id
: 性能监控线程ID,用于性能监控接口。m_psi_event_id
: 性能监控事件ID,用于性能监控接口。type_mode
: 锁的类型和模式,包括锁类型、模式、等待状态和精确模式。m_seq
: 创建锁的时间戳,用于调试。
type_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
};
MySQL中的锁主要分为以下几种模式:
- 意向共享锁(LOCK_IS):表级别的锁,表示事务意图在表中的某些行上加共享锁。
- 意向排他锁(LOCK_IX):表级别的锁,表示事务意图在表中的某些行上加排他锁。
- 共享锁(LOCK_S):表级别或行级别的锁,多个事务可以同时读取同一资源。
- 排他锁(LOCK_X):表级别或行级别的锁,只有一个事务可以写入资源,其他事务必须等待锁释放。
- 自增锁(LOCK_AUTO_INC):表级别的锁,用于处理自增字段的插入操作。
锁的兼容关系
锁的兼容性矩阵展示了不同锁模式之间的兼容性:
查看代码
// https://github.com/mysql/mysql-server/blob/trunk/storage/innobase/include/lock0priv.h#L545
/* 锁兼容性矩阵
* IS IX S X AI
* IS + + + - +
* IX + + - - +
* S + - + - -
* X - - - - -
* AI + + - - -
*
* 注意,对于行来说,InnoDB 只获取 S 或 X 锁。
* 对于表来说,InnoDB 通常获取 IS 或 IX 锁。
* S 或 X 表锁仅在执行 LOCK TABLES 时获取。
* 由于 MySQL 二进制日志的语句级,需要 Auto-Inc 锁。
* 参见 lock_mode_compatible()。
*/
- IS(意向共享锁) 可以与 IS、IX、S、AI 兼容。
- IX(意向排他锁) 可以与 IS、IX、AI 兼容。
- S(共享锁) 可以与 IS、S 兼容。
- X(排他锁) 不与其他任何锁兼容。
- AI(自增锁) 可以与 IS、IX 兼容。
锁的强弱关系
锁的强弱关系矩阵展示了不同锁模式之间的强弱关系:
/* 强或等于关系(mode1=row,mode2=column)
* IS IX S X AI
* IS + - - - -
* IX + + - - -
* S + - + - -
* X + + + + +
* AI - - - - +
* 参见 lock_mode_stronger_or_eq()。
*/
- X(排他锁) 是最强的锁,可以覆盖所有其他锁。
- AI(自增锁) 次之。
- S(共享锁) 强于 IS 和 IX。
- IX(意向排他锁) 强于 IS。
- IS(意向共享锁) 是最弱的锁。
锁类型
- 表锁(LOCK_TABLE):锁定整个表。
- 行锁(LOCK_REC):锁定表中的特定行。
等待标志
/* 锁等待标志;当设置时,表示锁尚未被授予,
* 它只是在等待队列中等待它的轮次 */
#define LOCK_WAIT 256
type_mode
属性的第9位用于表示锁等待状态(LOCK_WAIT
),设置为 0 表示已经获得锁,设置为 1 表示处于锁等待状态。
精确模式
查看代码
/* 精确模式 */
#define LOCK_ORDINARY 0 /*!< 这标志表示一个普通的
Next-Key 锁,与 LOCK_GAP 或 LOCK_REC_NOT_GAP 相对 */
#define LOCK_GAP 512 /*!< 当设置这个位时,表示锁只持有记录前的间隙;
例如,间隙上的 x 锁不会允许修改设置该位的记录;
当记录从索引链中移除时,会创建这种类型的锁 */
#define LOCK_REC_NOT_GAP 1024 /*!< 这位表示锁只在于索引记录上,并且不会阻塞
插入到索引记录前的间隙;这在检索具有唯一键的记录时使用,
也用于在用户设置了 READ COMMITTED 隔离级别时锁定普通 SELECT(不是 UPDATE 或 DELETE 的一部分) */
#define LOCK_INSERT_INTENTION 2048 /*!< 当我们放置一个等待的间隙类型记录锁请求时设置这个位,
以便让索引记录的插入等待,直到没有其他事务在间隙上有冲突的锁;
注意,当等待锁被授予,或者如果锁被继承到相邻的记录时,这个标志保持设置 */
#define LOCK_PREDICATE 8192 /*!< 谓词锁 */
#define LOCK_PRDT_PAGE 16384 /*!< 页面锁 */
行锁可以进一步细分为以下几种类型:
- 普通记录锁(LOCK_ORDINARY):锁定特定的记录。
- 间隙锁(LOCK_GAP):锁定一个范围,但不包括记录本身。
- 记录锁(LOCK_REC_NOT_GAP):只锁定记录,不锁定范围。
- 插入意向锁(LOCK_INSERT_INTENTION):插入记录前,锁定一个范围以防止其他事务插入相同的范围。
关于 Next-Key 锁的标识,当锁模式为行锁(LOCK_REC)时,如果 10 ~ 32 位中所有位都被设置为 0,就表示加的行锁是 Next-Key 锁。
表锁
表锁(Table Locks)用于确保在并发环境下对表的读写操作不会相互干扰。表锁是作用于表级别的锁,可以防止多个事务同时修改同一表中的数据。
在InnoDB存储引擎中,表锁的结构定义在lock_t
和lock_table_t
两个结构体中。以下是这两个结构体的主要属性:
lock_t
结构体:trx
:指向持有锁的事务对象。trx_locks
:指向该事务持有的所有锁的链表。type_mode
:表示锁的类型和模式,其中第5位用于标识锁结构是否为表锁(LOCK_TABLE
)。
lock_table_t
结构体:table
:指向持有锁的表对象。表锁结构必须知晓自己属于哪张表,以便正确地管理锁定的资源。locks
:指向该表中所有锁的链表。当一个事务试图对一张表加锁时,InnoDB会遍历这个链表来判断是否可以立即授锁或者需要等待。如果表中没有任何锁,或者所有现有的锁与新请求的锁兼容,事务可以立即获得锁;否则,它必须等待。
表锁的加锁过程
- 当MySQL执行DDL(Data Definition Language,数据定义语言)或DML(Data Manipulation Language,数据操作语言)语句时,InnoDB会创建对应的事务来执行这些操作。
- 事务中的加锁操作会在事务内部进行。
trx
属性指向的就是加锁的事务对象。 - 如果事务执行DML语句,可能会涉及多个表(比如一张表加钱一张表减钱),这时就会加多个表锁。同一个事务可以加多个表锁和多个行锁,这些锁结构通过
trx_locks
属性形成一个混合的链表。 - 表锁是加在表上的,所以需要知道表锁结构属于哪个表。
table
属性就是指明这个表锁结构所属的表对象。 - 同一时刻,可能有多个事务对同一个表加锁。如果这些锁根据兼容性矩阵判断是兼容的,多个事务可以同时加锁;如果不兼容,后加锁的事务就需要等待。
- InnoDB判断事务是否可以立即获得锁还是需要进入等待状态,是通过遍历
locks
链表来实现的。如果链表中没有表锁结构,或者所有锁结构对应的表锁都和事务当前要加的表锁兼容,事务就可以立即获得锁;否则,就需要等待。 - 如果事务不能立即获得表锁,
type_mode
属性的第9位会被设置为1,表示处于锁等待状态。 - 如果事务可以立即获得锁,InnoDB会将锁结构添加到事务的
trx_locks
链表中,并将锁状态设置为占用。
行锁
在MySQL的InnoDB存储引擎中,行锁(Record Locks)是一种用于保护行级数据完整性的锁。行锁允许事务对数据库中的特定行进行操作,同时允许其他事务读取同一表中的其他行。
行锁结构定义在lock_t
和lock_rec_t
两个结构体中。以下是这两个结构体的主要属性:
lock_t
结构体:trx
:指向持有锁的事务对象。trx_locks
:指向该事务持有的所有锁的链表。index
:指向行锁结构所属的索引对象。行锁通常是对主键索引或二级索引中的记录加锁,这个属性指明了锁结构所属的索引对象。hash
:用于形成行锁结构链表的哈希值。基于page_id
计算出的哈希值通过hash
属性形成链表,这有助于快速定位到特定数据页上的锁结构。type_mode
:表示锁的类型和模式,其中第6位用于标识锁结构是否为行锁(LOCK_REC
)。锁的具体模式(如共享锁或排他锁)及精确模式(如普通记录锁、间隙锁等)也在此属性中编码。
lock_rec_t
结构体:page_id
:表空间ID和数据页号。n_bits
:表示锁结构能保存多少条记录的行锁状态。page_id 和 n_bits这两个属性共同决定了哪些记录由同一个锁结构保护。bitmap
:用于标识加锁记录的内存区域。每一位代表事务是否对某条记录加了行锁。这种设计允许多个行锁结构在满足一定条件时合并成一个,以节约内存空间并提高管理效率。
行锁的加锁过程
- 当事务对表中的特定行加锁时,会创建一个行锁结构。如果事务对多条记录加锁,可能会产生多个行锁结构。
- 行锁结构会根据
page_id
属性中的表空间ID和数据页号计算得到一个哈希值,哈希值相同的多个行锁结构通过hash
属性形成一个链表。 n_bits
属性的值是无符号整数,表示锁结构能保存多少条记录的行锁状态,即最多有多少记录能共用这个行锁结构。- 对于行锁,
type_mode
属性的第6位会被设置为1,第1~4位会被写入锁模式对应的整数值。 - 行锁的不同精确模式,
type_mode
属性的10~32位各位的赋值情况如下:- 普通记录锁,
type_mode
属性的第10位会被设置为1。 - 间隙锁,
type_mode
属性的第11位会被设置为1。 - 插入意向锁,
type_mode
属性的第12位会被设置为1。 - Next-Key锁,
type_mode
属性的第10~32位都设置为0。
- 普通记录锁,
- 如果事务不能立即获得行锁,
type_mode
属性的第9位会被设置为1,表示处于锁等待状态。
行锁的合并条件
事务对多条记录加行锁时,想要共用一个行锁结构,需要满足以下条件:
- 同一个事务对多条记录加行锁。
- 这些记录位于同一个数据页中。
- 这些行锁的锁模式相同(共享锁或排他锁)。
- 这些行锁的精确模式相同(普通记录锁、间隙锁或Next-Key锁)。
- 这些行锁都处于获得锁的状态,不能处于锁等待状态。
锁的粒度划分
根据锁的作用范围和粒度,可以将锁划分为全局锁、表级锁和行级锁。
- 全局锁:全局锁是锁粒度中最粗的一种,它会锁定整个数据库实例。
- 表级锁:表级锁是一种中间粒度的锁,它会锁定整张表。
- 行级锁:行级锁是粒度最细的一种锁。它只锁定表中的一行或者多行数据,其它事务仍然可以对未被锁定的数据行进行操作。
全局锁
全局锁通常用于维护数据库的一致性,在某些情况下需要暂停所有的写操作,比如进行备份或者迁移的时候。全局锁可以阻止任何写入操作,使得数据库进入只读模式,这样可以确保在执行某些关键操作期间数据不会被修改。
在MySQL中,可以通过FLUSH TABLES WITH READ LOCK
命令来获取一个全局读锁,这会锁住所有的表,防止其他会话对这些表进行写操作(包括插入、更新和删除)。当完成备份等操作后,通过UNLOCK TABLES
命令来释放这个锁。
表级锁
表级锁是MySQL中常见的锁机制之一,适用于不同的存储引擎,如MyISAM、InnoDB和BDB。
表锁
表锁是最基本的表级锁,它可以分为以下两类:
表共享读锁
当一个线程对一个表加了读锁(Read Lock)后,其他线程可以读取这张表的数据,但不能对这张表进行写操作(如INSERT、UPDATE、DELETE)。
- 加锁:
LOCK TABLES 表名 READ;
- 释放锁:
UNLOCK TABLES;
或客户端断开连接时自动释放。
表独占写锁
这是一种排他锁,当一个线程对一个表加了写锁(Write Lock)后,其他线程不能读取或写入这张表的数据。
- 加锁:
LOCK TABLES 表名 WRITE;
- 释放锁:
UNLOCK TABLES;
或客户端断开连接时自动释放。
元数据锁
元数据锁(MDL)是为了保护表的元数据(即表结构)不受并发事务的影响而设计的一种锁机制。在执行DDL(数据定义语言)操作之前,必须确保没有活跃的DML(数据操纵语言)操作在使用该表。
当有线程正在访问一张表时,MDL会自动加锁,以防止其他线程对表结构进行修改(如ALTER TABLE、DROP TABLE、创建索引、删除列等DDL操作)。
MDL锁不需要用户显式加锁,系统会自动处理。MDL锁确保了在表上有活动事务时,不能对表结构进行修改,从而保证了数据的一致性。
意向锁
意向锁是InnoDB存储引擎特有的锁,用于表明事务在更细粒度(如行)上的加锁意图。
意向共享锁(IS)
- 作用:表明事务想要在表中的某些行上设置共享锁。
- 添加方式:通过
SELECT ... LOCK IN SHARE MODE
语句添加。 - 兼容性:与表锁的共享读锁兼容,但与表锁的独占写锁互斥。
意向排他锁(IX)
- 作用:表明事务想要在表中的某些行上设置排他锁。
- 添加方式:通过
INSERT
、UPDATE
、DELETE
或SELECT ... FOR UPDATE
语句添加。 - 兼容性:与表锁的共享读锁和独占写锁都互斥,但意向锁之间不会互斥。
意向锁的释放
- 当事务提交或回滚时,意向共享锁和意向排他锁都会自动释放。
意向锁的主要目的是为了提高性能,减少表锁与行锁之间的冲突检查。当需要加表锁时,InnoDB可以快速检查是否有意向锁与之冲突,而不需要检查每行数据上的锁。
自增锁
自增锁(LOCK_AUTO_INC)是InnoDB存储引擎中一种特殊的表级锁,它用于管理和同步对自增字段(通常是AUTO_INCREMENT
字段)的访问。当多个客户端同时尝试插入具有自增主键的新记录时,如果没有适当的同步机制,可能会导致数据不一致的问题,比如自增ID可能会被重复使用或跳过。自增锁就是为了避免这种情况的发生而设计的。
自增锁的主要目的是确保在多线程或多客户端环境中,当多个事务尝试插入具有自增主键的新记录时,能够正确地分配唯一的自增ID。
工作原理
当多个事务同时尝试插入数据到带有自增字段的表中时,以下步骤会发生:
- 第一个事务会获取自增锁,并从自增序列中获取一个值用于插入。
- 其他事务在尝试插入时,也会试图获取自增锁,但由于锁已经被第一个事务持有,它们将等待。
- 第一个事务完成插入操作后,释放自增锁。
- 等待的事务中的一个会获取到自增锁,并从序列中获取下一个值进行插入,以此类推。
参数配置
为了减少自增锁对性能的影响,InnoDB在后续的版本中进行了一些优化。
innodb_autoinc_lock_mode: 这个参数决定了InnoDB如何处理自增锁。它有三种模式:
- Mode 0(传统模式): 每次插入都会获取表级锁,直到事务结束。
- Mode 1(自增锁的快速释放): 仅在插入时获取锁,插入完成后立即释放。
- Mode 2(自增锁的批量获取): 如果在事务中插入多个行,允许一次性获取多个自增值,只需获取一次锁即可。
行级锁
行级锁是MySQL中锁定粒度最小的锁机制,它仅锁定数据表中的特定行,因此可以最大程度地减少锁冲突,提高并发访问的性能。
行锁
行锁(Record Lock)是针对单个行记录的锁定。
行锁可以防止其他事务对锁定行的数据进行UPDATE和DELETE操作。
行锁也可以分为两类:
- 共享锁(S):允许事务读取一行数据,但不允许其他事务获得相同数据行的排他锁。
- 排他锁(X):允许获得排他锁的事务更新数据行,阻止其他事务获取相同数据行的共享锁或排他锁。
InnoDB的行锁是通过索引来实现的。如果查询操作没有使用索引,或者使用了非索引列作为查询条件,InnoDB将无法使用行锁,而是对整个表加锁,这种情况下行锁会升级为表锁。
间隙锁
间隙锁(Gap Lock)是锁定索引记录之间的间隙,而不是具体的记录。
确保索引记录之间的间隙保持不变,防止其他事务在这个间隙中插入新记录,从而避免幻读问题。
间隙锁在REPEATABLE READ事务隔离级别下被支持。
间隙锁可以共存,即一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。
临键锁
临键锁(Next-Key Lock)是行锁和间隙锁的组合,它锁定一个范围,包括索引记录以及前面的间隙。
临键锁可以同时防止幻读和丢失更新问题。
临键锁在REPEATABLE READ事务隔离级别下被支持。
当InnoDB扫描索引记录时,它会使用临键锁来锁定扫描到的索引范围,这包括最后一个扫描到的索引记录以及它之前的间隙。
插入意向锁
插入意向锁(LOCK_INSERT_INTENTION)是InnoDB存储引擎中用来处理插入冲突的锁标识,它是在插入操作之前设置的一种锁,用于处理并发插入操作时的冲突。
插入意向锁的作用是在多个事务同时尝试在同一个索引间隙中插入记录时,确保它们不会互相冲突。它通过锁定一个范围来防止其他事务插入相同的范围,从而保证了数据的一致性和事务的隔离性。
插入意向锁是间隙锁的一种,但它专门用于处理插入操作。
当事务尝试获取间隙锁以进行插入操作时,如果发现有其他事务持有冲突的锁,则该事务会设置锁的LOCK_INSERT_INTENTION
标志,并等待直到可以安全地获取所需的锁。
插入意向锁不会阻塞其他事务对相同间隙的读取操作。
当等待的插入意向锁被授予时,或者如果锁被继承到相邻的记录时,这个标志保持设置。
工作原理
- 当一个事务准备插入一条记录到某个索引间隙中时,它首先会检查该间隙是否已被其他事务锁定。
- 如果间隙是空闲的,事务会进行插入操作。
- 如果间隙已被其他事务的间隙锁锁定,插入操作将无法立即进行,事务将设置一个插入意向锁并等待。
- 当锁定间隙的事务完成并释放间隙锁后,等待的插入意向锁将被授予,事务可以继续插入操作,期间插入意向锁标志保持不变。
参考链接:
https://segmentfault.com/a/1190000044930041
https://segmentfault.com/a/1190000017076101?utm_source=coffeephp.com#item-5-6