December 2012

[MySQL学习] Innodb锁系统(3)关键结构体及函数

1.锁对象的定义: 关键结构体: UNIV_INTERN lock_sys_t* lock_sys = NULL; lock_sys是一个全局变量,用于控制整个Innodb锁系统的全部锁结构,其对应的结构体为lock_sys_t,该结构体只包含两个成员: struct lock_sys_struct{     hash_table_t* rec_hash;     ulint rec_num; }; 从函数lock_rec_create可以很容易看出这两个变量的作用: quoted code:     HASH_INSERT(lock_t, hash, lock_sys->rec_hash,             lock_rec_fold(space, page_no), lock);     lock_sys->rec_num++; 每次新建一个锁对象,都要插入到lock_sys->rec_hash中,这里会根据space id 和page no来计算对应的哈希桶,然后再将锁对象插入到其中,并递增lock_sys->rec_num。 注意只有记录锁会存在lock_sys->rec_hash中,表锁是不会存这里,只会插入到事务trx->trx_locks链表和对应表对象的table->locks中,并且都是加到链表的尾部(lock_table_create)。 每个锁对象的类型为lock_t,在lock0priv.h中定义,描述如下: trx* trx 持有该锁对象的事务 UT_LIST_NODE_T(lock_t) trx_locks 在事务锁链表trx->trx_locks中对应的节点 ulint type_mode 锁类型,在前文已有介绍 hash_node_t […]

[MySQL学习]Innodb锁相关描述翻译

