Mysql Innodb 源代码调试跟踪分析 何登成


Mysql Innodb 源代码调试跟踪分析 何登成 1 早期结论 ................................................................................................................................... 4 2 测试一:死锁检测 .................................................................................................................... 5 3 测试二:CURSOR 测试 .............................................................................................................. 7 4 测试三:EXTERNAL_LOCK 测试 ................................................................................................ 7 5 测试四:杂项测试 .................................................................................................................... 8 6 测试五:AUTOCOMMIT 测试 .................................................................................................... 9 7 测试六:UNLOCK TABLES 测试 ............................................................................................... 10 8 测试七:锁等待超时测试 ....................................................................................................... 11 9 测试八:STORE_LOCK 函数 ..................................................................................................... 12 10 测试九:INNODB 两阶段提交 ............................................................................................ 13 10.1 AUTOCOMMIT = ON .................................................................................................................... 13 10.2 AUTOCOMMIT = OFF .................................................................................................................... 16 10.3 FLUSH_AT_TRX_COMMIT 参数处理 ............................................................................................. 16 10.4 INNODB GROUP COMMIT ............................................................................................................. 17 11 测试十:INNODB CRASH RECOVERY ................................................................................... 19 11.1 RECOVERY 的三种模式 ............................................................................................................. 20 12 测试十一:INDEX COVERAGE SCAN? ................................................................................ 23 13 测试十二:MINI TRANSACTION .......................................................................................... 23 14 测试十三:事务开始 .......................................................................................................... 23 14.1 AUTOCOMMIT=ON ..................................................................................................................... 23 14.2 AUTOCOMMIT=OFF .................................................................................................................... 24 14.3 INNODB 内部事务 .................................................................................................................... 24 15 测试十四:INSERT IGNORE 测试 ........................................................................................ 25 16 测试十五:AUTO_INCREMENT............................................................................................ 25 17 测试十六:数据格式转换 ................................................................................................... 29 18 测试十七:INNODB 加载表数据字典 ................................................................................. 29 19 测试十八:SCAN 测试 ........................................................................................................ 30 20 测试十九:加锁等待 .......................................................................................................... 32 21 测试二十:MYSQL 定位 TABLE ........................................................................................... 34 22 测试二十一:如何做 JOIN .................................................................................................. 34 23 测试二十二:LATCH & LOCK HOLDING LATCH ..................................................................... 35 24 测试二十三:MYSQL 上层加锁逻辑 ................................................................................... 37 25 测试二十四:GET_SHARE & FREE_SHARE ........................................................................... 37 26 测试二十五:INSERT ON DUPLICATE UPDATE ..................................................................... 38 27 测试二十六:PURGE 测试 .................................................................................................. 40 28 测试二十六(CONT.): PURGE 测试续 .................................................................................... 41 29 测试二十七:BLOB & BLOB PURGE ..................................................................................... 42 30 测试二十八:HA_READ_KEY_EXACT ................................................................................... 43 31 测试二十九:OFFLINE_DDL/FAST_IDX_CREATE................................................................... 45 32 测试三十:PARTITION & INNOPLUGIN ................................................................................ 48 33 测试三十一:VS 2008 + MYSQL5.5 ..................................................................................... 49 34 测试三十二:NTSE ONLINE ADD INDEX .............................................................................. 50 35 测试三十三:GROUP LOG WRITE & FLUSH ......................................................................... 51 36 测试三十三(CONT.): MUTEX & EVENT ............................................................................ 53 37 测试三十四:INNODB READVIEW 测试 .............................................................................. 56 38 测试建表三十五: UTF8 21845 VS 21846 .............................................................................. 57 39 测试三十六:INNODB 表元数据并发控制 ......................................................................... 58 40 测试三十七:NTSE 引擎 TABLE 模块 .................................................................................. 62 41 测试三十八:TRUNCATE VS DROP ...................................................................................... 65 42 测试三十九:加锁逻辑 INNODB VS NTSE .......................................................................... 65 43 测试四十:MYSQL+NTSE 实现 UPDATE .............................................................................. 66 44 测试四十一:HALLOWEEN,RBR........................................................................................ 67 45 测试四十二:INNODB 无主键表 ........................................................................................ 70 46 测试四十三:INNODB 处理 UTF8 ....................................................................................... 71 47 测试四十四:SYNC_ARRAY THREAD_CONCURRENCY ......................................................... 71 48 测试四十五:MYSQL+INNODB STATISTICS .......................................................................... 73 49 测试四十六:INNODB 处理 DELETE .................................................................................... 78 50 测试四十七:INFIMUM AND SUPREMUM .......................................................................... 79 51 测试四十八:事务 & DDL .................................................................................................. 79 52 测试四十九:SMO .............................................................................................................. 79 52.1 PAGE ALLOCATION....................................................................................................................... 81 52.2 RECORDS MOVE ......................................................................................................................... 82 本文档主要用于分析 Innodb 源代码。 目的: 设计 TNT 事务型引擎,作为参考 实验: create table tlock (id int primary key, comment varchar(200)); insert into tlock values(1, ‘aaaaaaaaaaaaaaaaa’); insert into tlock values(2, ‘bbbbbbbbbbbbbbb’); insert into tlock values(100, ‘zzzzzzzzzzzzzzzzzz’); insert into tlock values(1000,’AAAAAAAAAAAA’); 版本: mysql select version(); 5.1.49-debug-log innodb 早期收获: 一、基本了解 innodb 锁表模块功能,与 mysql 交互,表锁,行锁,加锁,放锁,死锁 检测,函数调用逻辑,TNT 锁表模块可以参考。 二、基本了解 innodb 事务模块功能,与上层交互接口,调用方式,事务提交选择,TNT 事务模块原型已经有底。 三、基本了解 innodb XA 事务,crash recovery 流程,与上层接口交互,恢复逻辑,TNT 支持 binlog 的二阶段提交,恢复功能可以做出。 四、了解部分函数功能,测试版本 innodb 的不足之处,可以在实现 TNT 引擎过程中, 加以避免。 五、其他… 1 早期结论 测试结果比较乱,看起来会比较累,但是基本上说明了 innodb 的事务/加锁/二阶 段提交/crash recovery 流程。对设计 TNT 引擎,十分有帮助。 1. 时间点选择 a) 表锁,在 statement 第一次取记录前加(LOCK_IS, LOCK_IX);external_lock 函数, 仅仅维护表计数(上层 mysql 对表加锁的计数),而不是真正加锁。 b) 行锁,根据模式,对记录加锁(LOCK_S, LOCK_X) c) 在加行锁之前,必须保证表级意向锁已经加上 d) autocommit = ON,Innodb Lock tables 不做任何操作,不对表加锁;但是 mysql 上层会对表加锁。 e) autocommit=OFF,Innodb Lock tables 对表加锁(LOCK_S, LOCK_X);同时 mysql 上层也会对表加锁。 f) mysql 上层会加表锁,而且保证加锁不会产生死锁;innodb 执行 statement 过 程中,只会加 LOCK_IS,LOCK_IX 表锁锁,不会加 LOCK_S,LOCK_X 表锁;LOCK_S, LOCK_X 表锁,只会在 autocommit=OFF 时,发出 LOCK TABLES指令是才加。innodb 没有锁升级。 g) 表意向锁(LOCK_IS,LOCK_IX)的加锁,延迟到 statement 取第一条记录之前。 2. external_lock 函数功能 a) external_lock 函数,顾名思义,外部的锁。对于 innodb 来说,外部的锁就是 上层 mysql 的锁。statement 开始时,mysql 上层会对 statement 涉及到的表加 锁,同时调用 external_lock 函数通知 innodb(每个表都调用一次),external_lock 函数记录下上层加表锁的数量;statement 结束时,mysql 上层释放 statement 涉及到的表锁,同时调用 external_lock 函数通知 innodb,每调用一次,计数 减减,当计数到 0 时,根据当前 autocommit 设置,判断是否需要自动提交事 务(因为 mysql 上层并不会自动调用 commit 函数,触发事务提交)。 3. 功能测试 a) 加锁流程,每个测试都有加锁流程 b) commit,放锁流程,详见测试 4,测试 6 c) 事务开始时与 mysql 的交互,详见测试 13 d) 死锁检测流程,详见测试 1 e) Lock 节点组织、定位,详见测试 4 f) autocommit 参数的影响,详见测试 4,测试 5 g) lock tables & unlock tables,详见测试 6 h) 加锁等待超时 vs 不超时,详见测试 7 i) store_lock 函数功能,见测试 8,不详尽 j) innodb 二阶段提交支持,见测试 9 k) innodb 的 crash recovery,见测试 10 l) innodb 如何实现 auto increment,见测试 15 m) mini transaction 的功能,见测试 12 4. Innodb 不足之处 a) 在我测试的版本中,一个 kernel mutex,保护 server,trx,query threads,lock table, 保护的资源太多,会是瓶颈之一。 b) Innodb 二阶段提交,开启 binlog,group commit 就被自动禁用。极大的增加了 fsync 调用,降低了并发系统性能,prepare_commit_mutex。 c) 死锁检测做的不是很高效。(当然这也与多版本并发控制有关,加锁概率小, 锁不会太多) d) 放锁时,唤醒操作也不是很高效。(挨个遍历需要唤醒的 Lock,每个 lock 又需 要与链表前面的 lock 比较是否冲突) e) 在发出 lock tables 命令之后,select … lock in share mode 仍旧需要对行记录加 锁 f) 锁等待超时innodb_lock_wait_timeout就报错返回,有时会对用户造成困扰 (当 然,mysql 的应用环境下,都是短小事务,遇见此报错的概率很小) 2 测试一:死锁检测 insert into tlock values(1000,’AAAAAAAAAAAAAAAAAAAAAA’); 目的:测试 autocommit 事务的加放锁 收获: i. thd->options = 10000000000001000100101000000000 mysql_priv.h line:446 ii. 事务开始时,需要注册到 mysql iii. mysql 事务有两个链表,thd->transaction.all,thd->transaction.stmt autocommit 事务,注册到 stmt 链表 iv. dict_table_struct 结构,dict0mem.h line:288 数据库表数据结构; dict_table_struct->locks:list of locks on the table lock_struct 结构, row_lock_table_for_mysql();Sets a table lock on the table mentioned in prebuilt lock_table();Locks the specified database table in the mode given. lock_mutex_enter_kernel();一个 kernel mutex,保护 server,trx,query threads, lock table,管理的资源太多,会是瓶颈之一。 lock_table_enqueue_waiting(mode,table,thr);尝试加的表模式与已有的冲突,等待;需 要进行死锁检测。 lock_table_create(table,mode|LOCK_WAIT,trx);创建需要等待的表锁结构 lock_deadlock_occurs(lock,trx); lock_deadlock_recursive(trx,trx,wait_lock,cost,depth);死锁检测主函数,递归函数。 参数说明: trx_t* start, /* in: recursion starting point */ trx_t* trx, /* in: a transaction waiting for a lock */ lock_t* wait_lock, /* in: the lock trx is waiting to be granted */ ulint* cost, /* in/out: number of calculation steps thus far: if this exceeds LOCK_MAX_N_STEPS_... we return LOCK_EXCEED_MAX_DEPTH */ ulint depth) /* in: recursion depth: if this exceeds LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK, we #define LOCK_MAX_N_STEPS_IN_DEADLOCK_CHECK 1000000 #define LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK 200 功能流程 (构造 Wait-For-Graph 流程,包含行锁/表锁的等待图 WFG): a) 从当前 wait_lock 开始,向前定位前一项 lock (表锁:UT_LIST_GET_PREV 行锁: lock_rec_get_prev) b) 如果 wait_lock 不必等待当前 lock,继续取当前 lock 的前一项 lock,设置为当前 lock, 继续判断 b) c) 如果 wait_lock 等待当前 lock,判断当前 lock 所属事务不为 start 事务(未产生死锁 环) a) 如果 lock 所属事务继续等待,则递归调用 lock_deadlock_recursive 函数,参 数 start 保持不变,其余参数换为当前事务参数 b) 如果 lock 所属事务不等待,则继续取当前 lock 的前一项 lock,继续判断 b) d) 如果当前 lock 所属事务为 start 事务(产生死锁环) a) 设置死锁信息 b) 选择死锁牺牲者,trx_weight_cmp,若 start 更轻量级,则选择 start c) 选择当前 lock 所属事务为牺牲者,唤醒牺牲事务,返回 LOCK_VICTIM_IS_OTHER d) 由于选择了其他事务作为牺牲者,因此函数 lock_deadlock_occurs 需要 retry, 继续判断当前事务是否仍旧包含死锁 v. 以上流程来自于代码阅读,测试一不会产生以上流程。 set autocommit = ‘off’; select * from tnew where id = 100 for update; innodb 处理锁等待的函数调用流程: ha_innobase::index_read -> row_search_for_mysql -> row_mysql_handle_errors(trx->error_state == DB_LOCK_WAIT) -> srv_suspend_mysql_thread -> os_event_set(srv_lock_timeout_thread_event : 唤 醒 锁 超 时 检 测 线 程 , why ?) -> srv_table_reserve_slot_for_mysql(从初始化好的 srv_mysql_table 中,拿到一个未被使用的 slot, 使 用 其 中 的 event 进 行等待) -> os_event_reset(slot->event) -> os_event_set(srv_lock_timeout_thread_event:唤醒锁超时线程) -> os_event_wait(slot->event: 事务进入等待,超时返回) -> 在发生 deadlock 时,InnoDB 层面首先回滚整个事务,然后将 rollback 信息通知 MySQL (调用 thd_mark_transaction_to_rollback(thd, all)方法,all = true 时回滚整个事务;false 回滚当前语 句)。 语句由于 DEAD_LOCK 出错,返回 MySQL 上层之后,MySQL 接下来还是会先回滚当前语句; 然后再是回滚整个事务(由于已经设置了 thd_mark_transaction_to_rollback 的 all 参数为 true); 由于事务在 InnoDB 中已经被回滚,因此这些调用都不需要实际操作,但是需要处理这些调 用的情况。 3 测试二:cursor 测试 create table tlock2 (id int primary key, comment varchar(300)); begin; 4 测试三:external_lock 测试 select * from tlock for update; external_lock 函数: prebuilt->sql_stat_start = TRUE; trx->mysql_n_tables_locked++; trx->n_mysql_tables_in_use++; prebuilt->mysql_has_locked = TRUE; 设置参数,不实际加锁。mysql 上层已经完成实际加锁动作。 设置 prebuilt->sql_stat_start = TRUE,在第一次取记录时,需要对表加意向锁(行锁:LOCK_S-> 表锁:LOCK_IS;行锁:LOCK_X->表锁:LOCK_IX) rnd_next->index_first->index_read->row_search_for_mysql->lock_table prebuilt->sql_stat_start = 1 && prebuilt->select_lock_type = 5 (LOCK_X) -> lock_mode = LOCK_IX lock_table && set prebuilt->sql_stat_start = FALSE; 5 测试四:杂项测试 autocommit = ON; select * from tlock where id = 1 for update; 测试 innodb commit 流程: 1. autocommit 设置下,external_lock 函数中,如果 trx->n_mysql_tables_in_use 降为 0,将自动 触发 commit innobase_commit -> innobase_commit_low -> trx_commit_for_mysql -> mutex_enter(&kernel_mutex)(再 次用到kernel_mutex) –> trx_commit_off_kernel -> TRX_COMMITTED_IN_MEMORY -> lock_release_off_kernel(Releases transaction locks, and releases possible other transactions waiting because of these locks.) -> lock_rec_dequeue_from_page -> lock_table_dequeue autocommit = OFF; select * from tlock for update; commit; 2. autocommit = off 时,statement 执行完,调用 external_lock 函数,但是不会触发 commit, 需要用户手动发出 commit 命令 sql_parse.cc::mysql_execute_command -> end_trans -> handler.cc::ha_commit_trans -> … 3. commit 后,如果有必要,需要唤醒等待中的 wait_lock,唤醒操作的流程(以行锁为例): 从第一个 lock 节点开始向后遍历,针对每一个遇到的 lock 节点,如果其 mode && LOCK_WAIT 为 TRUE,则判断其是否与前面的 lock 冲突,如果仍旧没有冲突,则获得锁; 否则需要继续等待 (等待超过 innodb_lock_wait_timeout 参数,报错返回)。 lock0lock.c::lock_rec_dequeue_from_page -> lock_rec_get_first_on_page_addr -> lock_get_wait(LOCK_WAIT ?) -> lock_rec_has_to_wait_in_queue (still need to wait ?) -> lock_grant -> lock_rec_get_next_on_page 4. lock 节点定位 a) 属于同一事务的 lock,存于 trx 的链表之中,trx->trx_locks (同时包括行锁,表锁) b) 锁模式(表锁 or 行锁),通过 lock_struct 结构中的 type_mode 字段区分,union un_member c) 表锁,还存于表数据字典中,dict_table_t->locks /* list of locks on the table */ d) 行锁,还存于 hash 表之中,lock_sys_struct->rec_hash(hash_table_struct) e) 行锁到 hash 表的映射,通过 record 的 space_id,page_no 计算得来 page = buf_frame_align(rec); space = buf_frame_get_space_id(page); page_no = buf_frame_get_page_no(page); return(hash_calc_hash(lock_rec_fold(space, page_no), lock_sys->rec_hash)); 5. -> trx_roll_free_all_savepoints 6 测试五:autocommit 测试 Autocommit = 1; http://www.bhcode.net/article/20090227/4247.html mysql 中的 lock tables 和 unlock tables http://dev.mysql.com/doc/refman/5.0/en/lock-tables.html lock tables and unlock tables syntax http://dev.mysql.com/doc/refman/5.0/en/innodb-locking-reads.html select … for update and select … lock in share mode session1:lock table tlock read; session2:lock table tlock write; session1 先执行,session2 后执行,此时 session2 会等待 session1 释放表锁;此时的等 待,是 mysql 上层的锁定等待,而不是 innodb 的锁定等待。Innodb 在 autocommit=ON 的设 置下,调用 lock tables 并不会对表加表锁。但是上层 mysql 还是会对表加锁,因此 session2 的等待,是发生在 mysql 层面的锁等待。 thd->options = 10000000001001000100101000000000 lock table tlock read; lock table tlock write; external_lock: 不实际加锁 select * from tlock for update; start_stmt: 不实际加锁 rnd_next: 加表锁 (mode = LOCK_IX) Autocommit = 0; show variables like '%autoc%'; set autocommit = 'off'; lock table tlock read; external_lock: 对表加 LOCK_S 锁 select * from tlock lock in share mode; rnd_next: 对表加锁 (LOCK_IS),但是由于 LOCK_S 锁已存在,直接返回。 sel_set_rec_lock: 对行加锁(LOCK_S),虽然表上有 LOCK_S 锁,但是行锁仍旧要加。 lock_clust_rec_read_check_and_lock() lock table tlock write; external_lock: 对表加 LOCK_X 锁 select * from tlock for update; start_stmt: rnd_next: 对表加 LOCK_IX 锁 总结: Innodb 引擎,在 autocommit=ON 的情况下,调用 Lock tables 并不会锁定表 (上层 mysql 对于表的锁定不算);在 autocommit=OFF 的情况下,调用 Lock tables 会在函数 external_lock 中显示对表加锁(LOCK_S or LOCK_X)。 7 测试六:unlock tables 测试 session1: session2: set autocommit = ‘off’; lock tables tlock write; select * from tlock for update; unlock tables; unlock tables 命令是否会引起 session1 commit?如果 commit,调用流程如何? unlock tables sql_parse.cc::mysql_execute_command -> end_active_trans -> handler.cc::ha_commit_trans -> ha_commit_one_phase -> ha_innodb.cc::innobase_commit -> innobase_commit_low -> trx0trx.c::trx_commit_for_mysql -> mutex_enter(&kernel_mutex) -> trx_commit_off_kernel -> lock0lock.c::lock_release_off_kernel -> lock_table_dequeue -> lock_rec_dequeue_from_page end_active_trans 说明: /* It is critical for mysqldump --single-transaction --master-data that UNLOCK TABLES does not implicitely commit a connection which has only done FLUSH TABLES WITH READ LOCK + BEGIN. If this assumption becomes false, mysqldump will not work. */ case SQLCOM_UNLOCK_TABLES: unlock_locked_tables(thd); if (thd->options & OPTION_TABLE_LOCK) { end_active_trans(thd); thd->options&= ~(OPTION_TABLE_LOCK); } 由于unlock tables命令会暗中执行commit事务的操作,因此unlock tables命令之后,无论是mysql 上层的表锁,还是innodb中的表锁,行锁都会被释放,事务被提交。 同理,lock tables命令也会暗中执行commit事务操作,具体可阅读 sql_parse.cc::mysql_execute_command(THD *thd)函数,对于每种命令,都有执行方案,了解mysql对于 不同命令的执行,才能清楚知道命令执行之后的各种情况。 8 测试七:锁等待超时测试 结论: innodb 加锁,会等待超时,由参数 innodb_lock_wait_timeout 控制;但是 mysql 上层加 锁不会等待超时。 测试 Innodb 加锁等待 session1: session2: set autocommit = ‘off’; select * from tlock where id = 1 for update; select * from tlock where id = 1 for update; session2 在执行之后,需要等待 session1 的锁。如果 session1 超过 50s 不提交,session2 等 待超时,返回错误。 show variables like ‘%innodb%’; | innodb_lock_wait_timeout | 50 | 测试 mysql 上层加锁等待 session1: session2: lock tables tlock read; select * from tlock; select * from tlock where id = 1 for update; session2 等待 mysql 上层表锁释放,此时等待并不会超时返回,而是一直忙等下去。接着, session1 发出以下命令。 session1: commit; unlock tables; session1 commit 后,由于 commit 并不会释放 mysql 上层表锁,因此 session2 继续等待。 session1 unlock tables 命令之后,mysql 上层释放表锁,session2 被唤醒执行成功。 mysql> select * from tlock where id = 1 for update; +----+-----------------------------+ | id | comment | +----+-----------------------------+ | 1 | aaaaaaaaaaaaaaaaaaaaaaaaaaa | +----+-----------------------------+ 1 row in set (9 min 36.85 sec) session2 总共等待了 9 分钟之多,并没有因为等待超过 innodb_lock_wait_timeout = 50S 而失 败。 9 测试八:store_lock 函数 测试 store_lock 函数功能 store_lock 函数定义如下: THR_LOCK_DATA** ha_innobase::store_lock(THD* thd, THR_LOCK_DATA** to, enum thr_lock_type) 功能: 猜测:改变传入参数 thr_lock_type 的取值,然后将其存入 THR_LOCK_DATA 中 实证: 1. 根据命令,改变传入的 lock_type 的取值,目的是达到更高的并发度 2. 将 innobase handler 层面的 THR_LOCK_DATA lock.type 赋值,然后存入变量 to 中 3. 设置本层加锁模式,prebuilt->select_lock_type,此值也会在 external_lock 中调整 session 1: set autocommit = ‘off’; select * from tlock; lock_type = TL_READ;保持不变; select * from tlock lock in share mode; lock_type = TL_READ_WITH_SHARED_LOCKS;保持不变 select * from tlock for update; lock_type = TL_WRITE ——》 lock_type = TL_WRITE_ALLOW_WRITE; update tlock set comment = ‘poip’ where id = 789; lock_type = TL_WRITE ——》 lock_type = TL_WRITE_ALLOW_WRITE; insert into tlock values (7689, ‘zxcvqr’); lock_type = TL_WRITE_CONCURRENT_INSERT ——》 lock_type = TL_WRITE_ALLOW_WRITE; create table tlock10 engine=innodb as select * from tlock; 原表:lock_type = TL_READ_NO_INSERT ——》 lock_type = TL_READ; 新表:lock_type = TL_WRITE;保持不变;extrenal_lock: prebuilt->select_lock_type = LOCK_X; alter table drop column gmt_create; First call: lock_type = TL_WRITE_ALLOW_READ;保持不变; Second call: lock_type = TL_IGNORE; lock tables tlock10 write; lock_type = TL_WRITE;保持不变; truncate table tlock10; lock_type = TL_WRITE;保持不变; drop table tlock10; lock_type 上层定义 enum thr_lock_type { TL_IGNORE=-1, TL_UNLOCK, TL_READ_DEFAULT, TL_READ, /* Read lock */ TL_READ_WITH_SHARED_LOCKS, TL_READ_HIGH_PRIORITY, TL_READ_NO_INSERT, TL_WRITE_ALLOW_WRITE, TL_WRITE_ALLOW_READ, TL_WRITE_CONCURRENT_INSERT, TL_WRITE_DELAYED, TL_WRITE_DEFAULT, TL_WRITE_LOW_PRIORITY, TL_WRITE, TL_WRITE_ONLY}; lock_type 冲突关系: 参考 thr_lock.c 中的 thr_lock 函数。 其他知识点: 1. thd_in_lock_tables():该函数判断当前语句是否在 lock tables 语句之内调用 2. 10 测试九:Innodb 两阶段提交 测试 Innodb 的二阶段 commit。 10.1 autocommit = ON autocommit = ON update tlock set comment = 'aaaaaaaaa' where id = 1; 总流程 sql_parse.cc::dispatch_command -> handler.cc::ha_autocommit_or_rollback -> handler.cc::ha_commit_trans -> innobase_xa_prepare -> handler.cc::ha_commit_one_phase -> innobase_commit innobase_xa_prepare 流程 pthread_mutex_lock(&prepare_commit_mutex) -> innobase_release_stat_resources(trx) -> trx_prepare_for_mysql(trx) -> mutex_enter(&kernel_mutex) -> trx_prepare_off_kernel -> mtr_start -> mutex_enter(&(rseg->mutex))( The rollback segment memory object) -> mtr_commit -> trx_undo_set_state_at_prepare(Sets the state of the undo log segment at a transaction prepare.) -> log_write_up_to(must_flush_log = 1) -> log_group_write_buf -> log_flush_do_unlocks(set the flush signal) 一、 prepare 开始的第一件事,就是获取 MySQL 层面的 xid,并写入 prepare 日志中。此 xid 在事务 prepare 完成,commit 未完成时,需要返回给 MySQL binlog,用于控制事务的最终 commit or rollback。 二、 若为语句级的 prepare(所谓语句级,指的是当前是 非 autocommit 模式,或是 autocommit 模式,但 是用户显示 begin;此时语句结束,MySQL 上层仍旧会调用 prepare-commit 序列,但是针对的是当前 语句,而非整个事务。整个事务,需要用户显示调用 commit 进行提交),则不进行真正的 prepare, 只需要释放 AutoInc 锁,在事务中标识语句结束的状态即可(记录当前语句的最新 log_lsn)。 三、 prepare 将日志写回日志文件,并且刷到磁盘(可控制是否刷)。 四、 在写回日至之前,需要将日志的状态设置为 prepare 。 由函数(mtr_start -> mutex_enter(&(rseg->mutex)) -> trx_undo_set_state_at_prepare -> trx_undo_set_state_at_prepare -> mtr_commit)完成。 五、 Innodb 支持 group log write&flush。函数 log_write_up_to,获得写日志权限的线程,将日志写到 当前最大 lsn 为止(log_sys->lsn);需要刷磁盘的话,也将日志刷到最大 lsn;如此一来,如果同时 有 N 个线程等待写日志,那么获得权限的线程就帮助其余 N-1 个线程完成了工作,其余的线程进入临 界区,等待当前线程完成即可返回。 六、 flush_at_trx_commit 参数处理,过程如下:参数处理函数 七、 开启 binlog,group commit 就被自动禁用(外部 XA 不禁用 group commit,因为 prepare 之后多久才 commit 由人为控制,极可能出现忙等现象)。为了保证底层 innodb 提交与上层 mysql binlog 提交的 一致性,prepare 前需要获得 prepare mutex。pthread_mutex_lock(&prepare_commit_mutex)。 prepare_commit_mutex 在 commit 完成之后释放。因此也禁止了前面提到的 group log write 支持, 因为同一时刻,只有一个线程可以做 prepare,commit binlog,commit 的动作。(在 MySQL 5.5.16 中,prepare_commit_mutex 在 prepare 操作完成之后再加上,相对于 5.1 允许了 group prepare,但 是仍旧是串行的 commit binlog -> commit 序列)。 binlog_commit 流程 handler.cc::ha_commit_one_phase -> log.cc::binlog_commit -> binlog_end_trans innobase_commit 流程 http://www.mysqlperformanceblog.com/2006/06/05/innodb-thread-concurrency/ --InnoDB thread concurrency handler.cc::ha_commit_one_phase -> innobase_commit -> pthread_cond_wait(&commit_cond,&commit_cond_m) -> trx_commit_for_mysql -> trx_commit_for_mysql -> trx->sess == NULL(If we are doing the XA recovery of prepared transactions, then the transaction object does not have an InnoDB session object) -> mutex_enter(&kernel_mutex) -> trx_commit_off_kernel -> mtr_start -> trx_undo_set_state_at_finish -> mtr_commit -> log_write_up_to 说明: 1. bool is_real_trans=all || thd->transaction.all.ha_list == 0; autocommit statement 不进入 transaction.all 链表 2. 同 prepare,若当前是语句级的 commit,那么不真正进行 commit 操作,是否 AutoInc 锁,记录当前 日志位置即可。 3. 获取 commit 事务对应的 MySQL 上层 binlog 最新文件的[文件名,位置]信息,并将此信息写入系统表 空 间 的 第 五 个 page[TRX_SYS_SPACE, TRX_SYS_PAGE_NO(FSP_TRX_SYS_PAGE_NO)] 的 TRX_SYS(FSEG_PAGE_DATA)处(也就是一个页面存放数据的起始位置,38)。 4. 于此同时,修改系统页面的操作被封装为一个 mini transaction,同样需要记录日志,将此日志写入 日志文件,并且 fsync,就能够保证整个 commit 操作的完成。此时哪怕系统崩溃,系统页面没有 flush 到 disk,通过日志仍然能够 redo 系统页面到最新的[binlog 文件名,binlog 位置]组合。 5. [binlog 文件名,binlog 位置]组合何用?在 InnoDB 系统完成 crash recovery 之后,会读取系统页 面 中 最 新 的 binlog 信 息 到 [trx_sys_mysql_bin_log_name, trx_sys_mysql_bin_log_pos] (trx_sys_print_mysql_binlog_offset())。而这两个全局参数,通过 静 态 方 法 get_mysql_bin_log_name/pos 可以返回给 MySQL 上层。但是问题在于,目前未看到 MySQL 上层对于这 两个方法的调用。无论是 MySQL 的 HotBackup,还是 Percona 的 XtraBackup,都需要短暂锁库,读取 Binlog 位置信息,用作 slave 的恢复起点,很奇怪为什么不直接用这个已经在 InnoDB 中存在多时的 Binlog 信息。至少在搭建备库时,不需要锁库的操作了,做到真正意义上的 HotBackup。 6. prepare_commit_mutex 释放 if (trx->active_trans == 2) pthread_mutex_unlock(&prepare_commit_mutex); 如果是二阶段提交,那么在 commit 完成之后,释放 prepare_commit_mutex 7. autocommit = on 时,更新事务(update/delete/insert)自动调用 ha_autocommit_or_rollback 函数 进行提交 or 回滚,然后才是调用 external_lock 函数。 dispatch_command(); ha_autocommit_or_rollback(); // 提交事务,同时清空 transaction.stmt 链表 close_thread_tables(); external_lock(); // 由于事务已提交,因此此处不需要实际操作 8. 快照读事务,MySQL 上层不会调用 commit,因此需要在 external_lock 函数中提交;当前读事务(select lock in share mode/select for update),同样在 external_lock 函数中提交事务,但是 MySQL 上 层接下来会调用 ha_autocommit_or_rollback 函数。 do_select(); // 对于非更新命令(非 IUD),在 do_select 函数中释放所有 cursor send_eof(); // 此函数,调用 innodb 的 external_lock,自动触发事务提交动作 mysql_unlock_tables(); external_lock(); ha_autocommit_or_rollback(); // 由于事务已提交,因此此处需要做的,就是将事务从 // transaction.stmt,transaction.all 链表中摘除即可 close_thread_tables(thd); 9. innobase_commit ,在调用 log_write_up_to 函数 write&flush 日志之前,调用函数 lock_release_off_kernel 释放事务持有的所有锁(表锁/行锁),此时事务的提交状态是内存提交 (trx->conc_state = TRX_COMMITTED_IN_MEMORY),虽然违背了在日志被回刷前,更新不能够被其他 事务看到的约定,但是能够保证正确性。日志 flush 之后,事务状态被重置(trx->conc_state = TRX_NOT_STARTED). 10. MySQL 5.5.16 相对于 MySQL 5.1,在 commit 时做了很多优化。例如:持有 prepare_commit_mutex 时 写 commit 日志,但是不做 fsync,写完 commit 日志之后,释放 prepare_commit_mutex,然后做 fsync, 此时可以做 commit 的 group commit。能这么做的原因在于,MySQL binlog 模块做了改造(提交时调 用 log_xid 函数),在 binlog 切换前,必须保证前一个 binlog 文件对应的 prepare 事务全部完成提 交。MariaDB 的 group commit 方案,很大一部分沿用了 MySQL 5.5 中的改造,并在此基础上实现了真 正意义上的 group commit。 10.2 autocommit = off set autocommit = ‘off’; update tlock set comment = 'abcdefghijklmnopqrstuvwxyz' where id = 1; First round:statement 执行结束 innobase_xa_prepare: if (all || (!thd_test_options(thd, OPTION_NOT_AUTOCOMMIT | OPTION_BEGIN))) // 所有statement同时提交,或者当前statement结束且是autocommit else trx_mark_sql_stat_end(trx); // 保存当前undo位置,作为savepoint binlog_commit: // cache 当前 statement,但是不提交 binlog trx_data->at_least_one_stmt_committed = my_b_tell(&trx_data->trans_log) > 0; if (!all) trx_data->before_stmt_pos = MY_OFF_T_UNDEF; // part of the stmt commit innobase_commit: row_unlock_table_autoinc_for_mysql(trx); trx_mark_sql_stat_end(trx); 总结:在 autocommit = ‘off’ && statement 结束时,prepare,binlog_commit,commit 主要的 功能就是保存 savepoint 点,不做其他功能性操作。 Second round:发出 commit 命令 innobase_xa_prepare: 与 autocommit = on 时的 statement 结束处理一致 innobase_commit: 与 autocommit = on 时的 statement 结束处理一致 10.3 flush_at_trx_commit 参数处理 trx0trx.c:: trx_prepare_off_kernel(trx) if (srv_flush_log_at_trx_commit == 0) { /* Do nothing */ } else if (srv_flush_log_at_trx_commit == 1) { if (srv_unix_file_flush_method == SRV_UNIX_NOSYNC) { /* Write the log but do not flush it to disk */ log_write_up_to(lsn, LOG_WAIT_ONE_GROUP, FALSE); } else { /* Write the log to the log files AND flush them to disk */ log_write_up_to(lsn, LOG_WAIT_ONE_GROUP, TRUE); } } else if (srv_flush_log_at_trx_commit == 2) { /* Write the log but do not flush it to disk */ log_write_up_to(lsn, LOG_WAIT_ONE_GROUP, FALSE); } else { ut_error; } 10.4 InnoDB group commit 在前面的分析中提到,当开启 binlog 时,为了保证 InnoDB 日志与 mysql binlog 日志的顺序 一致性,在 prepare 前需要获取 mutex,直到 commit 完成之后释放,这也禁用了 group commit 的功能。 对于 InnoDB group commit 的分析,可参考系列文章:Mysql/InnoDB 和 Group Commit(1); Mysql/InnoDB 和 Group Commit(2) 对于 MariaDB / Percona XtraDB 如何实现 Group commit,可参考 MariaDB 网站的 3 篇 WorkLog: 1. WL#116: Efficient group commit for binary log 2. WL#132: Transaction coordinator plugin 3. WL#164: Extend crash recovery to recover non-prepared transactions from binlog Percona 5.5.19-rel24 代码分析: prepare_ordered 功能: Log.cc:: MYSQL_BIN_LOG::write_transaction_to_binlog_events(group_commit_entry *entry) mysql_mutex_t LOCK_group_commit_queue; 事务由 mutex 保护,加入 queue,第一个加入 queue 的事务负责余下操作;其余事务进入等 待。 if (orig_queue != NULL) entry->thd->wait_for_wakeup_ready(); else trx_group_commit_leader(entry); commit_ordered 功能: ha_innodb.cc::innobase_init -> innobase_hton->commit_ordered = innobase_commit_ordered XA 事务总流程: handler.cc::ha_commit_trans()函数分析 for (Ha_trx_info *hi = ha_info; hi; hi = hi->next()) handlerton *ht = hi->ht(); // XA 事务第一阶段,group fsync prepare 日志 // 其中,binlog 的 prepare 方法为空实现 err = ht->prepare(); need_commit_ordered |= (ht->commit_ordered != NULL); // Binlog group commit,然后调用 commit_ordered 进行排序 cookie = tc_log->log_and_order(); // XA 事务第二阶段,fsync commit 日志 error = commit_one_phase_low(); TC_LOG_BINLOG::log_and_order()函数分析 binlog_commit_flush_stmt/trx_cache(); binlog_flush_cache(); write_transaction_to_binlog(); // 事务首先进入 binlog group commit queue // 第一个进入的事务进行 binlog group commit // 其余事务进入等待 write_transaction_to_binlog_events(); trx_group_commit_leader(); (entry->thd->wait_for_wakeup_ready()) … // binlog 完成 fsync 之后,调用此函数对事物进行排序 // 排序前,需要获取 LOCK_commit_ordered mutex run_commit_ordered(); for ( … ) ht->commit_ordered(); // InnoDB 提供了此函数,实现排序功能 innobase_commit_ordered(); Log.cc::MYSQL_BIN_LOG::trx_group_commit_leader()函数分析 // 获取当前 queue,并且重新开启一个 queue // 本 queue 中的 binlog,都由当前事务 group commit // 新 queue 中的 binlog,由新 queue 中的第一个事务等待 LOCK_log mutex mysql_mutex_lock(&LOCK_log); mysql_mutex_lock(&LOCK_group_commit_queue); current = group_commit_queue; group_commit_queue = NULL; mysql_mutex_unlock(&LOCK_group_commit_queue); // 写 binlog,最后执行一次 fsync 操作 for ( … ) write_transaction(); flush_and_sync(); // 获取 commit_ordered mutex,然后才能释放 LOCK_log mutex // 保证下一个 queue,不会在当前 queue 之前调用 commit_ordered mysql_mutex_lock(&LOCK_commit_ordered); mysql_mutex_unlock(&LOCK_log); // 按照 prepare 的顺序,调用引擎提供的 commit_ordered 方法 // 由于是按序逐个执行 commit_ordered 方法,因此能够保证事务 // commit 的顺序与 binlog commit 的顺序是完全一致的 // 在 commit_ordered 方法调用完成之后,才能唤醒对应的线程 // 事务线程被唤醒之后,才能够进入 2PC 的第二阶段 // 返回 handler.cc::ha_commit_trans()函数,执行 commit_one_phase_low current = queue; while (current != NULL) run_commit_ordered(current->thd, current->all); if ( ht->commit_ordered) ht->commit_ordered(ht, thd, all); current->thd->signal_wakeup_ready(); current = next; // 最后释放 commit ordered mutex mysql_mutex_unlock(&LOCK_commit_ordered); ha_innodb.cc::innobase_commit_ordered -> innobase_commit_ordered_low 函数分析 // 获取事务对应的 binlog 日志的位置 mysql_bin_log_commit_pos(); // 设置当前事务标识为 flush log later,写 commit 日志,但是不 flush trx->flush_log_later = TRUE; innobase_commit_low(trx); trx->flush_log_later = FALSE; 11 测试十:Innodb crash recovery 主流程: mysqld.cc::main -> mysql_service -> win_main -> init_server_components -> log.cc::TC_LOG_BINLOG::open -> recover -> handler.cc::ha_recover -> sql_plugin.cc::plugin_foreach_with_mask -> handler.cc::xarecover_handlerton -> ha_innodb.cc::innobase_xa_recover 流程说明: 一、 mysql recover 有三种模式,参考三种模式。这里只考虑第一种模式,crash recovery。crash recovery 需要 mysql 上层与 innodb 存储引擎协作进行恢复。必须保证 innodb 的恢复,与上层 mysql binlog 一致,如此,主库崩溃恢复之后,其数据能够保证与备库是一致的。 二、 binlog 中 commit 事务的 trx_id,在函数 log.cc::TC_LOG_BINLOG::recover 中读取并构造出来。然 后通过调用流程,一直传递给 handler.cc::xarecover_handlerton 函数,info->commit_list。 三、 innodb 层面的 innobase_xa_recover 函数,找出存储层面已经 prepare,但是没有 commit 的事务 id 数组,通过调用返回给 handler.cc::xarecover_handlerton 函数,info->list。 四、 handler.cc::xarecover_handlerton 函数,比较两个 trx_id 数组(info->commit_list 组织为 hash 表,在 hash 表中定位 info->list 中的事务)。 五、 如果 info->list 中的事务 id 在 commit_list 中存在,则调用 innobase_commit_by_xid 函数重新提 交事务; 六、 如果 info->list 中的事务 id 在 commit_list 中不存在,则调用 innobase_rollback_by_xid 函数回 滚事务。 七、 完成以上操作,crash recovery 成功,数据库恢复到崩溃前的状态。 Innodb 恢复流程: innobase_xa_recover -> trx_recover_for_mysql -> mutex_enter(&kernel_mutex) -> xid_list[count] = trx->xid(如果 trx_sys->trx_list 列表中存在 prepared 状态的事务,则设置 xid_list) 流程说明: 在进行 mysql 层与 innodb 层连动恢复之前,必须保证 innodb 已经完成恢复。innodb 自身恢复流程: mysqld.cc::main -> mysql_service -> win_main -> init_server_components -> sql_plugin.cc::plugin_init -> plugin_initialize -> handler.cc::ha_initialize_handlerton -> ha_innodb.cc::innobase_init -> innobase_start_or_create_for_mysql -> log0recv.c::recv_recovery_from_checkpoint_start(尝试恢复 innodb,哪怕是正常关闭) 在我测试的版本中,init_server_components 函数的 4009 行,会调用 innodb 进行恢复;4145 行,会 调用 innodb 进行二阶段恢复;完成二阶段恢复之后,整个 crash recovery 动作完成,数据库可以提供正 常服务。 11.1 recovery 的三种模式 recover的三种模式: 正常关闭数据库:模式3,no recovery 异常关闭数据库:模式1,automatic recovery 所有commit_list的事务都需要提交,不在commit_list中的事务回滚 commit_list事务数组作为参数传入Innodb的innobase_xa_recover, innobase_xa_recover根据传入的事务id数组,确定哪些事务commit,哪些事务rollback /** recover() step of xa. @note there are three modes of operation: - automatic recover after a crash in this case commit_list != 0, tc_heuristic_recover==0 all xids from commit_list are committed, others are rolled back - manual (heuristic) recover in this case commit_list==0, tc_heuristic_recover != 0 DBA has explicitly specified that all prepared transactions should be committed (or rolled back). - no recovery (MySQL did not detect a crash) in this case commit_list==0, tc_heuristic_recover == 0 there should be no prepared transactions in this case. */ struct xarecover_st { int len, found_foreign_xids, found_my_xids; XID *list; HASH *commit_list; bool dry_run; }; handler.h::class ha_trx_info /** Either statement transaction or normal transaction - related thread-specific storage engine data. If a storage engine participates in a statement/transaction, an instance of this class is present in thd->transaction.{stmt|all}.ha_list. The addition to {stmt|all}.ha_list is made by trans_register_ha(). When it's time to commit or rollback, each element of ha_list is used to access storage engine's prepare()/commit()/rollback() methods, and also to evaluate if a full two phase commit is necessary. @sa General description of transaction handling in handler.cc. */ handler.h::struct handlerton /* handlerton is a singleton structure - one instance per storage engine - to provide access to storage engine functionality that works on the "global" level (unlike handler class that works on a per-table basis) usually handlerton instance is defined statically in ha_xxx.cc as static handlerton { ... } xxx_hton; savepoint_*, prepare, recover, and *_by_xid pointers can be 0. */ /** to force correct commit order in binlog */ static pthread_mutex_t prepare_commit_mutex; /* This needs to exist until the query cache callback is removed or learns to pass hton. */ static handlerton *innodb_hton_ptr; log group flush /* Depending on the my.cnf options, we may now write the log buffer to the log files, making the prepared state of the transaction durable if the OS does not crash. We may also flush the log files to disk, making the prepared state of the transaction durable also at an OS crash or a power outage. The idea in InnoDB's group prepare is that a group of transactions gather behind a trx doing a physical disk write to log files, and when that physical write has been completed, one of those transactions does a write which prepares the whole group. Note that this group prepare will only bring benefit if there are > 2 users in the database. Then at least 2 users can gather behind one doing the physical log write to disk. TODO: find out if MySQL holds some mutex when calling this. That would spoil our group prepare algorithm. */ 12 测试十一:index coverage scan? 目的: 测试 Innodb 引擎,哪些情况下,会使用上 index coverage scan?从而提高整个系统的性能。 13 测试十二:mini transaction 在我的测试中,mini transaction 在 innobase_xa_prepare,innobase_xa_commit 函数中都 会调用,其功能是修改当前 undo log segment states。 在 innobase_xa_prepare 函数中,将其由 TRX_UNDO_ACTIVE 改为 TRX_UNDO_PREPARED。 trx_prepare_off_kernel(); mtr_start(); mutex_enter(&(rseg->mutex)); trx_undo_set_state_at_prepare(); mutex_exit(&(rseg->mutex)); mtr_commit(&mtr); 写 undo page 需要记录 redo 日志,日志记录在 mtr->log 中,在 mtr_commit 时,调用日 志模块的写日志函数,将 mtr->log 中的日志写出到文件,mtr->log 写完之后,根据系统是否 需要回刷日志,来判断最终是否将用户事务日志 && mtr 日志同时回刷到外存磁盘持久化。 在 innobase_xa_commit 函数中,也起到类似的功能。 14 测试十三:事务开始 测试 innodb 事务开始时,与上层 mysql 之间的交互。 14.1 autocommit=ON select * from tlock; //快照读,不加表级意向锁/行锁,并发通过上层 mysql 表锁保证 主流程: ha_innodb.cc::external_lock -> innobase_register_trx_and_stmt -> innobase_register_stmt -> handler.cc::trans_register_ha -> trans= &thd->transaction.stmt -> register_ha autocommit = ON 时,事务仅仅在 transaction.stmt 链表中进行注册。 事务何时从链表中摘除,这个是由 mysql 上层控制的,具体函数是 ha_autocommit_or_rollback。 14.2 autocommit=OFF set autocommit = ‘off’; select * from tlock; 主流程: 除了 autocommit = on 时的流程外,还多了以下流程 innobase_register_trx_and_stmt -> trans_register_ha(thd, TRUE, hton) -> trans=&thd->transaction.all -> register_ha autocommit = OFF 时,事务除了需要在 transaction.stmt 链表中进行注册,还需要在 transaction.all 链表中注册。 事务何时从链表中摘除,transaction.stmt 链表,仍旧是 statement 执行结束时,调用 ha_autocommit_or_rollback()函数摘除;transaction.all 链表,则是 mysql 在处理 commit 命令时, 调用 end_trans 函数摘除。 14.3 Innodb 内部事务 创建事务,调用流程(创建一个 trx_t 结构,但是并不开始事务): ha_innobase::open -> info -> update_thd -> check_trx_exists -> thd_to_trx -> sql_class.cc::thd_get_ha_data -> trx_allocate_for_mysql -> trx_create -> os_thread_get_curr_id -> os_proc_get_number -> sql_class.cc::thd_set_ha_data(可以将存储引擎特有的数据结构存储在 ha_data 中,后续通过 thd 就可以得到此结构,方便结构在各函数间传递,通过函数 sql_class.cc::thd_get_ha_data 可以取出此数据结构) -> 开始事务,调用流程(开始事务,设置事务 id,所属回滚段 id,事务状态,活跃事务链表; 这些操作的逆操作,在 trx_commit_off_kernel 中完成,但是事务结构不删除,也就是创建事 务的逆操作不做;创建的事务,将一直伴随中 session, 直 到 session 断开, 调 用 innobase_close_connection -> trx_free_for_mysql,此时释放事务内存): (For Scan) join_read_first -> ha_innobase::index_first -> index_read -> row_search_for_mysql -> trx_start_if_not_started -> trx_start -> trx_start_low 事务数据结构主要属性: 15 测试十四:insert ignore 测试 insert ignore into tlock select * from ctlock; write_row 函数与 trx_rollback_step 函数交替调用,对于每一个 unique 冲突的记录,都 做记录级别的 rollback; 每条记录被成功 insert 之后,trx_savept_struct 的取值会++,下一次的 insert 如果失败, 那么失败记录的操作会被 undo 到 savepoint,仅仅 undo 失败记录,已经成功 insert 的记录, 不会被 undo。 整个语句,将在语句结束之后 commit。有唯一性冲突的记录没有插入,而所有没有冲 突的记录,被成功插入到 tlock 表中。 TNT 由于是先判断 unique 冲突,然后 insert,因此可以很容易避免这个问题,甚至都不 需要 undo。 调用流程: sql_parse.cc:: mysql_execute_command -> sql_select.cc::handle_select -> mysql_select -> JOIN::exec -> do_select -> sub_select -> evaluate_join_record -> end_send -> select_insert::send_data -> write_record -> ha_write_row -> write_row -> row_insert_for_mysql -> row_mysql_handle_errors -> trx_general_rollback_for_mysql -> que_run_threads -> … -> trx_rollback_step 16 测试十五:auto_increment http://dev.mysql.com/doc/refman/5.6/en/innodb-auto-increment-handling.html -- auto increment handling in innodb 测试 innodb auto_increment 的实现方式。 -- 查询当前表的 auto_increment 值 show table status like ‘%tlock%’; -- 查询当前系统参数,auto inc 参数 show variables like ‘%innodb_autoinc%’; | innodb_autoinc_lock_mode | 1 | auto increment 并发控制,一共有三种模式: AUTOINC_OLD_STYLE_LOCKING 0 只需要 autoinc lock,不需要 mutex AUTOINC_NEW_STYLE_LOCKING 1 同时混用 mutex 与 autoinc lock 加上 mutex,需要检测当前是否有其他线程持有 autoinc lock,如果有,则降级为模式 2 AUTOINC_NO_LOCKING 2 只需要 mutex,不需要 autoinc lock 每种模式的意义,可参考上面的文档 A. 简单 insert insert into tauto (comment) values ('bbb'); 调用流程: mysql_insert -> handler::ha_write_row -> ha_innobase::write_row -> handler::update_auto_increment -> ha_innobase::get_auto_increment -> innobase_get_autoinc -> innobase_lock_autoinc(AUTOINC_NEW_STYLE_LOCKING 1) -> dict_table_autoinc_read(return table->autoinc) -> ha_innobase::get_auto_increment(非 old 模式下,设置当前 statement 需要消耗的 autoinc 大小,并更新 table->autoinc) -> handler::set_next_insert_id B. 批量 insert insert into tauto(id, comment) values (100,'ggg'), (null, 'hhh'), (200, 'iii'), (null,'jjj'),(null,’kkk’); 第一次: innobase::write_row -> update_auto_increment -> adjust_next_insert_id_after_explicit_value -> insert_id_for_cur_row = 0 -> innobase_set_max_autoinc 第二次: 正常流程,如 A 第三次: 同第一次流程 第四次: 同第二次流程 第五次: innobase::write_row -> update_auto_increment -> set_next_insert_id (由于第四次已经做 了缓存,因此此时直接使用缓存即可) C. insert into … select insert into tauto (comment) select comment from tlock; 第一次: innobase::write_row -> update_auto_increment -> get_auto_increment -> innobase_get_autoinc -> innobase_lock_autoinc -> row_lock_table_autoinc_for_mysql (需 要首先获得 autoinc table lock) -> dict_table_autoinc_lock (然后才是 autoinc mutex) -> nb_reserved_values = 1 (由于不知道 tlock 表中会有多少数据,因此只分配一个 autoinc 值) -> dict_table_autoinc_update_if_greater -> dict_table_autoinc_unlock (autoinc mutex 在此处直接释放,但是持有着 autoinc table lock) 第二次: 调用流程与第一次一致,唯一的不同之处在于,此时 nb_reserved_values = 2 (应该是一 种优化措施)。 第三次: update_auto_increment 函数中使用第二次的预取值,直接返回。 第四次: 预取已经消耗,此时预取 4 个。通过参数 auto_inc_intervals_count 控制。此参数每次左 移一位。因此预取值为 1,2,4,8,16… 最大左移 16 位。 … 最后: 提交事务,遍历 trx 的 lock 链表,释放对应的 autoinc lock。除此之外,InnoDB 的 trx 中, 还包含了一个栈:,ib_vector_t* autoinc_locks; 未知其作用。 判断当前表是否存在 auto_increment: (一) 通过 table->found_next_number_filed 字段判断 赋值:if (reg_field->unireg_check == Field::NEXT_NUMBER) share->found_next_number_field= field_ptr; (二) 通过 table->next_number_field 字段判断 赋值: 通过 table->found_next_number_filed 字段赋值 if(table->next_number_field && record == table->record[0]) 函数 get_auto_increment 分析: 1. 加 autoinc mutex,取得当前 autoinc 值(由于 innodb_autoinc_lock_mode = 1,加 mutex 即可) 2. 如果是第一次调用此函数,设置 trx->n_autoinc_rows 取值 3. 根据当前值以及当前 statement 需要 insert多少记录,更新 table->autoinc 取值[next_value = current + need ] 4. 释放 autoinc mutex[table->autoinc_mutex] 5. get_auto_increment 函数参数的意义: a) offset 多主库环境下,当前主库的编号;单主库下为 1 b) increment 多主库环境下,一共有多少节点;单主库下为 1 c) nb_desired_values 一次性需要申请多少个 autoinc 值,只有在 get_auto_increment 函数第一次被调用 时才使用此值,后续调用均不使用此值。 d) first_value 申请的 autoinc 值中的第一个取值,输入输出;若输入的 first_value 小于当前系统 autoinc 取值,则重设 first_value 取值;否则直接使用,当然,输入的 first_value 也 不能无限大,甚至是大于 autoinc 的最大取值。 e) nb_reserved_values 当前真正申请了多少个 autoinc 值,输出。等于 get_auto_increment 函数第一次调 用时的 nb_desired_values 取值。为了实现简单,也可设置为 1,那么相当于不 cache autoinc 取值,每次都重新加 mutex 获取,可能会对性能有所影响。 f) next_value(如何计算?) 若 increment = 1,单主环境,next_value = current + increment (当前值+递增步长 1) g) autoinc update? 更新 next_value 到表的 autoinc 中,然后可以释放 mutex,供其余并发线程申请。 同时记录 offset 与 increment 供 write_row 完成后使用(记录于 prebuilt 结构中) h) write_row? 在 insert 完 成 之 后 , 获取 insert 操 作 实 际 插 入 的 autoinc 取值 (table->next_number_field->val_int()),若此取值大于前面 f)步骤计算的 next_value, 则再次根据当前 autoinc 取值计算新的 next_value,并对 table 的 autoinc 进行更新。 此时需要用到 g)步骤保留下来的 offset 与 increment 值。 若在 insert 时指定 autoinc 值,则不会进入 get_auto_increment 函数,因此也不会 有 g)步骤的 offset 与 increment 的记录,会使用 row_create_prebuilt 函数中设置的 默认值:offset = 0;increment = 1。 i) 注意一类特殊的 insert:insert … on duplicate update,此时若给定的 new_value 对应 的是 autoinc 列,则需要在 update 时更新表 autoinc 的取值。计算的方式与前面的 h)步骤一致。直接的 update 操作并不会修改 table 的 autoinc 取值。 j) 函数 update_auto_increment 分析: a) 如果是第一次,或者是预存的 autoinc 值耗尽,则调用 get_auto_increment 函数获取新 预存值 (if (next_insert_id) >= auto_inc_interval_for_cur_row.maximum()) b) 计算 next_insert_id 。 autoinc 有两个变量: auto_increment_increment , auto_increment_offset。下一个取值,公式如下:next_autoinc = auto_increment_offset + N * auto_increment_increment c) 将 计 算 出 来 的 nr 值 存 入 autoinc 字段 field 。 通 过 调 用 函 数 table->next_number_field->store((longlong) nr, TRUE)实现。 d) 如果有必要,将 autoinc 的最小值,取值写入 binlog e) 设置 current_insert_id [insert_id_for_cur_row = nr]与 next_insert_id [set_nex_insert_id] f) auto_increment_offset,auto_increment_increment 含义分析 a) 首先,这两个值,在多主库环境下才有意义。目标是为了保证 auto increment 值的 产生,不同的主库,划分 auto increment 取值的不同区间,区间不冲突。 b) auto_increment_increment,表示多主环境下,有多少个主节点。n 个主节点的 mysql 集群,auto_increment_increment = n c) auto_increment_offset,表示多主环境下,当前主节点的编号。n 个主节点的 mysql 集群,auto_increment_offset 取值为[1, n]之间的任意一个数。 d) auto_increment_increment 是每次递增的步长;auto_increment_offset 是递增步长 中的第几位;n = 2,两节点集群时,节点 1 的 autoinc 取值为奇数(1,3,5,7,…); 节点 2 的 autoinc 取值为偶数(2,4,6,8,…) TNT 应该实现的功能: 一、write_row 函 数 中 , 判 断 当 前 表 是 否 有 autoinc 字 段 , 并 且 调 用 handle::update_auto_increment 函数 二、实现类似于 get_auto_increment 函数的功能,包括预取 三、实现 mutex,autoinc table lock 等并发功能 四、实现可以设置 autoinc 大小的功能,在 get_auto_increment 函数与 write_row 函数中都需 要类似的功能 五、需要实现将 autoinc 的值写入 binlog 的功能,只有这样才能保证复制到备库之后,事务 直接不会冲突 六、innodb 用表锁实现 autoinc 锁功能,一种特殊的表锁模式 七、TNT 准备用行锁实现 autoinc 锁功能,给定一个虚拟表,管理所有的含有 autoinc 列的表, 这些表在虚拟表中作为一个虚拟列存在,形如:(表,列) = (virtual_tab_id,tab_id),对 于表 t 的 autoinc 加锁,就是对(virtual_tab_id,tid)加行锁。同时虚拟表上不需要加意向 锁,因为虚拟表本身并不存在,并不会做 ddl。 八、AutoInc lock,在语句结束时就需要释放,而不能等待事务结束,否则并发性太差,因此 事务需要记录已加的 autoinc lock 信息。 九、针对于 auto_inc 的实现,TNT 可选两种两种方案: 方案一:TNT 不支持语句级 binlog,那么就不需要提供 autoinc lock,直接使用 mutex 即 可。Percona XtraDB Cluster (based on Galera replication)也是采用这种方案,只支持行级 binlog。 方案二:支持语句级 binlog,需要在事务中记录当前语句加了哪些 autoinc lock,语句结 束时释放。 十、 17 测试十六:数据格式转换 将 mysql 格式转换为 innodb 行格式 row_mysql_store_col_in_innobase_format 将 innodb 行格式转换为 mysql 格式 row_sel_field_store_in_mysql_format 18 测试十七:innodb 加载表数据字典 http://www.percona.com/docs/wiki/percona-server:features:innodb_dict_size_limit –Innodb Data Dictionary Size Limit 测试 innodb 加载表数据字典流程 测试方案:重启 mysqld,重启客户端,然后执行命令:select count(*) from tauto; 函数调用流程: handler::ha_open -> ha_innobase::open -> get_share(分配所有表可以共享的 innobase_share 结构,初始化 thr_lock) -> dict_table_get(在表数据字典 HASH 中查找当前表,如果未找到, 则加载创建,同时收集统计信息) -> dict_load_table(加载表定义/索引定义/cluster定义/foreing key 定义) -> dict_mem_table_create(创建表数据字典,初始化行数据字典,统计信息,autoinc 等 ) -> dict_load_columns( 用户列 ) -> dict_table_add_to_cache( 系 统 列 此 时 添 加 ) -> HASH_INSERT(name hash) -> HASH_INSERT(table id hash) -> UT_LIST_ADD_FIRST(table lru list) -> dict_load_indexes(加载索引字典) -> dict_mem_index_create() -> dict_index_add_to_cache -> dict_load_foreigns() -> END 同一个 session 第二次调用此函数: 第一次打开的表,在 mysql 上层做了缓存,进入 hash 表。因此第二次直接查找定位完成, 不需要再次打开。 mysql 上层可以调用 innobase::close()函数关闭已经打开的表。一般情况下,此函数不会被调 用,因此表打开之后,不会关闭。测试到有以下两种情况,mysql 会 close 表: 情况一:mysqld 进程正常退出,此时 mysql 会调用 close 函数关闭已经打开的表 情况二:已经打开的表,长时间没有被再次使用,此时 mysql 会主动调用 close 函数关闭此 表。timeout 通过参数控制 19 测试十八:scan 测试 测试 innodb 在各种查询条件情况下,如何完成扫描操作。 http://olavsandstaa.blogspot.com/2011/04/mysql-56-index-condition-pushdown.html --mysql 5.6: index condition pushdown create table t18 (a int primary key, b int, c int, d int, e int) engine=innodb; create index t18_idx1 on t18 (b, c, d); delimiter // CREATE PROCEDURE T18Insert() BEGIN DECLARE i INT; SET i=101; WHILE i<1000 DO INSERT INTO t18 (a,b,c,d,e) VALUES (i, i+1, i+2, i+3,i+4); SET i = i + 1; END WHILE; END;// delimiter ; call T18Insert(); --用例一 select * from t18 where b < 100 and d = 50; 调用流程: JOIN::exec -> do_select -> sub_select -> read_record (records.cc::rr_quick -> opt_range.cc::get_next -> handler.cc::read_multi_range_next -> read_range_next -> ha_innodb.cc:: index_next -> general_fetch -> row0sel.c::row_search_for_mysql -> ) -> evaluate_join_record (判断记录是否满足 d = 50 的条件,在我测试的版本中,d = 50 条件无 法下降到二级索引 t18_idx1 上)-> 循环(read_record -> evaluate_join_record) 退出判断: handler.cc::read_range_first -> ha_innodb.cc:index_next -> handler.cc::compare_key(比较当前返 回记录,是否已经超过范围扫描的最大值?,针对上例来说,判断 b 是否已经 >= 100) -> key.cc::key_cmp -> field.h::key_cmp -> field.cc::cmp create index idx_c1c2c3 on ncopy2 (c1,c2,c3); --用例二 select * from ncopy2 where c1 > 504 and c1 < 510; 1. 开始,调用 index_init 函数,指定查询需要使用的索引编号 ha_innobase::index_init(uint keynr); 2. 第一次,调用 index_read 函数,读取第一条满足条件的记录 ha_innobase::index_read // 根据 find_flag,确定当前为正向扫描,不包括指定的 key find_flag = HA_READ_AFTER_KEY; 3. 第二次,调用 index_next 函数,读取满足条件的下一条记录 ha_innobase::index_next 4. 最后,调用 index_end 函数,标识索引查找结束 --用例三 select * from ncopy2 where c1 >= 504 and c1 <= 510; 基本流程与前面的一致,除了步骤 2,调用 index_read 时,指定的 find_flag 参数不同。 // 当前为正向扫描,同时包括指定的查询键值项 find_flag = HA_READ_KEY_OR_NEXT --用例四 select min(c1), max(c1) from ncopy2; 1. 调用 index_first 函数,获取 c1 最小值 ha_innobase::index_first index_read(buf, NULL, 0, HA_READ_AFTER_KEY); 不指定查询条件,同时指定正向扫描,读取索引的第一条记录 2. 调用 index_last 函数,获取 c1 最大值 ha_innobase::index_last index_read(buf, NULL, 0, HA_READ_BEFORE_KEY); 不指定查询条件,同时指定反向扫描,读取索引的最后一条记录 --用例五 select ncopy2.c1 from nkeys, ncopy2 where nkeys.c1 = 504 and nkeys.c1 = ncopy2.c1; 对于 ncopy2 表的查询,流程如下: 1. 首先,调用 index_read 函数,读取 ncopy2 中的第一条 join 记录 ha_innobase::index_read find_flag = HA_READ_KEY_EXACT; 第一条记录的索引键值必须与指定的查询条件完全一 致(join) 2. 其次,调用 index_next_same 函数,读取 ncopy2 中下一条相同键值记录 ha_innobase::index_next_same general_fetch(buf, ROW_SEL_NEXT, last_match_mode); 读取下一条记录,last_match_mode = ROW_SEL_EXACT,下一条记录的键值必须相同。 3. 总结: 1. index condition pushdown 测试的 mysql 版本,5.1.49. 一个基于索引的查询,其 where 条件可以分为三类: first key(定位索引起始位置),last key(结束扫描位置); index filter(没构成索引范围,但是可以索引过滤); table filter(索引无法过滤)。 mysql 仅仅将 first key pushdown 了。其余都是上层判断。 mysql 5.6 版本,增强了 pushdown 功能,主要的增强在于,将 index filter pushdown 优势在于: (一) 减少了回表开销(二级索引访问聚簇索引),由于记录回表一般都是随机 IO,因此降 低了 IO 次数,提高了查询效率。 (二) 减少了记录 copy,传输开销,index filter pushdown 到索引,不满足 filter 条件的 记录不需要返回 mysql 上层,因此减少了记录的 copy 与传输。 2. 二级索引扫描,什么时候需要回表(回主键索引) a) need_to_access_clustered 参数赋值(Innodb) 在函数 build_template 中,判断是否需要设置此参数,逻辑如下: i. 传入参数 ROW_MYSQL_REC_FIELDS 时,遍历表中的每一个 filed,确定其是否包含 在当前索引中。 ii. 如果不包含,并且当前 scan 是 read_just_key (read_just_key 参数在 extra 函数 中设置),则跳过该 field。 iii. 判断当前查询是否需要该 field,通过 table->read_set,table->write_set 判断 iv. 对于需要的 filed,判断其在二级索引中的位置,如果不存在,则设置 need_to_access_clustered 参数。 v. 当然,如果传入的参数为 ROW_MYSQL_WHOLE_ROW,那么每一个 filed 都是必须的, 直接判断所有 field 在二级索引是否存在即可。 b) 二级索引无法判断可见性,需要通过主键索引判断 二级索引,除了页面级别有一个 MAX_TRX_ID,用于快速判断可见性之外,没有行 级的可见性判断,因此如果通过 MAX_TRX_ID 无法确定记录可见性,就需要访问主 键索引进行可见性判断 20 测试十九:加锁等待 session 1: session 2: set autocommit = ‘off’; select * from tlock for update; select * from tlock for update; session 2 将会等待 session 1 放锁。 session 2 锁等待流程如下: ha_innobase::index_read -> row_search_for_mysql -> row_mysql_handle_errors -> srv_suspend_mysql_thread -> os_event_set(srv_lock_timeout_thread_event) -> pthread_cond_broadcast(win: SetEvent) -> os_event_wait(event,此处时无限忙等,返回的条 件有两个:1) 其他事务放锁,唤醒此等待者;2) 由超时等待线程唤醒,等待超时,报错返 回) 锁等待超时唤醒 两个 event: srv_lock_timeout_thread_event: timeout monitor thread,控制等待超时 处理 timeout,有专门的一个线程,在 innodb 引擎启动的时候创建 innobase_start_or_create_for_mysql -> os_thread_create(&srv_lock_timeout_thread) -> srv_lock_timeout_thread(该函数是一个无限循环,每个循环休眠 1S,然后起来查询当前 系统中是否有超时等待的 event , 如 果 有 , 唤 醒 这 些 event 的 等 待 者 ) -> lock_cancel_waiting_and_release -> trx_end_lock_wait -> que_thr_end_wait_no_next_thr -> src_release_mysql_thread_if_suspended -> os_event_set(slot->event) (所有这些过程中,必 须持有 kernel_mutex) slot->event: 主 event,等待 session 1 释放锁/或者是被超时线程唤醒 event 分配: 每个需要等待的线程,都会选择一个没有被使用的 slot,并等待在其 event 之上。两个线程, 就算等待同一把锁,但是等待的 event 还是不同的。此时,如何做到同时放锁唤醒? 遍历 slot 数组,将其中等待超时的 slot->event,一次性唤醒,更为简洁。 如果没有统一分配的 slot 数组,那么可以遍历 TransactionSys 对象中的活跃事务数组,释放 其中超时等待的 event。 srv_suspend_mysql_thread -> srv_table_reserve_slot_for_mysql 放锁唤醒: trx_commit_off_kernel -> lock_release_off_kernel -> lock_rec_dequeue_from_page (lock_get_wait(lock) && !lock_rec_has_to_wait_in_queue(lock), 有等待的 lock,同时该 lock 不与 queue 前面的 lock 冲突) -> lock_grant -> trx_end_lock_wait -> que_thr_end_wait_no_next_thr -> srv_release_mysql_thread_if_suspended -> os_event_set (唤醒等待中的 event,同时将 trx->que_state 设置为 TRX_QUE_RUNNING 状态) /************************************************************** Waits for an event object until it is in the signaled state. If srv_shutdown_state == SRV_SHUTDOWN_EXIT_THREADS this also exits the waiting thread when the event becomes signaled (or immediately if the event is already in the signaled state). Typically, if the event has been signalled after the os_event_reset() we'll return immediately because event->is_set == TRUE. There are, however, situations (e.g.: sync_array code) where we may lose this information. For example: thread A calls os_event_reset() thread B calls os_event_set() [event->is_set == TRUE] thread C calls os_event_reset() [event->is_set == FALSE] thread A calls os_event_wait() [infinite wait!] thread C calls os_event_wait() [infinite wait!] Where such a scenario is possible, to avoid infinite wait, the value returned by os_event_reset() should be passed in as reset_sig_count. */ 21 测试二十:mysql 定位 table session 1: select * from aaa;(aaa table 不存在) 调用流程: execute_sqlcom_select -> open_and_lock_tables -> open_and_lock_tables_derived -> open_tables -> open_table -> open_unireg_entry -> get_table_share_with_create -> get_table_share -> open_table_def (path = “.\test\aaa.frm”)-> my_open (failed) -> strxnmov (path = “./test/aaa.frm”) -> my_open (failed again) -> select * from tlock2;(tlock2 table 存在) 调用流程: 接上,当 open_table_def 成功之后,open_unireg_entry 函数继续以下调用: open_unireg_entry -> handler::ha_open -> ha_innobase::open -> normalize_table_name (name = “.\test\tauto”, norm_name = “test/tauto”) -> get_share (初始化 innobase_share 结构) -> strstr(norm_name, “#P#”,判断是否为分区表) -> dict_table_get -> dict_table_get_low -> dict_load_table -> dict_table_get_low(“SYS_TABLES”) (innodb 通过系统表读取用户表定义) -> 22 测试二十一:如何做 join 目的: 测试 innodb 如何在两张表的 handler 之间做转换。 session 1: select t1.*, t2.* from tlock t1, tauto t2 where t1.id = t2.id; 第一阶段(create innobase table handler): open_tables -> open_table -> open_unireg_entry -> get_table_share_with_create -> get_table_share -> open_table_def -> open_binary_frm -> get_new_handler -> innobase_create_handler -> ha_innobase::ha_innobase(此处创建的 innobase 对象会销毁) -> handler::init -> ha_innobase::table_flags (设置表 flag,参考 handler.h 定义,Line 50) -> open_table_from_share -> ha_innobase::ha_innobase(此处创建的 innobase 对象会保存) -> 第二阶段(open innobase tables) open_unireg_entry -> open_table_from_share -> ha_open -> ha_innobase::open -> 第三阶段(index read & join: first) mysql_select -> JOIN::exec -> do_select -> sub_select -> join_read_first(此时,已经会调用第一阶 段 生 成 的 handler , innobase handler ,指向 tlock table) -> [handler::ha_index_init -> ha_innobase::index_init -> change_active_index] -> ha_innobase::index_first -> ha_innobase::index_read -> 第三阶段(index next & join: second) JOIN::exec -> do_select -> sub_select -> join_read_next -> ha_innobase::index_next 总结: 一、第一阶段创建 innobase 的 table handler 实例,tlock 与 tauto 实例,并且将实例按照 join 顺序创建,链接入链表。 二、第二阶段,为每个 table handler 实例,打开实例对应的表,并作相应的初始化。 三、第三阶段,根据第一阶段的链表,按照从链表头到链表尾的方式遍历链表,对于取出的 每一个 handler,调用 handler 提供的函数(index_first,index_read,index_next),读取记 录,进行 join。 23 测试二十二:latch & lock holding latch 测试 innodb 如何实现 latch,以及如何实现 lock holding latch?加 lock 是否会释放 latch?是 否会产生 lock 与 latch 间的 deadlock? session 1: set autocommit = ‘off’; select * from tlock where id = 111 for update; 函数调用流程: ha_innobase::index_read -> row_search_for_mysql -> btr_pcur_open_with_no_init -> btr_cur_search_to_nth_level -> dict_index_get_lock(获取 index 上的 rw_lock,每个 index 均有此 rw_lock,用于管理 index 的 非页节点) -> mtr_s_lock_func -> rw_lock_s_lock_func (对前面获得的index rw_lock加s latch) -> rw_lock_get_mutex(rw_lock , 都 包 含 一 个 mutex , 用 于 控 制 rw_lock 的 并 发 ) -> btr_cur_latch_leaves( 已经定位到叶节点,对叶节点加 latch) -> btr_page_get -> buf_page_get_gen(buffer pool 的 mutex 控制 block 并发,block 的 mutex 控制 block 上的并发) -> rw_lock_s_lock_func(rw_lock 的 并 发 获 取 , 由 rw_lock 上的 mutex 控制) -> rw_lock_s_lock_low -> (优化:叶节点 latch 加上之后,判断如果是第一次读取,则尝试进行 预读,buf_read_ahead_linear) -> mtr_release_s_latch_at_savepoint(是否 index tree 上的 s latch) -> lock_table(对表加意向锁,LOCK_IX) -> sel_set_rec_lock () -> mtr_commit(取到记录,在返回 记录之前,释放叶节点上的 s latch) -> mtr_memo_pop_all -> buf_page_release -> rw_lock_s_unlock 总结: 一、索引所有非页节点共用一个 latch::rwlock,用于控制 search path。optimistic i/u,search path会加s latch,叶节点加x latch,叶节点加上x latch之后,释放索引树s latch;pessimistic i/u,索引树加 x latch,叶节点加 x latch,并且索引树 x latch 要在 i/u 完成之后释放。 二、索引叶节点有自己的 latch::rwlock,保证叶节点的并发度 三、在获取叶节点自己的 latch::rwlock 之后,才可以释放 tree 的 latch;叶节点的 latch,在 buf_page_get_gen 函数中获取 四、表锁,行锁在叶节点 latch 的保护下获得 五、如果加锁需要等待,那么则释放叶节点 latch,加锁进入等待 六、锁等待被唤醒之后,需要 restart,重新定位记录 七、innodb index,叶节点与非叶节点,分配自两个 page pool,保证叶节点的连续性 注: InnoDB 索引未使用经典的 ARIES/IM 协议,使用了 Index Lock 加上更新(I/U)操作分类策略。 Index Lock 用于整个索引结构的访问控制,通过 RW 锁实现。I/U 操作分为 optimistic 与 pessimistic 两类,optimistic 更新不会引起索引结构变化(Structure Modification Operation), pessimistic 更新会导致索引 SMO。 续一:select 操作,对 Index Lock 加 S 锁,进行 search path,获得叶页面的 S latch 之后释放; optimistic 更新,与 select 操作一致,Index Lock S,获取叶页面的 X latch 之后释放;pessimistic 更新,对 Index Lock 加 X 锁,并且此 X 锁一直到更新操作(SMO)完成之后释放。SMO 禁止 Index 的并发访问。 续二:ARIES/IM 协议,是由 IBM 的研究员 C.Mohan 提出的索引并发控制以及恢复协议,属 于 ARIES 协议族的一部分。感兴趣的朋友可以看看这篇论文: http://www.ics.uci.edu/~cs223/papers/p371-mohan.pdf。 参考文献: http://blog.csdn.net/spche/article/details/6202273 --mysql innodb b-tree http://www.mysqlperformanceblog.com/2010/02/25/index-lock-and-adaptive-search-next-two- biggest-innodb-problems/ Index lock and adaptive search – next two biggest innodb problems 24 测试二十三:Mysql 上层加锁逻辑 上层 mysql 对表加锁的函数流程: sql_base.cc:: open_and_lock_tables_derived -> sql_base.cc::lock_tables -> lock.cc::mysql_lock_tables -> lock.cc::mysql_lock_tables_check -> thr_lock.c::thr_lock -> thr_lock.c::wait_for_lock -> my_wincond.c::pthread_cond_timewait thr_lock.h::enum thr_lock_type 上层 mysql 对表解锁的函数流程: 流程一:alter table tlock drop column gmt_create; mysql_alter_table -> intern_close_table -> closefrm -> ha_innobase::close() -> free_share -> thr_lock_delete(释放 innodb 层面生成的 thr_lock 对象) mysql_alter_table -> close_data_files_and_morph_locks -> mysql_unlock_tables -> thr_multi_unlock -> 流程二:select * from tlock; do_select -> JOIN::join_free -> mysql_unlock_read_tables -> thr_multi_unlock (TL_READ) -> 注意: mysql 的上层加锁,在调用 external_lock 函数之后进行,因此如果存储引擎在其重载的 external_lock 函数中 加底层锁,那么就会导致 mysql 上层与底层加锁的死锁问题,而且此死锁无法检测,无法 kill。 25 测试二十四:get_share & free_share 测试 get_share 函数,free_share 函数实现的功能。 猜测: get_share 创建一个表上全局唯一的 thr_lock 对象,用于 mysql 上层控制对于表文件 的并发访问。 验证: session 1: select * from tlock; 调用流程: 第一阶段,初始化 THR_LOCK 与 THR_LOCK_DATA open_table -> open_unireg_entry -> open_table_from_share -> handler::ha_open -> ha_innobase::open -> get_share -> hash_search( 第 一 次 open table , search failed) -> my_hash_insert(&innobase_open_tables,search 失败,创建 share 结构,并且存入 hash 表) -> thr_lock_init(初始化 share 中的 THR_LOCK 结构,一张表,只有一个 share,一个 THR_LOCK) -> dict_table_get(在底层 innodb 的 dictionary cache 中查找表) -> thr_lock_data_init(初始化 THR_LOCK_DATA 结构,一个 handler,有一个 THR_LOCK_DATA 实例) -> 第二阶段,将 THR_LOCK_DATA 返回给上层,并且加锁 lock_tables -> mysql_lock_tables -> get_lock_data -> store_lock(由于是只读 scan,lock_type 为 TL_READ,不做调整) -> thr_multi_lock -> thr_lock(根据 store_lock 函数返回的 THR_LOCK_DATA 加锁,THR_LOCK_DATA 指向 get_share 中初始化的 THR_LOCK 对象,由于此时 mysql 并未对 tlock 表加过锁,因此 TL_READ 锁加锁成功) -> 第三阶段,语句执行结束,释放 THR_LOCK do_select -> JOIN::join_free -> mysql_unlock_read_tables -> thr_multi_unlock -> thr_unlock -> 上层在 statement 执行完之后,会释放 THR_LOCK,但是并不释放 tlock 上的 handler,而是缓 存起来,以待下一个 statement 可以重用。 为了模拟同一张表,打开多次时的调用流程,验证 THR_LOCK 是 table 层面,一份, THR_LOCK_DATA 是 handler 层面,open 几次有几份的猜测,可以使用以下的 sql: select t1.*, t2.* from tlock t1, tlock t2 where t1.id = t2.id; t1 使用上层 mysql 缓存的 handler,省却了 open 操作 t2 需要再次打开同一个表上的第二个 handler,流程如下: ha_innobase::open -> get_share(hash 表 innobase_open_tables 中存在,只需要设置 share 实例 的 use_count 即 可,= 2) -> thr_lock_data_init(创建 第 二个 THR_LOCK_DATA 结构) -> store_lock(连续调用两次) -> thr_lock(连续调用两次,同一 THR_LOCK,两个 THR_LOCK_DATA) -> thr_unlock(同样两次,释放两个锁) -> 同理,第二次执行 select t1.*, t2.* from tlock t1, tlock t2 where t1.id = t2.id;,由于 mysql 上层 缓存了 tlock 表上的两个 handler,因此并不需要调用底层的 open 函数,直接应用缓存中的 handler 即可。 一般情况下,生成的 handler,share 结构都会缓存起来,留待下次之用,但是如此一来,缓 存就会越来越大。mysql 采用了一种超时的机制,如果一个 handler 结构在一定时间之内没 有被再次使用,则直接释放,类似于 LRU 策略。同时调用 free_share 函数,判断 use_count 是否归零,如果归零,则同时释放 share 结构。 share 结构释放流程如下: sql_manager.cc::handle_manager -> sql_base.cc::flush_tables -> hash.c::my_hash_delete -> free_cache_entry -> intern_close_table -> table.cc::closefrm -> ha_innobase::close() -> free_share(判断 use_count 是否归零,归零则释放缓存的 share) -> 注意:这里,只是释放上层缓存的 handler 以及可选择的释放 innodb 层面缓存的 share 结构, 并不释放 innodb 层面的 dictionary cache,已经打开的 table,仍旧处于打开的状态。毕竟, 相对于创建handler,share结构,open一个table的开销还是非常巨大的(读取所有的系统表, sys_table,sys_index,sys_columns…构造一个完整的 table)。 26 测试二十五:Insert on duplicate update 目的: 1. 测试 innodb 处理 insert on duplicate update 命令时的流程。冲突发生时,是否在 insert 函数中完成 update 操作? 2. 测试多 Unique key 的表,如何处理 Duplicate key 冲突? 3. 测试 replace into 的流程 测试一: insert into tlock values (30,’aaa’), (123, ‘ccc’) on duplicate key update comment = ‘eee’; 函数调用流程: (30, ‘aaa’) insert 成功; (123, ‘ccc’) insert 产生 duplicate key error,流程如下: ha_innobase::write_row -> row_insert_for_mysql -> row_ins_step -> row_ins -> row_ins_index_entry_step -> row_ins_index_entry -> row_ins_index_entry_low -> row_ins_duplicate_error_in_clust(在查找 insert 位置的过程中,找到了一项与插入键值完全相 同的 key , 加 锁 判 断 ) -> row_ins_dupl_error_with_rec( 判断之后,key 仍旧相同) -> trx->error_info = cursor_index, err = DB_DUPLICATE_KEY(设置 key 冲突索引) -> 冲突之后,持有冲突键值的锁,返回上层,上层 sql_insert.cc 中的 write_record 函数判断出 错是否需要退出,此处不需要退出。 sql_insert.cc::mysql_insert -> write_record -> index_read_idx_map(HA_READ_KEY_EXACT) -> ha_innobase::index_init -> ha_innobase::index_read -> row_search_for_mysql(冲突之后,需要 读取冲突行的完整记录,由于行已经加锁,因此肯定找得到,指定的冲突的 key 进行查找) -> handler::ha_update_row -> ha_innobase::update_row -> 测试二: create table nKeys (c1 int primary key, c2 int unique, c3 int unique, c4 int unique, c5 int) engine = innodb; insert into nKeys values (1,1,1,1,1); insert into nKeys values (2,2,2,2,2); insert into nKeys values (3,3,3,3,3); insert into nKeys values (4,4,4,4,4); insert into nKeys values (5,5,5,5,5); insert into nkeys values (1,2,3,4,5) on duplicate key update c3 = 3; sql_insert.cc::write_record -> if (info->handle_duplicates == DUP_REPLACE || DUP_UPDATE)(此处,为 DUP_UPDATE) -> if (info->handle_duplicates == DUP_UPDATE)(判断是 dup_update or dup_replace,走此路径) -> table->file->ha_update_row(table->record[1], table->record[0])(将 insert 转为正常 update,此调 用仍旧可以报 update conflict 错误) 测试三: replace into nkeys values (1,2,3,4,5); 调用流程: sql_insert.cc::write_record -> if (info->handle_duplicates == DUP_REPLACE || DUP_UPDATE)(此处,为 DUP_REPLACE) -> while ((error = table->file->ha_write_row(table->record[0])))(while 循环,保证 insert 操作必须成 功) -> if (info->handle_duplicates == DUP_UPDATE)(判断 dup_update or dup_replace,走 else 路径) -> if (last_uniq_key(table, key_nr)(若为最后一个 Unique 索引,则在冲突键值行上做 update) -> else (table->file->ha_delete_row(); info->deleted++)(若不为最后一个 Unique 索引,则删除当前 冲突的记录) -> 总结:  replace into,保证命令一定成功。若有多个冲突项,则删除这些冲突项,然后插入记录 (mysql 内部会将插入操作优化为 update)  on duplicate key update,不能保证命令一定成功。若 update 中也存在 Unique key 冲突, 那么语句直接报 unique 冲突,返回。  27 测试二十六:purge 测试 测试 innodb 如何完成索引上的多版本记录的回收。 update tlock set comment = ‘923’ where id = 123; update tlock set comment = ‘923’ where id = 2; update tlock set comment = ‘923’ where id = 111; purge 调用流程: srv0srv.c::srv_master_thread( 主函数,10S 调 用 一 次 purge) -> trx0purge.c::trx_purge -> read_view_close(close老的read view) -> read_view_oldest_copy_or_open_new(创建新的purge read view) -> que_thr_step -> row_purge_step -> row_purge(按行进行 purge 操作) -> trx_purge_fetch_next_rec(从最老的位置顺序读取 undo 信息) -> trx_purge_choose_next_log -> row_purge_parse_undo_rec(读取出一条 undo 信息之后,解析此 undo record,解析 undo record 中记录的修改的属性信息) -> row_mysql_freeze_data_dictionary(purge record 时,必须禁止 drop table 操作) -> row_purge_upd_exist_or_extern(按照先二级索引,最后聚簇索引的顺序, purge 索引上的与 undo 对应的过期记录) -> 循环调用,每次 purge 处理最多 20 个 undo log pages(purge_sys->n_pages_handled +20) -> 总结: 1. 20s 调用一次 purge 操作,主线程调用 2. 每次 purge 扫描最多 20 个 undo pages 3. 从 undo record 构造查询条件,然后按照二级索引,聚簇索引的顺序,purge 满足查询 条件的过期记录 4. purge 记录的过程中,必须保证 drop table 不能够操作 5. 根据参考文档,innodb 引擎在后续版本中对 purge 进行了优化,将 purge 操作从主线程 中解放出来,同时可以开始多个 purge 线程,提高 purge 的效率 参考文档: http://blogs.innodb.com/wp/2011/04/mysql-5-6-multi-threaded-purge/ --Mysql 5.6: multi threaded purge 28 测试二十六(cont.): purge 测试续 目的: 在 purge 测试的基础上,做进一步的测试,问题是: 1. 如果 update 操作仅仅修改了一个二级索引的部分字段,那么 purge 的时候,如何通过 这部分字段,来定位需要 purge 的记录? 2. 对于 delete 操作,没有修改任何属性信息,那么此时如何 purge?如何获得需要 purge 记录的完整信息? 测试用例: create table tpurge (c1 int primary key, c2 int, c3 int, c4 int); create index idx1 on tpurge (c2, c3, c4); create index idx2 on tpurge (c3, c4); insert into tpurge values (1,2,3,4); insert into tpurge values (2,3,4,5); insert into tpurge values (4,5,6,7); 测试 update 回收 update tpurge set c4 = 20 where c4 = 7; row_purge_step -> row_purge -> row_purge_upd_exist_or_extern -> row_upd_changes_ord_field_binary( 判断 update 是否更新了当前索引中的排序列 ) -> row_build_index_entry(根据解析的 undo,构造一个完整的能够唯一定位一条记录的 record, 对于 idx1 , 是 一 个 包 括 (c2,c3,c4,c1) 的记录项,虽然我们只更新了 c4) -> row_purge_remove_sec_if_poss -> row_search_index_entry(根据给定的 entry 查询 index,确定 是否能够定位到完全一致的 record) -> row_purge_reposition_pcur(查询当前 purge 记录在 clust_index 中的位置) -> row_vers_old_has_index_entry(判断待 purge 的记录,是否在 clust_index 当前存在或者是能够 undo 出一个完全一致的记录,如此一来,则不能 purge 二 级索引,后续有用) -> trx_undo_prev_version_build(构建当前 record 的前一个版本) -> btr_cur_optimistic_delete -> 测试 delete 回收 delete from tpurge where t4 = 4; row_purge -> row_purge_parse_undo_rec -> row_purge_del_mark -> row_build_index_entry(对 于 idx1,构造出的 entry 是包含(c2,c3,c4,c1)的一个完整索引记录;对于 idx2,则是包含(c3, c4, c1)的完整索引记录) -> 测试 undo 解析: row_purge_parse_undo_rec -> trx_undo_update_rec_get_sys_cols(事务号,rollback_ptr) -> trx_undo_rec_get_row_ref( 解析 undo 中,用于唯一定位一条记录的字段,主键) -> trx_undo_update_rec_get_update(解析 undo 中的事务 id,roll_ptr,以及更新过的字段) -> trx_undo_rec_get_partial_row(对于辅助索引,undo 记录了辅助索引包含的所有字段,需要 解析出来,用于 purge 辅助索引) 总结: 1. undo 日志量大。为了保证辅助索引上的过期记录的 purge,innodb 会在 undo 中记录所 有的辅助索引涉及的字段,哪怕此字段并未被 update;而对于 delete 操作,undo 中同 样会记录所有辅助索引涉及的字段。 2. purge 操作性能较差。innodb 的 purge 操作,是一个复杂的过程,包括各索引记录的构 造,根据构造的索引记录做 unique scan(辅助索引上),定位到记录之后,还需要到 clust_index 中判断辅助索引项是否可以被 purge(可能会涉及到 clust_index 上的记录 undo) 建议: 1. 考虑到 innodb 的索引覆盖扫描实现的较差,为了减少 undo 量,无用的字段,尽量不放 在索引中 2. 29 测试二十七:blob & blob purge 测试 innodb 如何实现 blob,以及如何回收过期的多版本 blob? create table tlob (id int primary key, comment blob) engine = innodb; insert into tlob values (2, lpad(‘923’, 8000, ‘z’)); insert into tlob values (3, rpad(‘hdc’, 10000, ‘r’)); select id, length(comment) from tlob; update tlob set comment = lpad(‘hcy’,10000, ‘l’)); update blob 调用流程: ha_innobase::update_row -> row_update_for_mysql -> row_upd_step -> row_upd -> row_upd_clust_step -> row_upd_clust_rec -> btr_cur_optimistic_update -> row_upd_changes_field_size_or_external(如果属性长度发生变化,或者 update 属性有链接行, 返回 true) -> rec_offs_nth_extern -> btr_cur_pessimistic_update(由于 blob 行外存储,因此 optimistic update 报错,需要做 pessimistic update) -> btr_cur_optimistic_update(再次尝试 optimistic update,仍旧报错) -> btr_cur_upd_lock_and_undo(记录 undo,写入 undo_no,trx_id, roll_ptr,comment 字段行内的 788 bytes,设置 type_cmpl | TRX_UNDO_UPD_EXTERN,说明 purge 时需要回收行外存储空间) -> trx_undof_page_add_undo_rec_log(记录 undo log 的 redo) -> purge 回收大对象流程 trx_purge -> row_purge_step -> row_purge -> row_purge_upd_exist_or_extern -> btr_free_externally_stored_field -> 总结: 1. innodb 的行有多种存储方式,处理 blob 的存储也有多种方式,具体可以看下面的参考 文档。我测试的 5.1 中,采用 compact row format,blob 需要 788 字节存储在行内(768 prefix + 20(true length, pointer to the overflow list));5.5 之后的 dynamic row format,blob 要么全部存储在行内,要么全部在行外,行内只保存 20-byte info。 20-byte 的组织形式如下: #define BTR_EXTERN_SPACE_ID 0 /* space id where stored */ #define BTR_EXTERN_PAGE_NO 4 /* page no where stored */ #define BTR_EXTERN_OFFSET 8 /* offset of BLOB header on that page */ #define BTR_EXTERN_LEN 12 /* 8 bytes containing the length of the externally stored part of the BLOB. The 2 highest bits are reserved to the flags below. */ /*--------------------------------------*/ #define BTR_EXTERN_FIELD_REF_SIZE 20 /* The highest bit of BTR_EXTERN_LEN (i.e., the highest bit of the byte at lowest address) is set to 1 if this field does not 'own' the externally stored field; only the owner field is allowed to free the field in purge! If the 2nd highest bit is 1 then it means that the externally stored field was inherited from an earlier version of the row. In rollback we are not allowed to free an inherited external field. */ #define BTR_EXTERN_OWNER_FLAG 128 #define BTR_EXTERN_INHERITED_FLAG 64 2. 参考文档: http://www.mysqlperformanceblog.com/2010/02/09/blob-storage-in-innodb/ --Blob storage in innodb http://dev.mysql.com/doc/innodb-plugin/1.0/en/innodb-row-format.html --Storage of variable-length columns http://mysqlha.blogspot.com/2008/07/how-do-you-know-when-innodb-gets-behind.html --Measure purge lags http://dev.mysql.com/doc/refman/5.0/en/functions.html --Mysql functions and operations 30 测试二十八:HA_READ_KEY_EXACT 测试上层 mysql 何时设置 HA_READ_KEY_EXACT 参数?测试 innodb 如何实现 HA_READ_KEY_EXACT 调用? session 1: select * from tlock where id = 111; session 2: select * from tlock where comment = ‘aaa’; session 3: select * from tlock where id > 1000000000; session 1 测试,id 是 primary key,session 2 测试,comment 为非 unique 属性。 session 1 测试,调用流程(HA_READ_KEY_EXACT,只有一条满足条件的记录): mysql_select ->JOIN::optimize -> make_join_statistics(判断 scan 是否为 unique scan,flags & (HA_NOSAME | HA_END_SPACE_KEY) ,如果是,则调用下面的函数 流程) -> join_read_const_table -> join_read_const (Read a table when there is at most one matching row) -> handler::index_read_idx_map -> handler::index_read_map -> ha_innobase::index_read -> row_search_for_mysql -> JOIN::exec -> do_select(由于在 optimize 函数中已经返回唯一的一条 记录,因此这里直接返回即可) ntse 如何完成? ntse 重载了 handler::index_read_map 函数,在函数中直接调用 index_read session 2 测试,调用流程(HA_READ_KEY_EXACT,有多条满足条件的记录): mysql_select ->JOIN::optimize(查询优化,执行计划选择) -> make_join_statistics(判断是否为 unique scan,判断失败,不能做 unique scan 的优化,调用下面的正常函数流程) -> JOIN::exec -> do_select -> sub_select(函数中包含一个 loop,取出满足查询条件的所有记录) -> join_read_always_key -> handler::index_read_map -> ha_innobase::index_read(这是第一次调用 流 程 , 取 第 一 条 记 录 ) -> join_read_next_same -> ha_innobase::index_next_same -> general_fetch(第二次之后,是循环调用,返回后面的所有记录) -> ntse 如何完成? first time: 调用 ntse 重载的 index_read_map 函数,index_read_map 第一次执行时调用, 主要在前面初始化扫描相关的参数,包括扫描方向、扫描类型等等。 second time:ntse 没有重载 index_next_same 函数,直接调用 index_next 函数 对于 HA_READ_KEY_EXACT 类型的查询,在找到 next 记录之后,需要判断记录是否与传入的 search key 相同。如果不相同,则直接返回 DB_RECORD_NOT_FOUND。标识此次扫描结束。 同时 general_fetch 函数会处理 DB_RECORD_NOT_FOUND 错误,将错误代码转换为 mysql 错 误码:HA_ERR_END_OF_FILE。 session 3 测试,调用流程(HA_READ_AFTER_KEY): mysql_select -> JOIN::exec -> do_select -> sub_select -> join_init_read_record -> rr_quick -> QUICK_RANGE_SELECT::get_next -> handler::read_multi_range_first -> handler::read_range_first -> handler::index_read_map -> ha_innobase:: index_read( 读 取 第 一 条 记 录 ) -> rr_qick_QUICK_RANGE_SELECT::get_next -> handler::read_multi_range_next -> handler::reag_range_next -> ha_innobase::index_next -> ha_innobase::general_fetch(读取第二 条满足条件的记录以及所有以后的记录) ntse 如何完成? 测试结论: 1) 等值查询,传入的参数均为 HA_REA_KEY_EXACT。 2) 等值查询,需要存储引擎判断扫描是否结束;非等值查询,由 mysql 上层判断扫描是否 结束,可结合测试十八:scan 测试一起学习。 3) mysql 为 unique key 的等值查询做了优化,调用路径也与普通等值查询不同,同时也不 需要存储引擎层面判断扫描是否结束。 4) 索引扫描结束,mysql 层面将会调用存储引擎层提供的 index_end 函数 31 测试二十九:offline_ddl/fast_idx_create 测试 mysql+innodb 如何实现为表增加列?增加 autoinc 列?增加索引?删除索引?如果一个 scan 跨越了 ddl,那么能否实现一致读? session 1: alter table tlock add column gmt_modified timestamp; session 2: alter table tlock drop index idx; session 3: create index idx on tlock (gmt_modified); session 4: rename table tlock to tnew; alter table tnew rename to tlock; session 5 (ntse 的处理方式有何不同?): alter table tntse add column gmt_create int; session 6: create index idx4 on tntse (gmt_create); session 1 调用流程: 与 session 2,drop index 的调用流程完全一致。 session 2 调用流程: do_command -> sql_parse.cc::dispatch_command -> sql_parse.cc::mysql_execute_command -> sql_table.cc::mysql_alter_table -> sql_base.cc::open_n_lock_single_table(mysql 上层对 tlock 表 加锁,锁模式为 TL_WRITE_ALLOW_READ) -> open_and_lock_tables_derived -> open_tables -> lock_tables( 此 处 需 要 做 以 下 判 断 : !thd->locked_tables && !thd->prelocked_mode) -> mysql_prepare_alter_table(alter table via a temporary table,将 alter 语句转换为 create temporary table 语句) -> compare_tables(对比新旧两张表) handler::alter_table_flags(判断底层 存储引擎是否支持 fast index create/drop,innodb 不支持,因此没有重载 alter_table_flags 函 数) -> sql_table.cc::mysql_create_table_no_lock(表名:#sql-62c_2) -> open_temporary_table(打 开临时表#sql-62c_2,但是这个不真正的临时表,因为该表会被 rename 为 tlock) -> copy_data_between_tables() -> read_record -> write_row( 循 环 读 取 tlock, 插 入 记 录 到 #sql-62c_2) -> ha_autocommit_or_rollback -> end_active_trans(数据 copy 结束之后,提交事务, 持久化) -> intern_close_table(new_table ,关闭新表, 此 时 必 定 没 有 人 访 问 ) -> wait_while_table_is_used(等待老表上当前打开表的 thd 释放表锁,然后对表加锁,所有 thd 在此之后,都需要重新打开表) -> mysql_lock_abort -> remove_table_from_cache(等待所有的 thd 结束,并且将 table 从 hash 表中删除) -> mysql_rename_table(连续两次 rename_table 函 数调用) -> ha_innobase::rename_table -> row0mysql.c::row_rename_table_for_mysql -> row_mysql_lock_data_dictionary( 对表数据字典加排它锁,防止并发操作 ) -> log_buffer_flush_to_disk(rename 完成之后,需要 flush log,用于保持 innodb 与上层 mysql frm 文件一致) -> quick_rm_table(将 rename 之后的表#sql-62c-2 删除) -> session 3 调用流程: 与 drop index 的调用流程一致。 session 4 调用流程: rename table tlock to tnew; mysql_execute_command -> mysql_rename_tables -> rename_tables -> do_rename -> mysql_rename_table -> handler::ha_rename_table -> ha_innobase::rename_table(调用一次即 可) alter table tnew rename to tlock; mysql_execute_command -> !(flags & ~(ALTER_RENAME | ALTER_KEYS_ONOFF))(如果是 rename 命 令 , 则 走 优 化 后 的 流 程 ) -> mysql_alter_table -> close_cached_table -> wait_while_table_is_used -> mysql_rename_table -> ha_innobase::rename_table(不同于临时表 copy 方案,此处只需要调用一次) session 5: ntse 处理 add column 的流程,与 innodb 完全一致。全部交由 mysql 上层处理。 session 6: mysql_alter_table -> needed_online_flags |= HA_ONLINE_ADD_INDEX; needed_fast_flags |= HA_ONLINE_ADD_INDEX_NO_WRITES( 可 以 采 用 fast 的 方 式 创 建 索 引 ) -> handler::alter_table_flages -> ha_ntse::alter_table_flags(ntse 支持 fast index create/drop,因此 重载了 alter_table_flags 方法) -> mysql_create_table_no_lock(只创建 mysql frm 文件,ntse 引 擎本身不创建新表) -> ha_ntse::add_index(创建索引) -> 同时,由于 ntse 没有生成临时表,因 此也不需要 rename_table 操作 测试结论: 1. 通过分析 alter table 主流程,可以得出:通过新建表,然后交换的方式完成;由于完成 之后是全新的表,因此一致读事务无法跨越 ddl 查询;建表通过单条记录读取+插入的 循环方式,性能较差;最后需要调用两次 rename_table 函数。 2. 连续两次 rename 调用过程: a) Phase 1:rename,from = .\test\tnew to = .\test\#sql-1120-2 b) Phase 2:rename,from = .\test\#sql-1120_2 to = .\test\tnew c) Phase 3:drop,table_name = .\test\#sql-1120-2 d) 其中,表#sql-1120-2 是交换过程中的中间表 3. add/drop index,也被映射为 alter table 操作。 4. rename table 的函数调用流程大为简化。 5. 由于 alter table 操作是新建表+交换完成,因此跨越 ddl 的事务,无法保证一致读。 6. 为了实现 fast index create/drop,存储引擎必须实现 handler::alter_table_flags 方法,告 诉 mysql 引擎支持哪些 fast index 方案。innodb 不支持。 7. fast index create/drop 是另一种意义上的 online ddl support。此时,mysql 对表加的是 TL_LOCK_WRITE_ALLOW_READ 锁,允许表上有并发的只读操作存在。 8. ddl 操作,在原表记录全表 copy 到新表,准备进行 rename 之前,需要等待所有表上的 操作结束。等待功能由函数 wait_while_table_is_used 实现。 9. 表重命名的两种不同语法,对应的函数处理流程也不相同。 10. BUG 今天测试出 mysql 5.5.16+innodb 的一个 bug。所做操作如下:atler table t2 add column (f int)。 mysql 通过 create temp table+2 次 rename+drop old table 完成操作,对应的函数是 sql_table.cc 中 的 mysql_alter_table。如果在第一次 rename 调用成功后系统崩溃。系统恢复之后,表 t2 丢失。 在同事帮助下找到了表,过程如下:1.首先根据 frm 文件获得临时表名:#sql2-14a0-1 (注意,是 14a0-1,而不是 14a0_1,应该会有两个 frm 文件);2.构造可以查询的临 时表,表两边加上`:`#sql2-14a0-1`;3. show create table `#sql2-14a0-1`;4. create table t2 as select * from `#sql2-14a0-1`; 进一步的测试,ha_innobase::rename_table函数中间崩溃,会导致同样的表丢失问题, 而且丢失的表无法寻回。这一系列问题,究其根本原因,无论是 add column,还是 rename table,这些 ddl 操作,mysql 层与 innodb 层没有实现二阶段提交,难免会造成 表定义上下层不一致的现象。 如果产生 rename 出错的问题,同样可以找回原表。在我的测试中:rename table t2 to t3. innodb 层 执行退出,导致的结果是 mysql 层:有 t2.frm;innodb 层:有 t3 表。此时,只要将 t2.frm 改名为 t3.frm, 既可以访问 innodb 层面重命名之后的 t3 表。 11. ntse a) ntse 不支持 add column timestamp。Auto update timestamp is not supported b) ntse 不支持增加 unique index,如果此时已经存在 non-unique index c) ntse database 层面封装了 ddl 操作,主要目的是提供并发控制支持。 i. m_ddlLock,控制并发 ddl;open_table 时需要加 IS 锁,保证无并行 ddl ii. m_atsLock iii. m_metaLock,控制对于 database tableinfo hash 表的并发操作 12. ;ntse 支持;ntse 的方法定义如下: uint r = HA_ONLINE_ADD_INDEX; r |= HA_ONLINE_DROP_INDEX_NO_WRITES; r |= HA_ONLINE_ADD_PK_INDEX_NO_WRITES; r |= HA_ONLINE_DROP_PK_INDEX_NO_WRITES; r |= HA_ONLINE_ADD_UNIQUE_INDEX_NO_WRITES; r |= HA_ONLINE_DROP_UNIQUE_INDEX_NO_WRITES; 13. 参考文档: http://ddgrow.com/mysql-timestamp-%E4%B8%8D%E8%83%BD%E4%B8%BAnull -- 说 明 了 mysql timestamp 的用法 32 测试三十:partition & innoplugin 测试 mysql+innodb plugin 对于 partition 表的支持?partition 表在创建索引失败时的操作? mysql:5.1.49 innodb_plugin: 1.0.10 装载 innodb plugin,修改 my.ini 文件 ignore_builtin_innodb plugin-load=innodb=ha_innodb_plugin.dll;innodb_trx=ha_innodb_plugin.dll;innodb_locks=ha_in nodb_plugin.dll;innodb_lock_waits=ha_innodb_plugin.dll;innodb_cmp=ha_innodb_plugin.dll;inn odb_cmp_reset=ha_innodb_plugin.dll;innodb_cmpmem=ha_innodb_plugin.dll;innodb_cmpmem _reset=ha_innodb_plugin.dll innodb_plugin partition table 创建索引流程: mysql_alter_table -> ha_innobase::innobase_alter_table_flags -> ha_partition(sql folder)::add_index(任何一个分区表创建索引出错,直接 break,返回;不做任何出错处理) -> ha_innobase::add_index(循环调用每个分区表,add index) -> 测试结论: 1. mysql 5.1.49+innodb_plugin 1.0.10,分区表建索引中途不能失败,否则会导致表不可用。 上层 mysql frm 文件与底层 innodb 文件表定义不一致。主要原因在于,分区支持引擎 ha_partition,在分区表一个索引失败之后,并未回滚前面已经成功的索引,而是直接将 错误返回给 mysql 上层,mysql 无法感知分区表存在,而是标识本次创建索引失败。导 致上层 frm 文件与底层 innodb 部分分区表定义不一致(如果第一个分区表就出错,那么 上下层仍旧一致)。 2. innodb plugin 支持 fast create index;部分支持 fast drop index(innodb plugin 不支持 HA_ONLINE_DROP_PK_INDEX_NO_WRITES,因此如果 drop 的索引是 pk 索引,或则是最 后一个 uk 索引,就不能采用 fast 方式进行 drop)。 3. innodb 在索引创建出错时,并不会导致 mysql 上层与 innodb 层表定义不一致。因为 innodb 不支持 fast index create,采用的仍旧是 create temp + copy + rename 的方式。mysql 首先调用 ha_partition 引擎创建分区表,然后 copy 数据,失败,此时 mysql 调用 ha_partition 引擎删除分区表。由于还没有进行 rename table 操作,因此并不会出现不 一致现象。 4. 最新 mysql:percona server 5.5.15 中已经修正此 bug,在 ha_partition.cc->add_index 函 数中,如果中间表创建索引失败,那么所有前面完成创建的索引,都会删除。 while (--file >= m_file) { (void) (*file)->prepare_drop_index(table_arg, key_numbers, num_of_keys); (void) (*file)->final_drop_index(table_arg); } mysql 原版 5.5.16 中,此问题同样得到解决。 5. mysql 新版本虽然修正了此 bug,但是全面改写了 fast create index 的逻辑,首先宏定义 从 online 变为 inplace(更符合逻辑),其次,提高了可以应用 fast create index 的逻辑。在 innodb 中,pk index,uk index 都不支持 fast create。 测试语句: create table t1(a int not null, b int, c varchar(10))engine=innodb PARTITION BY RANGE (a) ( PARTITION p0 VALUES LESS THAN (2), PARTITION p1 VALUES LESS THAN (11), PARTITION p2 VALUES LESS THAN (16), PARTITION p3 VALUES LESS THAN MAXVALUE ); insert into t1 values(1, 1, 'aaa'); insert into t1 values(2, 2, 'bbb'); insert into t1 values(4, 3, 'ccc'); insert into t1 values(4, 2, 'abc'); create unique index idx_t_a on t1(a); 33 测试三十一:vs 2008 + mysql5.5 测试目的: 在 windows 环境下,使用 vs 2008 编译 mysql5.5 版本,创建数据库,完成调试 所遇到的问题: 1. mysqld 无法启动,最后发现是 mysqld.cc 文件中的 test_lc_time_sz 函数 Assert 出错。注 释掉该函数,通过。 2. mysqld 仍旧无法启动,报错是 innodb 无法创建 temp 文件。跟踪调试,发现 tmp 目录 设置有问题。my.ini 中设置为 D:\mysql\tmp\,运行时变成了 D:\mysqlmp\。放弃设置 tmp 目录,使用默认 tmp 目录,通过。 3. 使用脚本初始化数据库;或者是直接启动 mysqld,让 mysqld 自动创建数据库。创建数 据库完成之后,再次碰到问题:Fatal error: Can't open and lock privilege tables: Table 'mysql.host' doesn't exist 根本原因,在于初始化数据库根本没有成功,看 mysqld.log 得知。没有成功的原因是, 使用脚本初始化数据库时,没有按照参考文档中的要求,在每个脚本的最开始添加上 use mysql 语句。导致初始化脚本报错,没有选择数据库。 4. 数据库初始化完成之后,运行 mysqld 启动。然后尝试用 mysql –uroot 连接,此时再次 出现问题(第几个了啊…),报错,localhost 无法连接数据库。求助 google,发现比较好 的几篇文章,说是权限控制的问题,在 my.ini 文件[mysqld]段加上 skip-grant-tables,问 题解决,登入数据库。 5. 登入 mysql 之后,发现 mysql 的系统表,user/db/host/tables_priv/columns_priv 都为空。 这应该就是 mysql 客户端本地不能登陆的原因。需要加以解决。 6. mysql 5.5 之后,已经没有了原生版 innodb 与 plugin 的区别。 参考文档: http://dev.mysql.com/doc/refman/5.5/en/installing-source-distribution.html --生成工程文件, 并编译 http://www.falcon-monitor.com/blog/243.html --mysql 配置文件 my.cnf 优化详解 http://dev.mysql.com/doc/refman/5.5/en/option-files.html http://www.sealee.com/mysql/mysql-7785.html --windows 环境下初始化 mysql 数据 库 http://blog.csdn.net/alifel/article/details/6419945 --WIN7 如何查看端口被什么程序占 用 http://hi.baidu.com/wallsgrass/blog/item/ca7b443471a18c305bb5f5f3.html/cmtid/ce4765245cd 0ff26d5074243 --Host ‘localhost’ is not allowed to connect to this mysql server http://www.mysqlops.com/2011/07/22/mysql-account-privileges-manager.html --mysql 权限系统之权限知识和管理 http://www.mysqlops.com/2011/07/08/mysql-privilege-architecture.html --mysql 权限的架构体系 34 测试三十二:ntse online add index 测试目的: 测试 ntse 在线加索引的功能,为了设计 TNT dump 备份的 recover 算法与流程。 在线创建索引命令: set ntse_command = "add index on test.tntse idx1 (comment)"; 在线删除索引命令: set ntse_command = "drop index on schema.table idx_name, idx_name2"; 表 rename 命令: session 2: select * from ntse; session 1: rename table ntse to tntse; session 3: select * from tntse; session 2 先执行,然后执行 session 1,session 1 函数调用流程如下: mysql_execute_command -> mysql_rename_tables -> lock_table_names_exclusively -> lock_table_names -> lock_table_name -> remove_table_from_cache -> my_hash_delete -> free_cache_entry -> intern_close_table -> closefrm -> ha_ntse::close() -> ha_ntse::closeTable(由 于 session 2 打开过表,因此在 rename 之前,必须将表 close) -> rename_tables -> do_rename -> mysql_rename_table -> handler::ha_rename_table -> ha_ntse::rename_table -> ntse::Database::renameTable -> Table::rename (在 close 表操作完成之后,进行真正的 rename 表操作,此时仅仅进行 rename,但是不 open 表,并且 rename 过程由 X 锁保护,其他事务 不可访问当前表) -> ntse::ControlFile::renameTable –> ControlFile::updateFile -> (修改控制文件, 控制文件的修改操作,由 ATSLock(S), DDLLock(X)保护,因此在控制文件修改完成前,其他进 程无法访问对应表,正常 openTable 需要加 DDLLock(IS)) session 1 rename 操作结束,session 2 再次执行 select 操作,此时 open 新表。 测试结论: 1. rename 操作,需要关闭原表,然后进行 rename 2. 内存控制文件 class 中,有表名-表 id 的对应关系 hash:m_pathToIds 3. 控制文件的 update,都在 ddlLock,或者是 metaLock 的保护下进行 4. 在 ddl 过程中,控制文件更新完成之前,其他进程无法操作正在进行 ddl 的表 5. ddl 完成之前,会调用 Database::bumpFlushLsn,其目的是在 controlFile 中记录当前 ddl 表的最后操作 LSN,crash recover 时,所有表需要跳过其对应 flushLsn 之前的日志 35 测试三十三:group log write & flush 测试目的: 1. 测试 innodb 5.1.49 在非 binlog 模式下,是否支持日志文件的批量 write,批量 flush?因 为 binlog 模式已经确定,是不支持批量操作的。 2. 如果 innodb 5.1.49 非 binlog 模式下支持日志文件 group write & flush,then how? 测试语句: insert into tnew value (90871, ‘zzz’, current_timestamp(), current_timestamp()); 日志操作相关代码调用流程: ha_autocommit_or_rollback -> ha_commit_trans -> innobase_xa_prepare -> trx_prepare_for_mysql -> trx_prepare_off_kernel -> log_write_up_to -> log_group_write_buf -> fil_flush -> 日志模块 write/flush 分析: log_write_up_to 函数 输入: lsn: 需要写到/flush 到的日志序列号 wait: 等待模式,LOG_NO_WAIT ; LOG_WAIT_ONE_GROUP ; LOG_WAIT_ALL_GROUPS;不需要等待;等待一组日志,wait on one_flushed_event;等待所 有日志组,wait on no_flush_event; flush_to_disk: 是否需要 flush 流程分析: 1. 获得 log_sys->mutex 2. 当前已经 write/flush 超过 lsn,直接退出。 3. 当前正在进行 write/flush(log_sys->n_pending_writes > 0),则 判断当前 flush_lsn/write_lsn, 若超过 lsn,则进入等待后返回;若未超过 lsn,直接等待,然后返回流程 1,重新开始 4. 进入真正 write/flush 模块,首先设置 log_sys->n_pending_writes,告诉其他线程当前有 正在写的操作,让其他线程等待在步骤 3 5. 重置 log_sys->no_flush_event, log_sys->one_flush_event 6. 设置 write/flush 参数,将 log_sys->buf 中的内容写到 log groups 中(log_group_write_buf)。 写入的结束位置是当前最新的 lsn 所对应的 log buf 中的所有内容,在等待过程中,log buf 可能已经被其他线程写入。写入开始位置是 log_sys->buf_next_to_write,此值在步骤 12 的函数 log_group_check_flush_completion 中修改。 7. 释放 log_sys->mutex 8. 如果当前为非 Direct IO 模式,同时需要 flush,则调用 fil_flush 函数进行 flush,一个 group 9. 在步骤 8 期间,通过 n_pending_writes 取值来保护 critical section 10. 获得 log_sys->mutex 11. 修改 n_pending_writes 取值 12. flush 完成(log_group_check_flush_completion),设置 log_sys->buf_next_to_write——下一 个开始写入的点;同时,如果当前 buf 数据量超过 1/2,移动 buf,覆盖已 flush 数据, 释放 buf 空间,修改 log_sys->buf_next_to_write,log_sys->buf_free 取值 13. 设置 log_sys->no_flush_event, log_sys->one_flush_event 14. 释放 log_sys->mutex 写入 log_sys->buf 的流程: trx_undo_assign_undo -> mtr_commit -> mtr_log_reserve_and_write -> log_reserve_and_write_fast -> 函数 log_reserve_and_write_fast 分析: 1. 获得 log_sys->mutex 2. 写入日志到 log_sys->buf 3. 返回(log_sys->mutex 在 mtr_commit 函数中调用 log_release 释放) 分析: 一、innodb,写 log buffer,写 log 日志文件通过 log_sys->mutex 保护 二、flush 日志文件不需要 log_sys->mutex 保护 三、写日志文件,flush 日志文件通过 n_pending_writes 参数保护,若此参数设置,后续的 write & flush 操作必须等待 四、在 flush 日志文件时(调用 fsync),其他线程可以写 log buffer,然后等待在 write 日志文 件阶段 五、flush 完成之后,通过设置 one_flushed_event,no_flushed_event,可以唤醒正在等待的 其他 write 日志文件线程。 六、若支持 binlog,那么在 innobase_xa_prepare 函数中将获得 prepare_commit_mutex,直 到 commit 时释放,因此只能有单线程进入 prepare,无法实现 group commit 七、整个 prepare 操作,通过 kernel_mutex 保护(trx_prepare_for_mysql) 36 测试三十三(cont.): mutex & event 附: mutex 实现方法: mutex 定义:sync0sync.h,主要的属性有红色 4 项 struct mutex_struct { os_event_t event; /* Used by sync0arr.c for the wait queue */ ulint lock_word; /* This ulint is the target of the atomic test-and-set instruction in Win32 */ #if !defined(_WIN32) || !defined(UNIV_CAN_USE_X86_ASSEMBLER) os_fast_mutex_t os_fast_mutex; /* In other systems we use this OS mutex in place of lock_word */ #endif ulint waiters; /* This ulint is set to 1 if there are (or may be) threads waiting in the global wait array for this mutex to be released. Otherwise, this is 0. */ UT_LIST_NODE_T(mutex_t) list; /* All allocated mutexes are put into a list. Pointers to the next and prev. */ } mutex_create windows: CreateMutex(); linux: os_fast_mutex_init -> pthread_mutex_init -> mutex_str->handle = mutex; mutex->event = os_event_create(NULL); mutex_enter: sync0sync.ic::mutex_enter_func -> mutex_test_and_set -> windows x32: XCHG windows x86: TAS linux: os0sync.ic::os_fast_mutex_trylock -> pthread_mutex_trylock -> mutex_spin_wait (if needed) while(mutex_get_lock_word != 0 && I < SYNC_SPIN_ROUNDS ) 空转一定时间,不放弃 cpu slice,尝试是否可以获得 mutex -> if (I == SYNC_SPIN_ROUNDS) -> os_thread_yield -> Sleep(0) or pthread_yield or os_thread_sleep 空转尝试失败,放弃 cpu slice,进入休眠,等待下一次调度 -> mutex_set_waiters -> sync0arr.c::sync_array_wait_event -> os_event_wait_low -> windows: WaitForSingleObject(event->handle, INFINITE) Others: os_fast_mutex_lock(&event->os_mutex) -> for(;;) pthread_cond_wait(&event->cond_var, &event-> os_mutex) mutex_exit: mutex_reset_lock_word -> XCHG or TAS 汇编指令: 处理 lock_word os_fast_mutex_unlock:pthread_mutex_unlock(fast_mutex) -> mutex_get_waiters -> mutex_signal_object -> mutex_set_waiters(0) -> // mutex 中的 event,实现 mutex 的等待与唤醒,log_sys->mutex -> os0sync.c::os_event_set(mutex->event) -> windows: SetEvent(event->handle) linux: os_fast_mutex_lock(&event->os_mutex) -> pthread_cond_broadcast(&event->cond_var) -> os_fast_mutex_unlock(&event->os_mutex) (event->os_mutex 保护 pthread_cond_t::cond_var) mutex 实现分析: 一、mutex 的等待,不是直接通过 pthread_mutex_lock 实现,而是通过以下流程实现: a) 先调用 pthread_mutex_trylock,若不能马上获得,则进行 spin(不放弃 cpu slice) b) 若 spin 无法获得,进行 sleep c) 若 sleep 之后仍旧无法获得,通过 mutex 中的 event,进入等待 d) 为何要如此完成,而不是简单调用 pthread_mutex_lock? 二、 event 实现方法: event 定义: windows: HANDLE handle; os_event_list linux: os0sync.h struct os_event_struct { os_fast_mutex_t os_mutex; /* this mutex protects the next fields */ ibool is_set; /* this is TRUE when the event is in the signaled state, i.e., a thread does not stop if it tries to wait for this event */ ib_longlong signal_count; /* this is incremented each time the event becomes signaled */ pthread_cond_t cond_var; /* condition variable is used in waiting for the event */ UT_LIST_NODE_T(os_event_struct_t) os_event_list; /* list of all created events */ }; event 操作流程: os_event_create windows: CreateEvent linux: os_fast_mutex_init(&event->os_mutex) -> pthread_cond_init(&event->cond_var, NULL) -> event->is_set = FALSE; -> event->signal_count = 1; -> add event to os_event_list os_event_wait windows: WaitForSingleObject(event->handle, INFINITE); linux: os_fast_mutex_lock -> for(;;) // 无限循环,cond_wait 存在假唤醒情况 // 1. 真唤醒,如果是由 event_set 唤醒,那么 is_set 变量一定设置为 TRUE // 2. reset 前的状态为唤醒状态 is_set = TRUE,都直接返回 if(event->is_set ==TRUE || event->signal_count != old_signal_count) os_fast_mutex_unlock && return; pthread_cond_wait(cond_var, event->os_mutex); os_event_set windows: SetEvent() linux: os_fast_mutex_lock -> event->is_set = TRUE; -> pthread_cond_broadcast -> os_fast_mutex_unlock os_event_reset windows: ResetEvent linux: os_fast_mutex_lock(&event->os_mutex) -> event->is_set = FALSE; -> os_fast_mutex_unlock(); 参考文献: [1]http://www.orczhou.com/index.php/2009/08/innodb_flush_method-file-io/ innodb_flush_method 与 File I/O [2]http://kerneltrap.org/node/7563 Linux: Accessing Files with O_DIRECT [3]http://mysqlha.blogspot.com/2009/06/buffered-versus-direct-io-for-innodb.html buffered vs direct io for innodb [4]http://www.orczhou.com/index.php/2010/06/mysql-innodb-source-code-sync-1/ Mysql/InnoDB 源代码:线程并发访问控制 [5]http://zqzhg0000.blog.163.com/blog/static/21915816201102710227948/ 互斥锁 pthread_mutex_t 的使用 [6] http://blog.csdn.net/qb_2008/article/details/6840570 spin lock 在 kernel 2.4 与 2.6 中的实现 与区别 [7]http://www.cppblog.com/mildforest/archive/2011/02/24/140610.html 关于 pthread_cond_wait 函数的理解 [8] [9] [10] [11] [12] [13] 37 测试三十四:innodb readview 测试 测试 innodb read_view 的定义与实现 innodb readview 定义: struct read_view_struct{ ulint type; /* VIEW_NORMAL, VIEW_HIGH_GRANULARITY */ dulint undo_no; /* (0, 0) or if type is VIEW_HIGH_GRANULARITY transaction undo_no when this high-granularity consistent read view was created */ dulint low_limit_no; /* The view does not need to see the undo logs for transactions whose transaction number is strictly smaller (<) than this value: they can be removed in purge if not needed by other views */ dulint low_limit_id; /* The read should not see any transaction with trx id >= this value */ dulint up_limit_id; /* The read should see all trx ids which are strictly smaller (<) than this value */ ulint n_trx_ids; /* Number of cells in the trx_ids array */ dulint* trx_ids; /* Additional trx ids which the read should not see: typically, these are the active transactions at the time when the read is serialized, except the reading transaction itself; the trx ids in this array are in a descending order */ dulint creator_trx_id; /* trx id of creating transaction, or (0, 0) used in purge */ UT_LIST_NODE_T(read_view_t) view_list; /* List of read views in trx_sys */ }; 关键属性解析: low_limit_no: low_limit_id: 所有 >= 此值的事务 id 所做的修改,当前 readview 均不可见 up_limit_id: 所有 < 此值的事务 id 所做的修改,当前 readview 一定可见 n_trx_ids: trx_ids: readview 创建时,活跃事务列表,此列表中的事务所做的修改,不可见, 事务 id 在其中,按照从下到大的顺序排列 innodb 事务开始处理流程: 1. trx_create:创建事务,不分配事务 id,不开始此事务 :conc_state=TRX_NOT_STARTED 2. trx_start:开始一个事务。获取事务 id,设置事务状态:conc_state = TRX_ACTIVE 38 测试建表三十五: utf8 21845 vs 21846 目的: create table my_utf8 (name varchar(21845)) engine = innodb default charset utf8;语句报错 create table my_utf8 (name varchar(21846)) engine = innodb default charset utf8;语句执行成功。 跟踪出现两种不同情况的原因? 测试一: create table utf2 (name varchar(20)) engine = innodb default charset = utf8; 函数调用流程: sql_parse.cc::do_command -> sql_parse.cc::dispatch_command -> mysql_parse -> mysql_execute_command -> mysql_create_table -> mysql_create_table_no_lock -> rea_create_table -> ha_create_table -> handler::ha_create -> ha_innobase::create -> 测试二: create table my_utf7 (name varchar(21845)) engine = innodb default charset utf8; 函数调用流程: mysql_create_table -> rea_create_table -> mysql_create_frm -> unireg.cc::pack_header -> length = field->pack_length; // 此处取 pack_length,而非 length reclength = field->offset +data_offset + length = 65538; if (reclength > (ulong) file->max_record_lenght()(65535)) my_error(ER_TOO_BIG_ROWSIZE); field->length=65535; field->char_length=21845; field->pack_length=65537; field->key_length=65535; field->unireg_check=NONE; 测试三: create table my_utf9(name varchar(21845)) engine = innodb default charset utf8; 函数流程: pack_header,不同之处 field->length = 8; field->char_length = 21846; field->pack_length=11; 11 = 8+3,其中 8 为 blob 指针长度 field->key_length=65538 field->unireg_check=BLOB_FIELD 原因分析: mysql_create_table_no_lock -> mysql_prepare_create_table -> field.cc::create_length_to_internal_length -> sql_table.cc::prepare_blob_field -> if(sql_field->length > MAX_FIELD_VARCHARLENGTH(65535) // 此 处 取 length 进 行 判 断 , 而 非 pack_length && !(sql_field->flags & BLOB_FLAG)) pack_length = 39 测试三十六:innodb 表元数据并发控制 目的: 测试 innodb 如何实现表元数据的并发控制 create table: row_mysql_lock_data_dictionary(trx); row_mysql_unlock_data_dictionary(trx); open table: mutex_enter(&(dict_sys->mutex)); dict_table_get_low(table_name); mutex_exit(&(dict_sys->mutex)); close table: row_prebuilt_free(); dict_table_decrement_handle_count(); mutex_enter(&(dict_sys->mutex)); table->n_mysql_handles_opened--; mutex_exit(&(dict_sys->mutex)); drop table: row_drop_table_for_mysql(); row_mysql_lock_data_dictionary(trx); rw_lock_x_lock(&dict_operation_lock); trx->dict_operation_lock_mode = RW_X_LATCH; mutex_enter(&(dict_sys->mutex)); row_add_table_to_background_drop_list( if table->n_mysql_handles_opened > 0); lock_remvoe_all_on_table(table, TRUE); row_mysql_unlock_data_dictionary(trx); mutex_exit(&(dict_sys->mutex)); rw_lock_x_unlock(&dict_operation_lock); trx->dict_operation_lock_mode = 0; truncate table: 注意:truncate table 操作会生成新的 table id row_truncate_table_for_mysql(); row_mysql_lock_data_dictionary(trx); rw_lock_x_lock(&dict_operation_lock); trx->dict_operation_lock_mode = RW_X_LATCH; mutex_enter(&(dict_sys->mutex)); lock_remove_all_on_table(table, FALSE); // 获取一个新的 table id dict_hdr_get_new_id(DICT_HDR_TABLE_ID); dict_table_change_id_in_cache(table, new_id); row_mysql_unlock_data_dictionary(trx); mutex_exit(&(dict_sys->mutex)); rw_lock_x_unlock(&dict_operation_lock); trx->dict_operation_lock_mode = 0; rename table: 注意:rename table 不改变 table id row_mysql_lock_data_dictionary(trx); rw_lock_x_lock(&dict_operation_lock); trx->dict_operation_lock_mode = RW_X_LATCH; mutex_enter(&(dict_sys->mutex)); dict_table_get_low(old_name); dict_table_rename_in_cache(table, new_name, !new_is_tmp); row_mysql_unlock_data_dictionary(trx); mutex_exit(&(dict_sys->mutex)); rw_lock_x_unlock(&dict_operation_lock); trx->dict_operation_lock_mode = 0; dict_operation_lock 在前面的调用中,都是 RW_X_LATCH。 dict_operation_lock 何时会被加上 RW_S_LATCH? 1. 做 foreign key check 的时候 a) row_ins_check_foreign_constraints 2. 解析 undo 日志的时候(rollback,purge) a) row_purge_parse_undo_rec b) row_undo row_mysql_freeze_data_dictionary(trx); rw_lock_x_lock(&dict_operation_lock); trx->dict_operation_lock_mode = RW_S_LATCH; row_mysql_unfreeze_data_dictionary(trx); 总结: 1. dict_sys->mutex 用于保护内存表数据字典 2. dict_operation_lock 用于保护表元数据不被修改 3. innodb 大部分 ddl(除了 rename 操作),包括 add/drop index,add/drop column,truncate, 都会改变表 ID,表名相同,表 ID 不同,内部看来是不同的表,外部看起来是相同的表 4. mysql 上层无法感知的操作,包括 foreign key check,rollback,purge,需要加共享元数 据锁,保证表定义不会被修改(此处特指删除) 5. mysql 发出的操作,不需要加 dict_operation_lock 元数据锁,因为 mysql 上层发出 ddl 操作之前,会调用 close 方法关闭已有的操作 6. 可参考 row_truncate_table_for_mysql 函数中的注释 7. 40 测试三十七:ntse 引擎 Table 模块 目的: 测试 ntse 引擎 Table 模块实现的各功能 测试一:全表扫描 select * from ntse; 调用过程: ha_ntse::beginTblScan -> Table::tableScan -> Table::beginScan -> Table::getNext -> Records::Scan::getNext -> m_records->m_heap->getNext (VariableLengthRecordHeap::getNext) -> 测试二:索引范围扫描 select * from ntse where id > 4; 调用过程: ha_ntse::index_read_map -> ha_ntse::indexRead -> Table::indexScan -> Table::beginScan -> scan->m_index = m_indice->getIndex(cond->m_idx) -> scan->m_coverageIndex = isCoverageIndex(m_tableDef, scan->m_indexDef, scan->m_readCols) (只要扫描的列都在索引中包含,则认为是索引覆盖扫描) -> scan->m_recInfo = m_records->beginBulkFetch() -> scan->m_mysqlRow = fetch->getMysqlRow() -> scan->m_indexScan = scan->m_index->beginScan() -> Table::getNext -> scan->m_index->getNext -> scan->m_indexScan->getRowId -> fetch->getNext -> 测试三:索引唯一扫描 select * from ntse where id = 4; 调用过程: 与索引范围扫描过程基本一致,不同之处在于: 1) Table::getNext 函数调用 Table::getNext -> scan->m_index->getByUniqueKey() -> fetch->getNext(rid, mysqlRow, None) 2) 由于是唯一扫描,因此找到一项之后,扫描已经结束,不需要进行再次的 getNext,判 断 scan 是否结束 测试四:索引覆盖扫描 select comment from ntse where comment = ‘aaa’; 与索引范围扫描过程基本一致,不同之处在于: 1) 索引覆盖扫描,不需要加行锁 2) 索引覆盖扫描,在索引扫描 getNext 之后,不需要进行 Records 层面的 getNext 操作 测试四-二:innodb 索引覆盖扫描 测试五:insert insert into ntse values (10, ‘1129’, NULL); 调用流程: mysql_insert -> write_record -> handler::ha_write_row -> ha_ntse::write_row -> Table::insert -> Records::prepareForInsert -> Records::insert(插入表记录) -> m_indice->insertIndexEntries(插入索引记录,如果当所有的唯一索引都 insert 完毕,则 insert 操作不会被回滚;如果没有索引,则表数据插入之后,调用 Session::setTxnDurableLsn,持久 化 insert,因此 TNT_UNDO_I_LOG 需要在此之前写入) -> m_db->getNTSECallbackManger()->callback(写 binlog,在此之前失败,会导致 binlog 与 ntse 记录不一致?binlog 写的太晚?) 测试五-二:insert on duplicate key update insert into ntse values (10, ‘1129’, NULL) on duplicate key update comment = ‘1130’; 调用流程: mysql_insert -> write_record -> handler::ha_write_row -> ha_ntse::write_row -> m_replace == true -> m_iuSeq = m_table->insertForDupUpdate(调用此函数实现 insert on duplicate key update) -> Table::insert( 先 尝 试 一 次 insert ,不冲突直接成功,返回记录 RowId ; 冲 突 则 返 回 INVALID_ROW_ID) –> Table::indexScan -> Table::getNext(根据返回冲突的 dupIndex 序号,进行 Index unique scan,找 到冲突列,生成 IUSequence 结构) -> 返回上层 mysql 上层,在收到 duplicate 错误之后,判断当前是 duplicate key update,不需要报错,继 续调用后续 update 流程,进行 update 操作: mysql_insert -> write_record -> handler::ha_update_row -> ha_ntse::update_row(判断:若用户 update,则必定先进行 scan,m_scan 不为 NULL;若是 on duplicate key update,则 m_iuSeq 一定不为 NULL) -> Table::updateDuplicate -> 注意: NTSE 处理 insert on duplicate key update,与 innodb 的不同之处在于,在发生 duplicate 之后, write_record 函数的判断(table->file->ha_table_flag() & HA_DUPLICATE_POS)的返回结果不同:  Innodb  判断失败,无法进行 position scan,因此需要进行一次 index scan,找到冲突的行, 并读取行的所有属性。  Ntse  判断成功,直接进行 position scan 即可。因为在函数 insertForDupUpdate 中,判断 出 duplicate,ntse 会自动进行一次 index unique scan,取到冲突行,存入 m_iuSeq 结构之中  write_record -> table->file->rnd_pos -> ha_ntse::rnd_pos -> 测试五-三:insert ignore insert ignore into ntse values (10, ‘1201’, NULL); 调用流程: mysql_insert -> write_record -> handler::ha_write_row -> ha_ntse::write_row -> Table::insert(insert 产生唯一性冲突,undo insert,直接返回 mysql 上层即可) -> 测试五-四:replace replace into ntse values (10, ‘1201’, NULL); 调用流程: 前半部分的流程与 on duplicate key update 的流程一致,一直到构造 m_iuSeq,返回 mysql。 后半部分: write_record 函数处理: replace into 语法定义:若不冲突,直接 insert;若冲突,删除原项,insert 新项  路径一:cheating 路径(满足一定条件) table->file->ha_update_row -> handler::ha_update_row -> ha_ntse::update_row -> Table::updateDuplicate 路径一下,m_replace 参数的设定: mysql_insert -> table->file->extra(HA_EXTRA_WRITE_CAN_REPLACE) -> m_replace = true; -> table->file->extra(HA_EXTRA_IGNORE_DUP_KEY) -> m_ignoreDup = true; Success Running…  路径二:正常路径 mysql_insert -> write_record -> handler::ha_delete_row -> ha_ntse::delete_row -> Table::deleteDuplicate -> mysql_insert -> write_record -> handler::ha_write_row -> ha_ntse::write_row -> Table::insertForDupUpdate -> 首先需要增加 delete trigger drop trigger if exists trihdc; delimiter $$ CREATE TRIGGER trihdc AFTER delete ON ntse FOR EACH ROW BEGIN insert into t1129 values(1); END$$ delimiter ; 路径二下,m_replace 参数的设定: mysql_insert -> table->file->extra(HA_EXTRA_IGNORE_DUP_KEY) -> m_ignoreDup = true; m_replace = false; Failed Running… 测试六:update update ntse set comment = ‘bbb’ where id = 4; 1) 先做索引唯一扫描,OpType = OP_WRITE;需要加行锁 X,流程如下: mysql_update -> info.read_record(&info) 2) 进行 update 操作,流程如下: mysql_update -> handler::ha_update_row -> ha_ntse::update_row -> Table::updateCurrent -> TblScan->prepareForUpdate -> Session::constructPreUpdateLog -> writePreUpdateLog(写 update 日志) -> m_indice->updateIndexEntries(修改 update 字段涉及到的索引) -> m_db->getNTSECallbackManger()->callback(写 binlog,更新索引成功,update 就一定成功?) -> scan->m_recInfo->updateRow(更新表记录) 测试七:delete delete from ntse where id = 4; 1) 先做索引唯一扫描,OpType = OP_WRITE;需要加行锁 X,流程如下: mysql_delete -> info.read_record(&info) 2) 进行 delete 操作,流程如下: mysql_delete -> handler::ha_delete_row -> ha_ntse::delete_row -> Table::deleteCurrent -> scan->prepareForDelete -> writePreDeleteLog(写 delete 日志) -> m_indice->deleteIndexEntries(删除索引项) -> scan->m_recInfo->deleteRow(删除表记录) -> m_db->getNTSECallbackManger()->callback(写 binlog,删除数据成功,为什么与 update 的位 置不同?) 41 测试三十八:truncate vs drop 测 试 了 innodb5.1.49. 无论是 truncate , 还 是 drop , 首 先 调 用 row_mysql_lock_data_dictionary 函数,锁住整个表数据字典;然后遍历表上的所有索引, 调用 btr_free_but_not_root 函数,释放除跟页面之外的所有索引页面;最后 row_mysql_unlock_data_dictionary 函数,释放表数据字典锁。二者没有本质上的区别。 42 测试三十九:加锁逻辑 innodb vs ntse 目的:简单测试 innodb 与 ntse 的加锁逻辑,从而指导 tnt 引擎的加锁实现 Innodb: store_lock 与 external_lock 函数中设置 prebuilt->select_lock_type 参数,行级;表级 锁通过 select_lock_type 推出 1) select * from tpurge; 2) select * from tpurge lock in share mode; 3) select * from tpurge for update; row lock type table lock type 1) NULL NULL 2) LOCK_S LOCK_IS 3) LOCK_X LOCK_IX 一致读下,innodb 不加表意向锁,如何保证并发正确性?表元数据不被修改? NTSE:store_lock 与 external_lock 函数中设置 ha_ntse::m_wantLock 参数,表级;行级锁通过 m_wantLock 推出 meta lock type row lock type table lock type 1) IL_S IL_IS 2) IL_S IL_IS 3) IL_S IL_X (这个模式太强?) TNT:store_lock 与 external_lock 函数中设置,ha_tnt::m_wantLock 参数,表级;行级锁通过 m_wantLock 推出 meta lock type row lock type table lock type 1) IL_S NULL NULL 2) IL_S IL_S IL_IS 3) IL_S IL_X IL_IX 43 测试四十:mysql+ntse 实现 update 目的: 测试 mysql+ntse 如何实现一个 update 操作,用于指导 tnt 设计 测试一:基于主键更新 测试语句: update ntse set gmt_create = 1201 where id = 2; 函数主要流程:Index unique scan + update First Round:index unique scan mysql_update -> rr_quick -> QUICK_RANGE_SELECT::get_next -> handler::read_multi_range_first -> handler::read_range_first(table->record[0], start_key->key, start_key->keypart_map) -> ha_ntse::index_reap_map(buf)( 保 存 输 出 记 录 的 缓 冲 区 , table->record[0]) -> ha_ntse::indexRead -> Table::indexScan -> Table::getNext(mysqlRow = buf) After first before secod:更新内容的准备与内容填充 mysql_update -> store_record(将 table->record[0]中的内容 copy 到 table->record[1],因此 table->record[1] 中为 old_data , 但 是 table->record[0] 中的 new_data 何 时 填 充 ?) -> fill_record_n_invoke_before_triggers(.,fields,values,.)(fileds+values 为 update 的更新项和内容) -> fill_record(fields, values) -> Second Round:update mysql_update -> table->file->ha_update_row(table->record[1], table->record[0])(其中,record[1] 为 olddata , record[0] 为 newdata) -> handler::ha_update_row(old_data, new_data) -> ha_ntse::update_row -> Table::updateCurrent(update, oldRow)(update = table->record[0], oldRow = table->record[1]) -> TblScan::prepareForUpdate(update, oldRow) -> Records::BulkOperation::prepareForUpdate(oldRow, update) -> MmsTable::canUpdate -> RecordOper::getUpdateSizeVR(m_tableDef, &oldRecord, subRecord)(其中,subRecord 为 updaet 属性信息,此函数判断 oldRow 中,哪些属性需要更新,并计算更新之后的行长度) -> session->constructPreUpdateLog(m_tableDef, scan->m_redRow, &rsUpdate->m_updateMysql…) -> 44 测试四十一:Halloween,RBR 目的: 测试 mysql 如何处理 Halloween 问题?如何实现 Row-Based Replication(有主键 vs 无主键)? 测试一:无主键表,通过索引更新 测试语句: CREATE TABLE `DailyStatistic` ( `UserId` bigint(20) NOT NULL DEFAULT '0', `BlogCount` int(11) NOT NULL DEFAULT '0', `PhotoCount` int(11) NOT NULL DEFAULT '0', `SpaceUsed` int(11) NOT NULL DEFAULT '0', `DateTime` bigint(20) NOT NULL DEFAULT '0', KEY `IDX_DAYSTAT_USERID_DATETIME` (`UserId`,`DateTime`) ) ENGINE=NTSE DEFAULT CHARSET=gbk; insert into dailystatistic values (1, 1, 1, 100, 1322755200000); UPDATE DailyStatistic SET BlogCount = (BlogCount + 1) WHERE (UserId = 1) AND (DateTime = 1322755200000); 函数调用流程: mysql_update -> 1. 重设 write_set & read_set: table->mark_columns_needed_for_update(修改 table->write_set, 开始为 2=00010,更新第 2 列,此时是正确的;table->read_set = 10011,也 OK) -> st_table::mark_columns_needed_for_update -> file->ha_table_flags(取出 table 的 flag 设置 file->ha_table_flags() 25837437323 : handler.h) -> handler::use_hidden_primary_key(HA_PRIMARY_KEY_REQUIRED_FOR_DELETE 设置,同时表上 无主键, 调 用 此 函 数 ;如果表上有主键,调用的函数是: mark_columns_used_by_index_no_reset , 将 主 键 列 加 入 read/write bitmap 中 ) -> table.h::use_all_columns -> column_bitmaps_set(将 read_set,write_set 都设置为取所有列) -> handler::column_bitmaps_signal -> 2. 判 断 是 否 会 出 现 Halloween 问题: used_key_is_modified = select->quick->is_keys_used(table->write_set)(检查需要更新的列,是否也同时包含在 search key 中,如果此检查通过,那么说明表不能直接更新,而需要先取出所有满足条件的项) -> QUICK_SELECT_I::is_keys_used -> old_covering_keys.is_set(used_index)(此处判断是取当前索引 列,还是取表的所有列) -> table->add_read_columns_used_by_index() -> 3. 更新所有列,存在 Halloween 问题,采用不同的更新方法:while(info.read_record(&info)) -> my_b_write( 循环读取记录,并将满足条件的记录保存 在 temp 文 件 中 ) -> while(info.read_record(&info)) -> table->file->ha_update_row(循环取出前面保存的记录,并逐 个更新) 疑问一:以上 update 的是 BlogCount 字段,为什么 used_key_is_modified 会被设置为 true? 将以上的用例用 innodb 引擎测试,问题没有重现,两者的区别在于: idx key map = 17 = 10001,索引列是第 0,4 列 ntse: table->write_set->bitmap = (-1) = 11111,需要更新所有列,当然包括索引列 innodb:table->write_set->bitmap = 2 = 01000,不需要更新索引,没有 Halloween 问题 应该是 ntse 的 bug。下周调试。 2011-12-05:已确认,非 NTSE bug,原因在以上的步骤中已经给出,NTSE 采用的是行级复 制,RBR,对于没有主键的表做 update 操作,需要将 read_set/write_set 均设置为行对应的 所有列. 将以上的 update sql 修改如下: UPDATE DailyStatistic SET DataTime = (DataTime + 1) WHERE (UserId = 1) AND (DateTime = 1322755200000); innodb:table->write_set->bitmap = 16 = 10000,需要更新索引列,有 Halloween 问题,需要 先取记录,然后更新。 mysql+innodb 处理 Halloween 问题的详细流程: mysql_update -> 1. 已 经 产 生 Halloween 问题,需要做的前期准备 , 如 下 : table->add_read_columns_used_by_index -> set_keyread -> ha_innobase::extra(HA_EXTRA_KEYREAD)(告诉 innodb,当前需要读取的,是表的主键) -> bitmap_copy -> mark_columns_used_by_index_no_reset(将主键列,添加到 read_set 之中,由 于主键是内部 logical row_id,mysql 不可见,此处仍旧保持 read_set = 17 = 10001) -> 2. 读取所有满足 where 条件的记录,存入 tempfile 之中: while(info.read_record(&info)) -> table->file->position(table->record[0]) -> ha_innobase::position(record , 将 当 前 行 存 入 handler->ref 空间之中:如果是用户定义主键,则将主键 copy 入 ref 中;若是无主键表,则 将 logical row_id copy 入 ref 中。此处是 rowid) -> 3. 满 足 条 件 项 读 取 完 毕 , 开 始 逐 项 更 新 记 录 : while(info->read_record(&info)) -> rr_from_tempfile -> file->rnd_pos -> ha_innobase::rnd_pos -> change_active_index -> btr_pcur_open_with_no_init -> btr_cur_search_to_nth_level(定位索引叶节点,search path) -> page_cur_search_with_match( 页内二分查找,定位到满足条件的项 ) -> cmp_dtuple_rec_with_match(由于 clust index 是 innnodb 内部生成的,根据 rowid 组织,因此 只比较 rowid 即可) -> 分析:  Halloween 问题,mysql 通过先 fetch(fetch 什么?),后更新(如何更新?)的方式解决。  fetch 什么?  NTSE 堆表:fetch rowid,保存在上层的 tempfile 中(mysql_update);  INNODB 索引组织表:主键表,fetch primary key;非主键表:fetch logical row_id  如何更新?rnd_pos scan  NTSE 堆表:通过保存的 rowid 定位记录,然后更新  INNODB 索引组织表:通过主键(logical rowid),在聚簇索引上做 unique scan,找到 对应的项,然后更新  如何判断出现 Halloween 问题?  mysql_update 函数中,通过对比 write_set 与索引的列,如果索引列出现在 write_set 中,则认为出现了 Halloween 问题  如何实现 Row-Based Replication?  NTSE 堆表:为了实现 RBR,必须保证能够唯一定位一行记录。  表存在主键,则通过主键可以唯一确定一条记录  表不存在主键,则需要通过记录全项(用户定义属性+rowid)唯一确定一条记录  设置 table flag:HA_PRIMARY_KEY_REQUIRED_FOR_DELETE,说明 delete/update 时,需要读取主键,如果没有主键,则 mysql 直接将 read_set 与 write_set 转 换为 all_columns  类似于 Oracle supplemental log。shareplex,goldenGate 等软件解析 Oracle redo log,并应用到备库,必须要求 redo log 中包含唯一确定一行的附件属性值(主 键 or 完整行记录)  INNODB 索引组织表:未测试  Mysql + innodb,对 于无主键表,innodb 内部会产生 logical row_id,作为表的逻辑主键。 logical row_id 的产生方式,在下面的测试中给出。 总结:  Halloween 问题是有害的,需要首先 scan 记录存储在 tempfile 中,然后在进行 position scan,并完成 update。对于 mysql+innodb,由于 innodb 没法通过 rowid 直接定位页面, 因此其在 rnd_pos 函数中,是通过主键索引的 Unique scan 实现,比对表 rowid scan 要 慢。  频繁更新的属性,如果可能,不放在索引中,降低 Halloween 问题产生的概率。  开启 RBR,日志中需要额外记录部分属性,用于唯一定位变化的行记录。  45 测试四十二:innodb 无主键表 目的: 测试 innodb 如何处理无主键表,如何生成 clust index,如何产生 clust index column CREATE TABLE `DailyStatistics` ( `UserId` bigint(20) NOT NULL DEFAULT '0', `BlogCount` int(11) NOT NULL DEFAULT '0', `PhotoCount` int(11) NOT NULL DEFAULT '0', `SpaceUsed` int(11) NOT NULL DEFAULT '0', `DateTime` bigint(20) NOT NULL DEFAULT '0', KEY `IDX_DAYSTAT_USERID_DATETIME` (`UserId`,`DateTime`) ) ENGINE=INNODB DEFAULT CHARSET=gbk; 语句一: insert into dailystatistics values (1,2,3,4,5); 生成 clust index column(rowid)方法: row_insert_for_mysql -> row_ins_step -> row_ins -> row_ins_alloc_row_id_step(生成 rowid) -> dict_sys_get_new_row_id -> mutex_enter(&(dict_sys->mutex)); id = dict_sys->row_id; mutex_exit(&(dict_sys->mutex)); 此时产生的 row_id = 2050 语句二: update dailystatistics set UPDATE DailyStatistics SET userid = 1021 WHERE (UserId = 1) AND (DateTime = 5); 读取 clust index column 方法: row_search_for_mysql -> row_sel_store_row_id_to_prebuilt -> 此时取出的 row_id = 520,不等于 2050,why? 分析:  rowid 如何存储? 2050 = 1000,0000,0010 520 = 0010,0000,1000 因此,只要将 2050 改为大端存储,既为 520。 2051 = 1000,0000,0011 取出的 row_id 应该是:0011,0000,1000 = 776 2052 = 1000,0000,0100 ~ = 100,0000,1000 = 1032,验证通过。  尽量为所有表指定主键。  如果不指定主键,innodb 会产生一个全局的 rowid 序列。所有 innodb 非主键表共 享这一序列,并发性能较差,因此建议所有 innodb 表,指定主键。 table flags: handler.h 46 测试四十三:innodb 处理 utf8 目的: 测试 innodb 如何处理 utf8? 测试语句: CREATE TABLE `sbtest` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `k` int(10) unsigned NOT NULL DEFAULT '0', `c` char(120) NOT NULL DEFAULT '', `pad` char(60) NOT NULL DEFAULT '', PRIMARY KEY (`id`), KEY `k` (`k`) )ENGINE=INNODB DEFAULT CHARSET=utf8; insert into sbtest(k,c,pad) values (0, '', 'qqqqqqqqqqwwwwwwwwwweeeeeeeeeerrrrrrrrrrtttttttttt'); 处理流程: ha_innobase->write_row -> row_insert_for_mysql –> row_mysql_convert_row_to_innobase -> row_mysql_store_col_in_innobase_format -> row_ins_step -> row_ins -> row_ins_index_entry_step -> row_ins_index_entry_set_vals() -> while (col_len > n_chars && ptr[col_len - 1] == 0x20)(计算 char 可以被压缩到什么长度,for utf8) -> dfield_set_data(dfield, ptr, col_len) -> 总结: innodb 会压缩 utf8 下的 char 47 测试四十四:sync_array thread_concurrency 目的: 测试 Innodb 的 sync_array 实现方式 测试 innodb 的两个参数: innodb_sync_spin_loops innodb_thread_concurrency innodb_concurrency_tickets innodb_thread_sleep_delay 参考:  测试三十三:mutex & event  http://www.mysqlperformanceblog.com/2011/12/02/kernel_mutex-problem-or-double-thr oughput-with-single-variable/  http://www.mysqlperformanceblog.com/2011/12/02/kernel_mutex-problem-cont-or-triple- your-throughput/  http://www.mysqlperformanceblog.com/2006/06/05/innodb-thread-concurrency/ InnoDB thread concurrency  http://blogs.innodb.com/wp/2011/07/improve-innodb-thread-scheduling/ Improve InnoDB thread scheduling (mysql 5.6.3 针对 thread 做的优化) 测试一: select count(*) from dailystatistics; 调用流程: enter_innodb ha_innobase::index_first -> ha_innobase::index_read -> innodb_srv_enter_innodb -> srv0srv.c::srv_conc_enter_innodb -> thd_is_replication_slave_thread(trx->mysql_thd)(判断当前 是否为 slave 线程) -> 条件 1:if (trx->n_tickets_to_enter_innodb > 0)(若当前不是第一次进入 innodb,同时有剩余的 tickets,tickets--,直接返回) -> os_fast_mutex_lock(&srv_conc_mutex)(进入 connection mutex,尝试获取新的连接) -> 条件 2:if (srv_conc_n_threads < (int)srv_thread_concurrency)(判断当前 innodb 内线程数量, 是否小于定义的 innodb_thread_concurrency,若是,直接获取) -> 条件 2.1:trx->n_tickets_to_enter_innodb = SRV_FREE_TICKETS_TO_ENTER (进入 innodb,获取 票根,默认为 500 次,同一事务可以连续进入 innodb 500 次,不需要判断) -> 条件 3:if (SRV_THREAD_SLEEP_DELAY > 0) -> os_thread_sleep(sleep 一段时间,再次判断,默 认为 10000) -> 条件 4:for(I = 0; I < OS_THREAD_MAX_N; i++)(获取一个等待队列中的空闲项,空闲项数目是 innodb 能够创建的最大线程数,也是 wait slot arrary 数目,我的测试中是 10000) -> 条件 4.1:if (I == OS_THREAD_MAX_N)(等待队列已满,直接进入) -> 条件 5:os_event_wait(slot->event) 获取 slot,进入等待队列 srv_conc_queue,直至被唤醒 exit_innodb innodb_srv_conc_exit_innodb -> srv0srv.c::srv_conc_exit_innodb -> 条件 1:if (trx->n_tickets_to_enter_innodb > 0)(若 tickets 有多余,直接返回,不真正退出) -> srv_conc_force_exit_innodb(trx)(真正退出 innodb,sql statement 结束/lock wait 时,都会退出 innodb,进入 lock wait 的线程,不统计) -> 条件 2:if (srv_conc_n_threads < (lint)srv_thread_concurrency)(若当前 innodb 内部线程小于最 大线程数,尝试唤醒一个正在等待进入的线程) -> os_event_set(slot->event)(如果 slot 存在) 测试二: 总结:  innodb_srv_conc_enter_innodb 在 真正进入 innodb 存 储 时 调 用 , 在 函 数 row_search_for_mysql,row_update_for_mysql,row_insert_for_mysql 前  innodb_srv_conc_exit_innodb 在退出 innodb 存储时立即调用,在函数 row_search_for_mysql,row_update_for_mysql,row_insert_for_mysql 后  srv_conc_force_exit_innodb 在语句结束或者是加锁等待时,调用此函数,彻底退出 innodb  48 测试四十五:Mysql+innodb statistics 目的:  mysql+innodb 收集哪些统计信息?  mysql+innodb 如何收集统计信息?  mysql+innodb 如何使用统计信息?  mysql+innodb 如何根据统计信息确定执行路径  单表 unique scan 的路径选择?  已完成  单表 range scan 的路径选择?  已完成  单表 index only scan 的路径选择?  已完成  多表 join 选择? 测试准备: create table nkeys (c1 int primary key, c2 int unique, c3 int unique, c4 int unique, c5 int) engine = innodb; insert into nkeys values (1,1,1,1,1); insert into nkeys values (2,2,2,2,2); insert into nkeys values (3,3,3,3,3); insert into nkeys values (4,4,4,4,4); insert into nkeys values (5,5,5,5,5); 测试一: show index from nkeys; 函数调用流程: sql_parse.cc::mysql_execute_command(lex->sql_command == SQLCOM_SHOW_KEYS) -> sql_show.cc::get_schema_stat_record -> 1. ha_innobase::info(HA_STATUS_VARIABLE | HA_STATUS_NO_LOCK | HA_STATUS_TIME)(info 函数,实现统计信息收集功能,宏定义说明了需要收集的统计信息的类型) -> 2. dict0dict.c::dict_update_statistics(ib_table)( HA_STATUS_VARIABLE,指定此参数时进行统计 信息的重新收集) -> 3. dict_update_statistics_low(统计信息收集主函数,遍历表上的所有 index,收集统计信息, 设置到 dict_index 与 dict_table 结构之中) -> btr_estimate_number_of_different_key_vals -> a) dict_index i. 索引总页面数;叶节点页面数 ii. 索引中不同键值个数,对于索引中的每一列,都需要计算。 1. 随机在索引中定位 BTR_KEY_VAL_ESTIMATE_N_PAGES = 8 次页面。一定是 8 次,无论实际页面是否小于 8. 2. 读取页面中的索引项,与前一项进行对比,如果不等,则++,需要跳过相 等的 prefix,n_diff[j]++ (其中 j 为前面相等的 prefix 列数) 3. b) dict_table i. 表总行数。 table->stat_n_rows = index->stat_n_diff_key_vals[dict_index_get_n_unique(index)]; 总行数,为表第一 个索引,unique key 的个数。 ii. 聚簇索引页面数 iii. 二级索引页面数 iv. table->stat_initialized = 1; 统计信息为重新收集的 v. table->stat_modified_counter = 0; 信息收集之后,表上无 DML 操作发生,统计 信息是准确的 4. 测试二:单表 unique key 查询 select * from nkeys where c1 = 3; 调用流程: mysql_execute_command -> handle_select -> mysql_select -> JOIN_optimize -> make_join_statistics -> 1. if (join->const_tables != join_tables)(判断不通过,走 else 路径,当前是单表 uk 查询,直 接返回执行计划,不需要进行 choose_plan 调用) -> 测试三:单表 range 查询 select * from nkeys where c3 > 3; 不能进行索引覆盖扫描 index range scan select c3 from nkeys where c3 > 3; 可以进行索引覆盖扫描 index only range scan 调用流程: msyql_select -> JOIN::optimize -> make_join_statistics -> 0. get_quick_record_count -> SQL_SELECT::test_quick_select –> ha_innobase::scan_time -> get_key_scans_params -> check_quick_select –> check_quick_keys -> ha_innobase::records_in_range -> get_index_only_read_time -> ha_innobase::read_time -> get_best_ror_intersect -> get_best_covering_ror_intersect -> a) ha_innobase::scan_time 函数,给出全表扫描 read_time i. scan_time = (double) records / TIME_FOR_COMPARE + 1; 1. mysql 层面,返回一个 record 需要的时间(CPU 时间) 2. TIME_FOR_COMPARE = 5 ii. return (double) (prebuilt->table->stat_clustered_index_size(聚簇索引叶页面数); 1. innodb 层面,全表扫描时间,用读取的 page 数计算(IO 时间) 2. 由于 innodb 是索引组织表,用不到 page 的预读,因此一次读取一个 page iii. table_read_time = ha_innobase::scan_time() + scan_time + 1; 1. 全表扫描总时间 = innodb 读取数据块时间 + mysql 比较记录时间 + 1 2. 测试中:table_read_time = 4.3000000000000007 b) check_quick_select 函数,判断索引扫描的代价 c) ha_innobase::records_in_range 函数,判断给定 range 的索引扫描,将返回多少记 录 i. 给定 range 的 min_key,max_key,根据 min_key,max_key 构造查询条件,分 别进行 btr_cur_search_to_nth_level ii. 传入的 level 是 0,search 到叶页面 iii. 根据返回的两个页面的关系,计算 range 中的数据量 d) get_index_only_read_time 函数,当前 scan 为 index only scan,调用此函数计算 read_time i. cpu_cost = (double) found_records / TIME_FOR_COMPARE; 1. range 中的记录数,除以比较时间(CPU 时间) ii. get_index_only_read_time,mysql 上层提供函数,用于计算 index only scan 的 代价 1. keys_per_block = (table->file->stats.block_size/2)(a) / (table->key_info[keynr].key_length + table->file->ref_length + 1)(b) a) (a) 估计索引页面的利用率为 1/2 b) (b) 索引中,每个索引占用的空间;keynr 为索引的编号,哪个索引? c) 测试中:keys_per_block = 911 2. io_time = (double) (records + keys_per_block - 1) / keys_per_block; a) 需要进行多少次 index 叶页面的 IO (index only scan,不需要回表) b) 测试中:io_time = 1.0021953896816684 (records = 3) 3. index_only_read_time = cpu_cost + io_time + 0.01 = 1.6121953896816683 a) index_only_read_time < table_read_time b) 测试中:index only scan 要好于 table scan,针对第二条语句 c) 对于语句(2),mysql 选择索引覆盖扫描 e) ha_innobase::read_time 函数,非 index only scan 时,调用此函数计算 read_time i. cpu_cost = (double) found_records / TIME_FOR_COMPARE; 1. range 中的记录数,除以比较时间(CPU 时间) ii. ha_innobase::read_time Innodb 层面读取的页面,IO 时间 1. 聚簇索引 a) rows <= 2 时 return rows b) return (ranges + rows)/total_rows * 全表扫描 IO 时间 2. 二级索引 a) return rows2double(ranges+rows) b) IO 时间为 ranges 个数+range 中的行数 = 二级索引定位代价 + 回聚 簇代价(由于无法进行 index only scan) iii. index_read_time = cpu_cost + io_cost + 0.01; 1. 测试中:index_read_time = 4.6099999999999994,大于全表扫描时间 f) 比较所有 e 步骤计算出来的 index_read_time 与 a 步骤计算出来的 table_read_time 之间的大小关系,确定当前 scan 是选择全表扫描,还是索引扫描 i. 对于语句(2),mysql 选择索引覆盖扫描 ii. 因此对于语句 1),mysql 最终选择全表扫描 g) get_best_ror_intersect 函数,是一个优化路径。 i. ROR = Rowid Ordered Retrieval key scan,索引扫描得到的记录,其 rowid 是排 序的,极大降低了回表开销。 1. optimize_keyuse -> a) 针对 join 查询 b) 2. choose_plan -> a) 执行计划选择主函数,读取分析用户定义属性 3. greedy_search -> a) 从 join_tables 中逐个选取最优的表,加入当前已选择的 pplan @code procedure greedy_search input: remaining_tables output: pplan; { pplan = <>; do { (t, a) = best_extension(pplan, remaining_tables); pplan = concat(pplan, (t, a)); remaining_tables = remaining_tables - t; } while (remaining_tables != {}) return pplan; } 4. best_extension_by_limited_search -> a) 从 join_tables 的 remain_tables 中选择一个 table 加入 pplan,目标使得整体 pplan 的开销最小 5. best_access_path -> a) 针对单表,计算单表的全表扫描代价。 6. make_join_readinfo -> pick_table_access_method -> tab->index = find_shortest_key(table, & table->covering_keys) -> tab->read_first_record = join_read_first -> tab->type = JT_NEXT -> a) 对于单表扫描,步骤 0 确定是否可以选择索引。步骤 5 返回全表扫描开销。步骤 6 主要处理 index coverage scan 的部分优化。 b) 在函数 find_shortest_key 中,选择合适的索引,for index coverage scan。 i. 索引必须包含 scan 键值? ii. 索引列的 key_length 最小? iii. 7. 测试四: 测试五: 测试六: 总结:  make_join_statistics 函数中,判断当前查询是否为 unique key 查询,形如(select * from nkeys where c1 = 1;)  若为简单 uk 查询,则直接返回,不需要进行 choose_plan 调用  若不为简单 uk 查询,需要调用 choose_plan 函数,查找最优执行计划  附录: 一: 统计信息宏定义 /* this one is not used */ #define HA_STATUS_POS 1 assuming the table keeps shared actual copy of the 'info' and local, possibly outdated copy, the following flag means that it should not try to get the actual data (locking the shared structure) slightly outdated version will suffice #define HA_STATUS_NO_LOCK 2 /* update the time of the last modification (in handler::update_time) */ #define HA_STATUS_TIME 4 update the 'constant' part of the info: handler::max_data_file_length, max_index_file_length, create_time sortkey, ref_length, block_size, data_file_name, index_file_name. handler::table->s->keys_in_use, keys_for_keyread, rec_per_key #define HA_STATUS_CONST 8 update the 'variable' part of the info: handler::records, deleted, data_file_length, index_file_length, delete_length, check_time, mean_rec_length #define HA_STATUS_VARIABLE 16 get the information about the key that caused last duplicate value error update handler::errkey and handler::dupp_ref see handler::get_dup_key() #define HA_STATUS_ERRKEY 32 update handler::auto_increment_value #define HA_STATUS_AUTO 64 49 测试四十六:Innodb 处理 delete 目的: 测试 innodb 如何处理 delete?如何读取索引属性,将所有索引项删除? create table innodb (id int primary key, comment varchar(50), gmt_create int) engine = innodb; create index idx on innodb (comment); create index idx2 on innodb (gmt_create); insert into innodb values (1, ‘1208’,2); insert into innodb values (2,’1749’,3); 测试语句: delete from innodb where gmt_create = 2; 调用流程: select 阶段:  首先遍历 idx2 索引,找到满足条件的项,由于是 delete 操作,记录需要加锁,X 锁。因 此此时需要回聚簇索引,在聚簇索引上加事务锁,然后返回记录。  由于更新操作必须访问聚簇索引,加事务锁,因此 innodb 对于所有更新操作的 select 阶段,都无法实现 index coverage scan。此方案同样可以被 TNT 采用。 delete 阶段: mysql_delete -> handler::ha_delete_row -> ha_innobase::delete_row -> row_get_prebuilt_update_vector(构造 prebuilt->upd_node) -> row_create_update_node_for_mysql -> row_update_for_mysql -> row_upd_step -> row_upd -> row_upd_clust_step -> row_upd_del_mark_clust_rec(删除主键索引) -> row_upd_store_row -> node->row = row_build(删除主键索引记录之前,读取记录全项,作为 后续删除二级索引时,定位之用) –> 总结:  innodb delete 时,先删除主键索引,然后删除二级索引  删除主键索引时,读取记录全项,作为删除二级索引的定位键值  先删除聚簇索引,后删除二级索引,如何保证正确性?  当前读正确性:innodb 在处理更新操作时,不支持 index coverage scan,也就不会 返回二级索引上存在,但是实际上却被删除的记录。  快照读正确性:快照读仍旧可以用 index coverage scan。因为只要二级索引页面能 够判断出可见性。那么无论该记录对应的聚簇索引已删除,最后都会回滚到当前版 本。  50 测试四十七:Infimum and Supremum 验证 InnoDB index page 中,Infimum 与 Supremum 两个伪列的作用 http://forge.mysql.com/wiki/MySQL_Internals_InnoDB#The_Infimum_and_Supremum_Records http://dev.mysql.com/doc/refman/5.1/en/innodb-record-level-locks.html InnoDB Index Page 上有两个虚拟列:Infimum 与 Supremum。 Supremum 功能我的理解:减少跨页开销。Serializable 隔离级别下,为了防止查询出现幻 象读,需要读取 next rec 加 Next_key lock,而 next rec 若为 Supremum,则刚好减少了一 次读 next page 的代价,减少了 IO。 那么 Infimum 又有什么作用? 大家指点下? 51 测试四十八:事务 & DDL 测试一个事务中间过程有 DDL 语句时,MySQL 与 InnoDB 分别如何处理 准备知识: DDL 操作完成之后,不能够 rollback。 测试语句: begin; select count(*) from a; rename table a to b; select count(*) from b; commit; MySQL 的事务,并不能跨 DDL 存在。MySQL 上层在执行每条语句之前,都会先调用 stmt_causes_implicit_commit 函数判断当前语句是否会造成事务的隐式提交 CF_AUTO_COMMIT_TRANS,DDL 操作都会导致事务的隐式提交。语句执行结束之后,同样 需要调用 stmt_causes_implicit_commit 函数,判断是否再次提交语句对应的事务。这么做的 原因是:DDL 操作如果成功,就不能够回滚,因此不能与用户的事务操作混在一起。 52 测试四十九:SMO insert into a(c2) select c2 from a; 函数流程: row_ins -> row_ins_index_entry_low -> btr_cur_pessimistic_insert -> // 加锁,并且写 undo 日志 btr_cur_ins_lock_and_undo(); lock_rec_insert_check_and_lock(); trx_undo_report_row_operation(&roll_ptr); // 写完 undo 日志,获取的 roll_ptr,可以更新到 insert 的行的系统列中 row_upd_index_entry_sys_field(DATA_ROLL_PTR, roll_ptr); // 申请 n_extents 个 extents 空间,防止分裂过程中出现空间不足的情况 // 实际事情的 extents 数量,与当前索引的层数有关 n_extents = cursor->tree_height / 16 + 3; fsp_reserve_free_extents(n_extents, FSP_NORMAL); btr_page_split_and_insert // 计算 split 操作的 split rec;同时判断是否可以做分裂优化: // 1. 计算记录在当前页面中应该插入的位置 // 2. 读取页面头,判断页面上一次插入是否满足规律(递增/递减/无规律) // 3. 若递增,并且插入在最后,则不移动任何索引记录;否则移动插入记录 // 之后的记录(下一次即可以不移动记录) —— 新页面在右边,同时需要插入 // 新页面的键值到其父节点(InnoDB 索引父节点,记录的是底层页面的 // low key,而非是 Oscar 中记录的 high key,因此此时不需要更新原有页面 // 对应的 low key,保持不变) // 4. 若递减,并且插入在最前,则移动下一条记录到新页面(新页面在左边); // 否则移动当前插入位置的记录与前面所有记录都移动到前面页面。 // 由于 InnoDB 索引记录的是 low key,因此首先需要更新原有页面对应的 // low key,需要更新其 page_no 为新页面的 page_no;原有 low key 对应 // 页面变为新页面。 // 同时需要提取原有页面中的 low key,插入到父页面,作为原页面新的 low key btr_page_get_split_rec_to_right(); btr_attach_half_pages(); // 若当前满足递减插入的条件,则向左分裂,同时将原有页面对应的 low key // 更新为新页面对应的 low key。新页面的 low key,在记录移动之后重新计算。 if (direction == FSP_DOWN) // 获取当前页面的父页面 // 1. 读取当前页面上的最小值 // 2. 根据最小值,进行 search path,到当前页面的上一层页面 // 3. 优化:优于 InnoDB SMO 串行化,因此完全可以在第一次 search path 时 // 记录下从根页面到叶页面的 path,直接读取此 path 即可,不会并发修改 btr_page_get_father_block(); page_rec_get_next(page_get_infimum_rec()); btr_page_get_father_node_ptr_func(); btr_cur_search_to_nth_level(); btr_node_ptr_set_child_page_no(); btr_insert_on_non_leaf_level_func(); page_header_get_ptr(page, PAGE_LAST_INSERT); // 非叶节点的更新操作(SMO),不记录 undo 日志,不能回滚 // InnoDB 的索引页面分裂操作,不支持 ARIES/IM 并发控制与恢复协议, // 索引分裂过程不能失败,否则整个索引树结构就会损坏。索引页面分裂 // 不记录 undo 日志,如果分裂过程中系统 crash,那么系统做完 crash recovery // 之后,索引 B+树结构无法回滚到分裂前的状态。 // 聚簇索引/二级索引均有此问题。 btr_cur_pessimistic_insert(BTR_NO_LOCKING_FLAG | BTR_NO_UNDO_LOG_FLAG); // 分裂之后,如果当前叶页面能够存储记录,则释放 index lock,提高索引并发 if (insert_will_fit && page_is_leaf(page)) mtr_memo_release(mtr, dict_index_get_lock()); InnoDB 索引操作,通过一个 Index Lock 控制并发访问,具体可参考 测试二十二:latch & lock holding latch 章节。 52.1 Page Allocation InnoDB 如何实现 page 的分配,page 分配的算法是怎样的? 函数流程: btr0btr.cc::btr_page_split_and_insert(); btr_page_alloc(); // 如果当前索引为 Insert Buffer 索引 if (dict_index_is_ibuf) // 1. 首先获取 Ibuf 索引的根页面 // 2. 根据根页面中的 PAGE_BTR_IBUF_FREE_LIST 链表,获取一个 free page // 3. 将分配的 free page,从 Ibuf free page 链表中摘除 btr_page_alloc_for_ibuf(); // 当前索引非 Insert Buffer 索引 root = btr_root_get(index); // InnoDB 的索引,区分叶页面与非叶页面;两者使用不同的 segment 管理空间 if (level == 0) seg_header = root + PAGE_HEADER + PAGE_BTR_SEG_LEAF; else seg_header = root + PAGE_HEADER + PAGE_BTR_SEG_TOP; // 分配一个空闲页面。参数意义: // 1. hint_page_no: 新页面最优的位置;向右分裂,则为原页面 page_no 加 1 // 2. direction: 分裂的方向;向右分裂或者向左分裂; fseg_alloc_free_page_general (seg_header, hint_page_no, direction); // 获取当前 tablespace 的 latch,并加上排他锁; latch = fil_space_get_latch(); mtr_x_lock(latch); // 若当前 space 为系统表空间,则顺便将 Ibuf 的空闲页面释放 ibuf_free_excess_pages(); // 实际获取一个空闲页面的函数 // 1. 获取当前 segment 已经使用的页面数量,以及一共占用的页面数量 // 2. 根据指定的 hint_page_no,获取对应的 extent descriptor 结构,详细 // 结构说明可见 fsp0fsp.c: EXTENT DESCRIPTOR。分配算法如下: // 2.1 若指定的 hint_page 为空闲 page(XDES_FREE_BIT),并且 hint_page 对应的 // extent 被分配给当前 segment,则直接选择 hint page // 2.2 若指定的 hint_page 对应的为 free extent,不属于任何 segment,但是 // 当前 segment 使用了至少 FSEG_FRAG_LIMIT 个页面,同时未使用页面 // 数量小于 reserved / FSEG_FILLFACTOR,则将此 free extent 分配给当前 // segment,并且选择 hint page // 2.3 与 2.2 相对,若当前 hint_page 对应的 extent 不为 free extent,但是满足 // 2.2 中的其余条件,则新分配一个 extent,并分配给当前 extent,根据 // 分裂方向,判断最终选择 extent 中最大/最小的 page // 2.4 若 hint_page 对应的 extent 属于当前 segment,同时含有空闲 page,则 // 选择其中的一个空闲 page 即可 // 2.5 若当前 segment 持有的页面数量超过使用的页面数量,仍旧有空闲页面 // 那么则从 segment 对应的 FSEG_NOT_FULL(FSEG_FREE)链表中选择空闲页 // 2.6 若当前 segment 使用的单独页面(不属于任何一个 extent)未超过 // FSEG_FRAG_LIMIT 上限,则分配一个独立页面给当前 segment,选择此页 // 2.7 最后,若前面六个条件均不满足,则分配一个新 extent,并选择其中 // 第一个空闲页面 fseg_alloc_free_page_low(); fseg_n_reserved_pages_low(); // 流程 1 xdes_get_descriptor_with_space_hdr(); // 流程 2 // 若 hint_page_no 指定的 page 超过 tablespace 上限,则直接选择第 1 页 xdes_get_descriptor(); xdes_calc_descriptor_page(); … // 根据选择的 page,新建内存 buffer page;然后初始化 buf_page_create(); fsp_init_file_page(); fseg_mark_page_used(); 52.2 Records Move 函数流程: page0page.cc::page_move_rec_list_start(); page_copy_rec_list_start(); // 1. 插入一条新纪录到页面中,主流程 page_cur_insert_rec_low(); // 1.2 疑问:如何获得插入位置? // 1.4 疑问:why linked list? // 1.6 更新页面中最后一次 insert 对应的信息 // 包括 PAGE_LAST_INSERT,最后插入位置 page_header_set_ptr(page, NULL, PAGE_LAST_INSERT, insert_rec); // 1.8 疑问:何为 page directory?何为 page slot? // 1.9 记录 insert 日志,for redo page_cur_insert_rec_write_log(); // 2. 修改二级索引页面上的 MAX_TRX_ID 属性 page_update_max_trx_id(); // 3. 记录 move 完成之后,对 lock 锁对象进行分裂 lock_move_rec_list_start();
还剩82页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享pdf获得金币 ] 1 人已下载

下载pdf

pdf贡献者

n2m7

贡献于2014-01-02

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf