锁是计算机协调多个进程或线程并发访问某一资源的机制。
MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。
事务是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。
在默认情况下,MySQL每执行一条SQL语句,都是一个单独的事务。
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | 是 | 是 | 是 |
不可重复读(read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
mysql默认的事务隔离级别为repeatable-read
在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
MySQL/InnoDB定义的4种隔离级别:
可以读取未提交记录
Serializable隔离级别下,读写冲突,因此并发度急剧下降,在MySQL/InnoDB下不建议使用。
MVCC是一种多版本并发控制机制。锁机制可以控制并发操作,但是其系统开销较大,而MVCC可以在大多数情况下代替行级锁,使用MVCC,能降低其系统开销。
MVCC是通过保存数据在某个时间点的快照来实现的。 不同存储引擎的MVCC、不同存储引擎的MVCC实现是不同的,典型的有乐观并发控制和悲观并发控制。
InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的,这两个列,分别保存了这个行的创建时间,一个保存的是行的删除时间。这里存储的并不是实际的时间值,而是系统版本号(可以理解为事务的ID),没开始一个新的事务,系统版本号就会自动递增,事务开始时刻的系统版本号会作为事务的ID。IN
注意:
在SELECT时,只满足上述两个条件也是不能达到快照读的要求的比如在RR的隔离级别下会有如下情况:
启动1号事务、启动2号事务、1号事务更新x行并提交事务(此时x行的修改版本号为1,删除版本号为未定义)、2号事务读取x行
按照如上步骤,如果只满足上述两个条件的话
显然2号事务时可以读取到1号事务所做的更新
(x行修改版本号为1满足小于2删除版本号为未定义满足事务开始之前未删除),显然是不足够满足快照读的要求
事实上,在读取到满足上述两个条件的行时,InnoDB还会进行二次检查,如上图所示
活跃事务列表:RC隔离级别下,在语句开始时从全局事务表中获取活跃(未提交)事务构造Read View,RR隔离级别下,在事务开始时从全局事务表中获取活跃事务构造Read View
至此,通过上述步骤,可以实现真正的快照读。
上述策略的结果就是,在读取数据的时候,InnoDB几乎不用获得任何锁,每个查询都通过版本检查,只获得自己需要的数据版本,从而大大提高了系统的并发度。 这种策略的缺点是,每行记录都需要额外的存储空间,更多的行检查工作和一些额外的维护工作。
上述更新前建立undo log,根据各种策略读取时非阻塞就是MVCC,undo log中的行就是MVCC中的多版本,这个可能与我们所理解的MVCC有较大的出入,一般我们认为MVCC有下面几个特点:
就是每行都有版本号,保存时根据版本号决定是否成功,听起来含有乐观锁的味道,而Innodb的实现方式是:
二者最本质的区别是,当修改数据时是否要排他锁定,如果锁定了还算不算是MVCC?
Innodb的实现真算不上MVCC,因为并没有实现核心的多版本共存,undo log中的内容只是串行化的结果,记录了多个事务的过程,不属于多版本共存。但理想的MVCC是难以实现的,当事务仅修改一行记录使用理想的MVCC模式是没有问题的,可以通过比较版本号进行回滚;但当事务影响到多行数据时,理想的MVCC据无能为力了。
比如,如果Transaciton1执行理想的MVCC,修改Row1成功,而修改Row2失败,此时需要回滚Row1,但因为Row1没有被锁定,其数据可能又被Transaction2所修改,如果此时回滚Row1的内容,则会破坏Transaction2的修改结果,导致Transaction2违反ACID。
理想MVCC难以实现的根本原因在于企图通过乐观锁代替二段提交。修改两行数据,但为了保证其一致性,与修改两个分布式系统中的数据并无区别,而二提交是目前这种场景保证一致性的唯一手段。二段提交的本质是锁定,乐观锁的本质是消除锁定,二者矛盾,故理想的MVCC难以真正在实际中被应用,Innodb只是借了MVCC这个名字,提供了读的非阻塞而已。
另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
意向锁仅仅用于表锁和行锁的共存使用。如果我们的操作仅仅涉及行锁,那么意向锁不会对我们的操作产生任何影响。在任一操作给表A的一行记录加锁前,首先要给该表加意向锁,如果获得了意向锁,然后才会加行锁,并在加行锁时判断是否冲突。如果现在有一个操作要获得表A的表锁,由于意向锁的存在,表锁获取会失败(如果没有意向锁的存在,加表锁之前可能要遍历整个聚簇索引,判断是否有行锁存在,如果没有行锁才能加表锁)。
同理,如果某一操作已经获得了表A的表锁,那么另一操作获得行锁之前,首先会检查是否可以获得意向锁,并在获得意向锁失败后,等待表锁操作的完成。也就是说:1.意向锁是表级锁,但是却表示事务正在读或写某一行记录;2.意向锁之间不会冲突, 因为意向锁仅仅代表要对某行记录进行操作,在加行锁时,会判断是否冲突;3.意向锁是InnoDB自动加的,不需用户干预。
InnoDB使用MVCC来实现一致性非锁定读,在read-committed和repeatable-read两种事务隔离级别下使用,且效果不同,具体如下。
read-committed
在读已提交的隔离级别下,事务在一致性非锁定读始终读取当前最新的数据快照,即当其他事务提交更新后快照更新也会读取最新的,也就是出现不可重复读。
repeatable-read
在可重复读的隔离级别下,事务始终读取事务开始时的快照版本。
一致性锁定读有两种实现方式,一种是加X锁,一种是加S锁
select ... for update 显示的使用加X锁的方式读取
select ... lock in share mode 显示的使用加S锁的方式读取
innodb_autoinc_lock_mode有3种配置模式:0、1、2,
InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
InnoDB目前处理死锁的方法是:将持有最少行级排它锁的事务回滚。如果是因为死锁引起的回滚,可以考虑在应用程序中重新执行。
在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时再申请排他锁,因为当用户申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁。
通常来说,死锁都是应用设计的问题,通过调整业务流程、数据库对象设计、事务大小,以及访问数据库的SQL语句,绝大部分死锁都可以避免。介绍几种避免死锁的常用方法。
如果出现死锁,可以用SHOW INNODB STATUS命令来确定最后一个死锁产生的原因。返回结果中包括死锁相关事务的详细信息,如引发死锁的SQL语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等。
死锁的发生与否,并不在于事务中有多少条SQL语句,死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。而使用本文上面提到的,分析MySQL每条SQL语句的加锁规则,分析出每条语句的加锁顺序,然后检查多个并发SQL间是否存在以相反的顺序加锁的情况,就可以分析出各种潜在的死锁情况,也可以分析出线上死锁发生的原因。
问题:
按索引项来加锁的话,不同索引相同行,会不会同时获得不同的锁却操作同一行
自增锁,是语句级的锁,如果当前事务先获取锁,却后执行完,在从库按语句复制的话,会不会出现ID不一致
lock0types.h 事务锁系统的类型定义,包含了 lock_mode定义
lock0priv.ic 锁模块内部的一些方法,被用于除了lock0lock.cc的三个文件里,
lock_get_type_low 获取锁是表锁还是行锁
lock_clust_rec_some_has_impl 检查一行数据上是否有隐示的x锁
lock_rec_get_n_bits 获取一个记录锁的锁位图的bit数目
lock_rec_set_nth_bit 设置第n个记录锁bit位为true
lock_rec_get_next_on_page 获取当前page上的下一个记录锁
lock_rec_get_next_on_page_const
lock_rec_get_first_on_page_addr 获取当前page上第一个记录锁
lock_rec_get_first_on_page
lock_rec_get_next 返回当前记录上的下一个显示锁请求的锁
lock_rec_get_next_const
lock_rec_get_first 获取当前记录上的第一个显示锁请求的锁
lock_rec_get_nth_bit Gets the nth bit of a record lock.
lock_get_mode 获取一个锁的 lock_mode
lock_mode_compatible 判断两个lock_mode是否兼容
lock_mode_stronger_or_eq 判断lock_mode 1 是否比 lock_mode 2更强
lock_get_wait 判断一个锁是不是等待锁
lock_rec_find_similar_on_page 查找一个合适的锁结构在当前事务当前页面下???找到的话就不用新创建锁结构???
lock_table_has 检查一个事务是否有指定类型的表锁,只能由当前事务调用
lock0priv.h 锁模块内部的结构和方法
struct lock_table_t 表锁结构
struct lock_rec_t 行锁结构
struct lock_t 锁通用结构
static const byte lock_compatibility_matrix[5][5] 锁的兼容关系
static const byte lock_strength_matrix[5][5] 锁的强弱关系
enum lock_rec_req_status 记录锁请求状态
struct RecID 记录锁ID
class RecLock 记录锁类
add_to_waitq 入队一个锁等待
create 为事务创建一个锁并初始化
is_on_row Check of the lock is on m_rec_id.
lock_alloc 创建锁实例
prepare 做一些检查个预处理为创建一个记录锁
mark_trx_for_rollback 收集需要异步回滚的事务
jump_queue 跳过所有低优先级事务并添加锁,如果能授予锁,则授予,不能的话把其他都标记异步回滚
lock_add 添加一个记录锁到事务锁列表和锁hash表中
deadlock_check 检查并解决死锁
check_deadlock_result 检查死锁检查的结果
is_predicate_lock 返回时不是predictate锁
init 按照要求设置上下文
lock_get_type_low 返回行锁还是表锁
lock_rec_get_prev 获取一个记录上的前一个锁
在 Innodb 内部用一个 unsiged long 类型数据表示锁的类型, 最低的 4 个 bit 表示 lock_mode, 5-8 bit 表示 lock_type, 剩下的高位 bit 表示行锁的类型。
5-8 bit 位标识 lock_type 目前只使用了两个,第5位标识是表锁,第6位标识是行锁
#define LOCK_TABLE 16 /*!< table lock */ //表锁
#define LOCK_REC 32 /*!< record lock */ //记录锁
lock描述了锁的基本模式,目前有5种模式,IS、IX、S、X、AI
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
};
以下是锁的基本模式的兼容关系和强弱关系
/* LOCK COMPATIBILITY MATRIX
* IS IX S X AI
* IS + + + - +
* IX + + - - +
* S + - + - -
* X - - - - -
* AI + + - - -
*
* Note that for rows, InnoDB only acquires S or X locks.
* For tables, InnoDB normally acquires IS or IX locks.
* S or X table locks are only acquired for LOCK TABLES.
* Auto-increment (AI) locks are needed because of
* statement-level MySQL binlog.
* See also lock_mode_compatible().
*/
static const byte lock_compatibility_matrix[5][5] = {
/** IS IX S X AI */
/* IS */ { TRUE, TRUE, TRUE, FALSE, TRUE},
/* IX */ { TRUE, TRUE, FALSE, FALSE, TRUE},
/* S */ { TRUE, FALSE, TRUE, FALSE, FALSE},
/* X */ { FALSE, FALSE, FALSE, FALSE, FALSE},
/* AI */ { TRUE, TRUE, FALSE, FALSE, FALSE}
};
/* STRONGER-OR-EQUAL RELATION (mode1=row, mode2=column)
* IS IX S X AI
* IS + - - - -
* IX + + - - -
* S + - + - -
* X + + + + +
* AI - - - - +
* See lock_mode_stronger_or_eq().
*/
static const byte lock_strength_matrix[5][5] = {
/** IS IX S X AI */
/* IS */ { TRUE, FALSE, FALSE, FALSE, FALSE},
/* IX */ { TRUE, TRUE, FALSE, FALSE, FALSE},
/* S */ { TRUE, FALSE, TRUE, FALSE, FALSE},
/* X */ { TRUE, TRUE, TRUE, TRUE, TRUE},
/* AI */ { FALSE, FALSE, FALSE, FALSE, TRUE}
};
剩下的高位标识行锁的模式,对于表锁这些位都是空的
目前record_lock_type有以下值
#define LOCK_WAIT 256 /*!< Waiting lock flag; when set, it //锁等待
means that the lock has not yet been
granted, it is just waiting for its
turn in the wait queue */
/* Precise modes */
#define LOCK_ORDINARY 0 /*!< this flag denotes an ordinary
next-key lock in contrast to LOCK_GAP
or LOCK_REC_NOT_GAP */
#define LOCK_GAP 512 /*!< when this bit is set, it means that the
lock holds only on the gap before the record;
for instance, an x-lock on the gap does not
give permission to modify the record on which
the bit is set; locks of this type are created
when records are removed from the index chain
of records */
#define LOCK_REC_NOT_GAP 1024 /*!< this bit means that the lock is only on
the index record and does NOT block inserts
to the gap before the index record; this is
used in the case when we retrieve a record
with a unique key, and is also used in
locking plain SELECTs (not part of UPDATE
or DELETE) when the user has set the READ
COMMITTED isolation level */
#define LOCK_INSERT_INTENTION 2048 /*!< this bit is set when we place a waiting
gap type record lock request in order to let
an insert of an index record to wait until
there are no conflicting locks by other
transactions on the gap; note that this flag
remains set when the waiting lock is granted,
or if the lock is inherited to a neighboring
record */
#define LOCK_PREDICATE 8192 /*!< Predicate lock */
#define LOCK_PRDT_PAGE 16384 /*!< Page lock */
首先是锁系统结构,在Innodb启动的时候初始化,在Innodb结束的时候释放
主要保存着锁的hash表,以及相关事务、线程的一些信息
/** The lock system struct */
struct lock_sys_t{
char pad1[CACHE_LINE_SIZE]; /*!< padding to prevent other
memory update hotspots from
residing on the same memory
cache line */
LockMutex mutex; /*!< Mutex protecting the
locks */
hash_table_t* rec_hash; /*!< hash table of the record
locks */
hash_table_t* prdt_hash; /*!< hash table of the predicate
lock */
hash_table_t* prdt_page_hash; /*!< hash table of the page
lock */
char pad2[CACHE_LINE_SIZE]; /*!< Padding */
LockMutex wait_mutex; /*!< Mutex protecting the
next two fields */
srv_slot_t* waiting_threads; /*!< Array of user threads
suspended while waiting for
locks within InnoDB, protected
by the lock_sys->wait_mutex */
srv_slot_t* last_slot; /*!< highest slot ever used
in the waiting_threads array,
protected by
lock_sys->wait_mutex */
ibool rollback_complete;
/*!< TRUE if rollback of all
recovered transactions is
complete. Protected by
lock_sys->mutex */
ulint n_lock_max_wait_time; /*!< Max wait time */
os_event_t timeout_event; /*!< Set to the event that is
created in the lock wait monitor
thread. A value of 0 means the
thread is not active */
bool timeout_thread_active; /*!< True if the timeout thread
is running */
};
lock_sys_create . Creates the lock system at database start.
lock_sys_close Closes the lock system at database shutdown
无论是行锁还是表锁都使用lock_t结构保存,其中用一个union来分别保存行锁和表锁不同的数据,分别为lock_table_t和lock_rec_t
struct lock_t {
trx_t* trx; /*!< transaction owning the lock */
UT_LIST_NODE_T(lock_t)
trx_locks; /*!< list of the locks of the transaction */
dict_index_t* index; /*!< index for a record lock */
lock_t* hash; /*!< hash chain node for a record lock. The link node in a singly linked list, used during hashing. */
union { //表锁和记录锁不同的数据
lock_table_t tab_lock;/*!< table lock */ //表锁结构体
lock_rec_t rec_lock;/*!< record lock */ //记录锁结构体
} un_member; /*!< lock details */
ib_uint32_t type_mode; /*!< lock type, mode, LOCK_GAP or LOCK_REC_NOT_GAP, LOCK_INSERT_INTENTION, wait flag, ORed */ //锁类型
/** Remove GAP lock from a next Key Lock */
void remove_gap_lock() //移除一个next-key锁的gap锁
{
ut_ad(!is_gap());
ut_ad(!is_insert_intention());
ut_ad(is_record_lock());
type_mode |= LOCK_REC_NOT_GAP;
}
/** Determine if the lock object is a record lock.
@return true if record lock, false otherwise. */
bool is_record_lock() const //判断是否是记录锁
{
return(type() == LOCK_REC);
}
/** Determine if it is predicate lock.
@return true if predicate lock, false otherwise. */
bool is_predicate() const
{
return(type_mode & (LOCK_PREDICATE | LOCK_PRDT_PAGE));
}
bool is_waiting() const
{
return(type_mode & LOCK_WAIT);
}
bool is_gap() const
{
return(type_mode & LOCK_GAP);
}
bool is_record_not_gap() const
{
return(type_mode & LOCK_REC_NOT_GAP);
}
bool is_insert_intention() const
{
return(type_mode & LOCK_INSERT_INTENTION);
}
ulint type() const {
return(type_mode & LOCK_TYPE_MASK);
}
enum lock_mode mode() const
{
return(static_cast<enum lock_mode>(type_mode & LOCK_MODE_MASK));
}
/** Get lock hash table
@return lock hash table */
hash_table_t* hash_table() const
{
return(lock_hash_get(type_mode));
}
/** Get tablespace ID for the lock
@return space ID */
ulint space() const
{
return(un_member.rec_lock.space);
}
/** Get page number of the lock
@return page number */
ulint page_number() const
{
return(un_member.rec_lock.page_no);
}
/** Print the lock object into the given output stream.
@param[in,out] out the output stream
@return the given output stream. */
std::ostream& print(std::ostream& out) const;
/** Convert the member 'type_mode' into a human readable string.
@return human readable string */
std::string type_mode_string() const;
const char* type_string() const
{
switch (type_mode & LOCK_TYPE_MASK) {
case LOCK_REC:
return("LOCK_REC");
case LOCK_TABLE:
return("LOCK_TABLE");
default:
ut_error;
}
}
};
/** A table lock */
struct lock_table_t {
dict_table_t* table; /*!< database table in dictionary
cache */
UT_LIST_NODE_T(lock_t)
locks; /*!< list of locks on the same
table */
/** Print the table 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;
};
/** Record lock for a page */
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;
};
Innodb 使用位图来表示锁具体锁住了那几行,在函数 lock_rec_create 中为 lock_t 分配内存空间的时候,会在对象地址后分配一段内存空间(当前行数 + 64)用来保存位图。n_bits 表示位图大小。
/* Make lock bitmap bigger by a safety margin */
n_bits = page_dir_get_n_heap(page) + LOCK_PAGE_BITMAP_MARGIN;
n_bytes = 1 + n_bits / 8;
lock = static_cast<lock_t*>(
mem_heap_alloc(trx->lock.lock_heap, sizeof(lock_t) + n_bytes));
explicit lock 显示锁 implicit lock 隐示锁
InnoDB增加隐示锁的目的是在INSERT的时候不加锁
具体实现为
lock system 开始启动 申请lock_sys_t结构,初始化结构体
lock system 结束关闭 释放lock_sys_t结构的元素,释放结构体
https://www.colabug.com/32979...
问题:为什么有GAP也能插入(有GAP是不能插入的),插入意向锁什么时候加(插入之前尝试加插入意向锁,冲突加等待,不冲突直接插数据), 有什么用,唯一键冲突如何处理的(需要检测冲突会先尝试给行加S|next-key lock,加成功再检测)
和加锁有关的流程大概如下
删除加锁有个重要的问题是,删除并发的时候的加锁会有以下死锁问题
lock_table
主要的参数是 mode(锁类型),block(包含该行的 buffer 数据页),heap_no(具体哪一行)。就可以确定加什么样的锁,以及在哪一行加。
加锁流程主要是lock fast和lock slow,首先进入lock fast进行快速加锁,如果快速加锁失败则进入lock slow开始正常加锁流程,可能有锁冲突检查、死锁检查等流程
lock fast
lock slow
2.有更强的锁,直接返回成功,什么都不需要做
3.如果没有更强的锁,调用lock_rec_other_has_conflicting判断是否有锁冲突需要等待,如果有转4,没有转5。lock_rec_other_has_conflicting函数遍历rec_hash,拿出对应行上的每一个锁,调用 lock_rec_has_to_wait 进行冲突判断
1)如果当前锁和要加的锁是同一个事务的,直接返回,没有冲突
2)根据兼容矩阵判断当前锁和要加的锁是否兼容,如果兼容,直接返回,没有冲突
3)如果要加的是lock_gap或者heap_no是页面上界,且不是lock_insert_intention的话,可以直接返回,没有冲突,因为非插入意向锁的gap锁是不用等待的,都不冲突
4)如果要加的锁不是插入意向锁lock_insert_intention,且当前锁是一个gap锁,直接返回,没有冲突
5)如果要加的锁是gap锁,且当前锁是lock_rec_not_gap锁,直接返回,没有冲突
6)如果当前锁是一个插入意向锁,直接返回没有冲突
7)不满足上述条件,返回冲突
ps:为什么经过2步骤判断锁不兼容还需要往下走5个判断,是因为锁类型lock_mode/lock_type/rec_lock_type三种标记位同时有,如lock_x|lock_gap, lock_s|lock_rec_not_gap 这两个锁虽然lock_mode不兼容,但不冲突
lock_rec_dequeue_from_page
lock_rec_has_to_wait_in_queue
对于不需要等待的锁,调用lock_grant进行加锁
lock_table_dequeue
构造wait-for graph
构造一个有向图,图中的节点代表一个事务,图的一个边A->B代表着A事务等待B事务的一个锁
具体实现是在死锁检测时,从当前锁的事务开始搜索,遍历当前行的所有锁,判断当前事务是否需要等待现有锁释放,是的话,代表有一条边,进行一次入栈操作
死锁检测
有向图判断环,用栈的方式,如果有依赖等待,进行入栈,如果当前事务所有依赖的事务遍历完毕,进行一次出栈
回滚事务选择
如果发现循环等待,选择当前事务和等待的事务其中权重小的一个回滚,具体的权重比较函数是 trx_weight_ge, 如果一个事务修改了不支持事务的表,那么认为它的权重较高,否则认为 undo log 数加持有的锁数之和较大的权重较高。
DeadlockChecker::search()
等待与唤醒 锁的等待以及唤醒实际上是线程的等待和唤醒,调用函数 lock_wait_suspend_thread 挂起当前线程,配合 OS_EVENT 机制,实现唤醒和锁超时等功能
分裂
索引页面分裂导致的锁分裂
合并
索引页面合并导致的锁合并
迁移
插入和删除记录时的GAP锁的迁移
主要是出现在当前事务拥有一个记录的GAP锁,又在这个记录前插入记录时
set global tx_isolation='repeatable-read';
create table t1(c1 int primary key, c2 int unique) engine=innodb;
insert into t1 values(1,1);
begin;
# supremum 记录上加 LOCK_X|LOCK_GAP 锁住(1~)
select * from t1 where c2=2 for update;
# 发现插入(3,3)的间隙存在GAP锁,因此给(3,3)加LOCK_X | LOCK_GAP锁。这样依然锁住了(1~)
insert into t1 values(3,3);
for (lock = lock_rec_get_first(lock_sys->rec_hash, block, heap_no);
lock != NULL;
lock = lock_rec_get_next(heap_no, lock)) { //遍历当前行的所有锁
if (!lock_rec_get_insert_intention(lock)
&& (heap_no == PAGE_HEAP_NO_SUPREMUM
|| !lock_rec_get_rec_not_gap(lock))) { //如果不是插入意向锁 且 heap是上界或者不是一个非GAP锁
lock_rec_add_to_queue( //添加一个GAP的且mode和lock一致的锁到下一行
LOCK_REC | LOCK_GAP | lock_get_mode(lock),
block, heir_heap_no, lock->index,
lock->trx, FALSE);
}
}
如有记录 1 3 5
例子:select * from meng_hinata where id = 10 for update
组合一:id列是主键,RC隔离级别
在主键id=10列加上X锁
组合二:id列是二级唯一索引,RC隔离级别
在唯一索引id=10列上加X锁,在主键索引上对应列加X锁
组合三:id列是二级非唯一索引,RC隔离级别
在二级索引上所有id=10列加上X锁,这些列对应的主键索引列加上X锁
组合四:id列上没有索引,RC隔离级别
在聚簇索引上扫描,所有列上加X锁,此处有个优化,不满足的列在加锁后,判断不满足即可释放锁,违背二阶段加锁
组合五:id列是主键,RR隔离级别
在主键id=10列上加X锁
组合六:id列是二级唯一索引,RR隔离级别
在唯一索引id=10列上加X锁,在主键索引上对应列加X锁
组合七:id列是二级非唯一索引,RR隔离级别
在二级索引上查找id=10列,找到则加上X锁和GAP锁,然后对应的聚簇索引列加上X锁,最后一个不满足的列只会加上GAP锁
组合八:id列上没有索引,RR隔离级别
在聚簇索引上扫描,所有列加上X锁和GAP锁
Innodb默认事务隔离级别为RR
看出默认隔离级别为Repeatable Read,对user_id = 100000745进行count并显示加一个X锁,使用explain看出使用了uid索引即user_id字段的索引
id = 1449731912的数据user_id = 100000745,可以看出在对user_id字段索引加了X锁之后,操纵相对应的主键索引时也会被阻塞,验证了对非主键索引加X锁的同时会对相应主键索引也加锁
同时在对user_id字段索引加了X锁之后,也不能插入user_id相同的新数据,验证了innodb再RR隔离级别下也是防止了幻读的现象,实际上当范围查找时也会再加一个间隙锁来保证不会有幻读
可以看出不使用for update之后变为非当前读,也没有进行加锁,可以进行插入操作。
以上两个图设置隔离级别为RC后,可以看出在user_id = 100000745 进行for update查询后,还是能进行插入相同user_id值的列,说明只加了X锁并没有加间隙锁,同时因为X锁的原因,不能进行删除,验证了innodb引擎在RC隔离级别对于当前度也是会对读到的信息加锁,但没加间隙锁,会出现幻读。
以上两个图,在RC隔离级别下。
首先事务A对无索引的列进行查找并加锁,会扫描全表,注意并没有加表锁,而是对所有行都加了X锁,但是没加间隙锁,事务B还是可以插入。
此时事务A再扫描全表并加X锁,发现被阻塞,提交事务B后继续运行
事务A对全表加X锁后,事务B再次尝试插入,同样成功,但无法删除数据,说明还是没用表锁,对所有行加了X锁。
InnoDB条件变量核心数据结构为os_event_t,类似pthread_cont_t。如果需要创建和销毁则分别使用os_event_create和os_event_free函数。需要等待某个条件变量,先调用os_event_reset,然后使用os_event_wait,如果需要超时等待,使用os_event_wait_time替换os_event_wait即可
InnoDB自旋互斥锁的实现主要在文件 sync0sync.cc 和sync0sync.ic 中,头文件sync0sync.h 定义了核心数据结构ib_mutex_t。使用方法很简单,mutex_create创建锁,mutex_free释放锁,mutex_enter尝试获得锁,如果已经被占用了,则等待。mutex_exit释放锁,同时唤醒所有等待的线程,拿到锁的线程开始执行,其余线程继续等待。
InnoDB读写锁的核心实现在源文件sync0rw.cc 和sync0rw.ic 中,核心数据结构rw_lock_t 定义在sync0rw.h 中。使用方法与InnoDB 自旋互斥锁很类似,只不过读请求和写请求要调用不同的函数。加读锁调用rw_lock_s_lock, 加写锁调用rw_lock_x_lock,释放读锁调用rw_lock_s_unlock, 释放写锁调用rw_lock_x_unlock,创建读写锁调用rw_lock_create,释放读写锁调用rw_lock_free。函数rw_lock_x_lock_nowait和rw_lock_s_lock_nowait表示,当加读写锁失败的时候,直接返回,而不是自旋等待。
----------伟大的分割线-----------