以下翻译自lock0lock.c的文件头部注释,翻译的比较凌乱… //////////////////////////////////////////////////////////////////////////////////// 几个hardcode的宏: LOCK_MAX_N_STEPS_IN_DEADLOCK_CHECK 1000000 该宏用于控制在事务的waits-for-graph中的查找深度 LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK 200 用于控制死锁递归检测深度 LOCK_RELEASE_KERNEL_INTERVAL 1000 在释放锁的过程中(lock_release_off_kernel),每隔这么多次循环释放kernel mutex LOCK_PAGE_BITMAP_MARGIN 64 创建锁的bitmap至少有这么多个比特位 一个显式锁同时影响记录和记录前面的gap。一个隐式x锁不影响gap,只会锁住读活更新的索引记录。 如果一个事务修改或插入了一个索引记录,它拥有在该就上的隐式x锁。如果事务修改了聚集索引记录,会在二级索引记录上也有一个隐式x锁,二级索引记录所在的page上的事务id>=当前事务的trx id, 并且没有二级索引记录上的显式 非gap锁请求 这种对二级索引的相容性定义是根据其实现决定的:我们希望通过查看聚集索引记录来判断一个二级索引记录是否有一个隐式x锁,而不是根据二级索引记录的历史版本。 不同的事务可能同时在同一个GAP上产生冲突锁。在gap上的锁被单纯的禁止:无法插入,或者如果一个不同的事务在gap上有一个冲突的锁,一个select cutsor可能需要等待。GAP上的x锁并不意味着有权力向GAP中插入记录。 一个显式锁可以设置在一个用户记录或者supremum记录上。在supremum记录上的锁通常被认为是一个GAP锁,尽管GAP位没有设置。当我们修改一条记录并且修改了记录的大小时,可能会临时的将它的显式锁存储到infimum记录上,除此之外,不会在infimum记录上加锁。 一个正在等待的记录锁也可以是GAP类型;当在显式锁队列中,一个锁请求的前面没有另外一个冲突模式锁对象时,就可以获得锁。 RULE1:如果在记录上有一个隐式X锁,并且有一个non-gap锁请求在队列中等待,这时候持有隐式x锁的事务同样也会有一个显式的Non-gap记录X锁。这样当锁被释放时,我们只需要通过查看队列中的显式锁请求来为等待锁请求赋予锁 RULE3:不同的事务不可以在同一个记录上同时拥有冲突的NON-GAP锁。但是,他们可以拥有冲突的GAP锁。 RULE4:如果在队列中有一个等待的锁请求。没有锁请求,gap或者not-gap锁可以插入到队列中该等待锁请求前面。在记录删除和page分裂时,数据库管理者可以为一个事务创建新的gap类型的锁。如果没有RULE4,事务waits-for graph可能变成循环而数据库无法注意到这一点,因为死锁检测只有在一个事务本身请求一个锁时才发生。 当在下一个记录上没有其他事务持有的显式锁时,允许在GAP中插入记录。这些锁请求被赋予或等待,gap位是否被设置都无所谓,除了另外一个事务的gap类型请求,并且正在等待轮到他去插入时,会被忽略掉。换句话说,一个其他事务持有的隐式X锁不会阻止一次写入,这允许在是用类似ORACLE序列生成器为许多事务并发产生主键,这会允许更大的并发度。 如果事务在记录上有X锁,或者其他事务没有在该记录上任何non-gap锁时,则允许该事务修改记录 如果事务有一个Non-gap显式、或者隐式锁在记录上,或者其他事务有在该记录上的非X锁请求,则允许是用一个cursor读取一个用户记录。在一个page上supremum总是允许被读。 总的来说,一个隐式锁看起来仅仅像是在一个记录上被赋予的X锁,而不是在GAP上。一个没有设置GAP位的显示锁是一个同时在记录和GAP上的锁。如果设置了GAP位,则表明锁只在GAP上。不同的事务不可以在同一时间拥有相互冲突的锁。但可以在GAP上拥有冲突的锁。被赋予的记录锁允许操作记录的权限,但gap类型的锁仅仅禁止操作。 注意:如果一些事务在一个二级索引记录上拥有隐式x锁,这种情况比较难处理。我们可能需要查看对应聚集索引记录的前面的版本,以查找一个被标记删除的二级索引记录是否是被一个活跃的事务标记删除的,而不是一个已经提交的事务。 FACT A:如果一个事务已经插入了一条记录,它可以在任何时候删除它,无需等待任何锁。 PROOF:事务拥有插入记录在每个索引记录上的隐式X锁,并且可以修改每个索引记录而无需等待。 FACTB:如果一个事务通过一个cursor产生结果集,它可以再次读取,并获取一些结果集合,如果它没有同事修改他们的话。因此没有幻读问题。如果被cursor访问到的按照字母序的最大记录被移除,一次锁等待可能会发生,其他情况则不会。 PROOF:当一次读cursor执行时,它会对扫描过的用户记录设置S锁,以及每个page supremum上一个gap类型的S锁。cursor必须等待直到它拥有了这些锁。这样其他事务就不可以在这些用户记录上加任何X锁,也不能修改用户记录。甚至其他事务不可以插入到cursor扫奥的GAP中。Page分裂和合并,或者移除废弃版本的记录不会受此影响,因为当一个用户记录或一个Page上的supremum被移除时,下一个记录继承 了其锁作为GAP类型,因此会阻塞插入到相同的GAP中。同样的,如果一个page supremum被插入,它从成功的记录那继承它的锁。当cursor被重新放置到结果集的起始位置时,它将查看到的记录是上次查看到的或者新插入的page supremums.它能够立刻获取这些记录,当到达最大的记录时,它注意到结果集已经完成。如果最大的记录被移除,可能需要等待锁,因此下一个记录只继承了GAP类型的锁,这时候需要等待。 如果一个索引记录可以被修改或者新的插入,我们必须检查该记录和下一个记录上的锁。当一个read cusor开始读数据时,我们会在每个经过的记录上设置一个记录级别的s锁,除了在开始获取记录之前cursor所在的初始化记录。我们的索引树查找约定B-TREE上的cursor在查询时被防止在第一个可能匹配的记录前面,这里可能会有一些优化:如果记录是通过唯一索引上的等值条件查找的,我们实际上设置了一个特殊的锁在记录上,这个锁不会阻止任何在该记录之前的插入。而一个记录上的next-key x Lock同样会阻止该记录之前的插入。 每个Page上有特殊的infimum和supremum记录。一个supremum记录可以被一个read curosr锁住。这个记录无法被更新,但这个锁可以阻止将用户记录插入到该Page的用户记录尾部(也就是最大的那个记录之后) next-key锁也可以防止幻读,防止两次select的结果集不同,防止幻读保证了事务的串行化。 当需要插入一条新记录时,我们需要检查什么呢?只需要检查同一个Page上的下一条记录,因为supremum记录也会持有一个锁。一个S锁会阻止插入,但一个X锁呢?如果是由一个查询更新(searched update)加上的锁,这时候同样有一个隐式的s锁,插入会被阻止。但如果我们的事务拥有下一个记录的X锁,但在下一个记录上有一个等待的S锁请求呢?如果这个S锁是被一个在索引上升序移动的read cursor锁设置的,我们无法立刻做插入操作,因为当我们最终提交事务时,read cursor应该可以看到新插入的记录。所以我们应该跳到新插入记录的下一个记录。这种向后移动可能太难处理。如果我们正处于将在下一个记录上第二个x锁请求入队列的过程中,这时候死锁检测机制会发现我们的事务和其他持有s锁请求事务之间的死锁。这种解决方案看起来是Ok的。 […]

[MySQL 学习] Innodb锁系统(2)关键函数路径

前提: 以下分析基于标准的配置选项: tx_isolation = REPEATABLE-READ innodb_locks_unsafe_for_binlog = OFF lock->type_mode用来表示锁的类型,实际上lock->type_mode包含了几乎所有锁的模式信息,例如锁类型判断是X锁还是S锁 lock->type_mode &LOCK_TYPE_MASK LOCK_MODE_MASK 0xFUL 用于表示锁模式掩码 LOCK_TYPE_MASK 0xF0UL 用于表示锁类型,LOCK_TABLE或者LOCK_REC LOCK_WAIT 256 表示需要锁等待,还没有获得锁,只是在等待队列中等待 LOCK_ORDINARY 0 普通的next-key锁,锁记录,并锁记录前面的gap,这样可以防止幻读。 假设索引包括10,11,13,20,则next-key锁为: (negative infinity, 10], (10, 11], (11, 13], (13, 20], (20, positive infinity) 我们经常在innodb_locks表中看到的supremum pseudo-record就是锁住了最大值往后的gap. LOCK_GAP 512 只持有记录前的gap锁,例如,在一个gap上的x锁无法修改bit被设置的记录 在从索引记录链上移除记录时会加该类型的锁。 LOCK_REC_NOT_GAP 1024 也就是普通记录锁,只锁住记录,因此不会阻塞向该记录之前的gap中插入记录。 LOCK_INSERT_INTENTION 2048 插入意图锁,目的是让插入索引记录时等待,直到在gap上没有其他冲突的锁 记住,即使获得了等待的锁,也依然会持有插入意图锁 不同锁模式 enum lock_mode {     LOCK_IS = 0, /* […]

[MySQL 学习] Innodb锁系统(1)之如何阅读死锁日志

前言: 最近经常碰到死锁问题,由于对这块代码不是很熟悉,而常持有对文档怀疑的观点。决定从几个死锁问题着手,好好把Innodb锁系统的代码过一遍。 以下的内容不敢保证完全正确。只是我系统学习的过程。 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 最近有同学发现,走二级索引删除数据时,两条delete出现死锁。 我们可以保证二级索引记录是唯一的,按理说回表查到的主键记录不可能相同。死锁的现象如下所示: *** (1) TRANSACTION: TRANSACTION 1E7D49CDD, ACTIVE 69 sec fetching rows mysql tables in use 1, locked 1 LOCK WAIT 4 lock struct(s), heap size 1248, 4 row lock(s), undo log entries 1 MySQL thread id 1385867, OS thread handle 0x7fcebd956700, query id 837909262 10.246.145.78 im_mobile updating delete    from        offmsg_0007    […]

[MySQL 学习]show engine innodb status中的history list length

Percona bug#1058100提到trx_purge_add_update_undo_to_history函数,不甚了解,用gdb跟踪了下 trx_commit_off_kernel->trx_write_serialisation_history->trx_undo_update_cleanup->trx_purge_add_update_undo_to_history lsn = trx_write_serialisation_history(trx); a.首先对当前事务的undo段进行标记,以表明事务提交。 对于update undo日志,为事务分配id(trx_serialisation_number_get->trx_sys_get_new_trx_id),标记undo状态为TRX_UNDO_TO_PURGE(trx_undo_set_state_at_finish), 对于insert undo标记undo回滚段状态为TRX_UNDO_TO_FREE 如果update undo段只占用一个Page,并且使用的字节数小于TRX_UNDO_PAGE_REUSE_LIMIT(3/4的page size)时,则将undo段标记为TRX_UNDO_CACHED,表示下次可以重用该回滚段。 b.对于update undo,还需要将undo加到history list上,并做一些清理工作 trx_undo_update_cleanup(trx, undo_hdr_page, &mtr); (1)trx_purge_add_update_undo_to_history(trx, undo_page, mgr); 当undo 段的state不为TRX_UNDO_CACHED时,需要更新undo段的history list length,然后将undo加入到改回滚段的TRX_RSEG_HISTORY的链表上(不太了解undo这部分相关的逻辑),将事务号写入undo_header + TRX_UNDO_TRX_NO中,并在TRX_UNDO_DEL_MARKS标注是否存在标记删除记录(需要purge) 在kernel_mutex的保护下trx_sys->rseg_history_len++; 我们在show engine innodb status里看到的History list length实际上就是trx_sys->rseg_history_len值。 由于该函数总是在事务提交时才被调用到,因此我们也可以把history list lengh理解为尚未被清理update undo的事务数.在update/delete为主的工作负载中,可能会看到length明显的增大。 这里有一段被注释的代码,也是Percona bug#1058100提出的质疑 //  if (!(trx_sys->rseg_history_len % srv_purge_batch_size)) { /*should wake up always*/         /* Inform the […]