MySQL数据库上层加锁逻辑
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) ->
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)。
Insert on duplicate update
测试InnoDB处理insert on duplicate update命令时的流程。冲突发生时,是否在insert函数中完成update操作?
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 ->
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) ->
总结:
- 20s调用一次purge操作,主线程调用
- 每次purge扫描最多20个undo pages
- 从undo record构造查询条件,然后按照二级索引,聚簇索引的顺序,purge满足查询条件的过期记录
- purge记录的过程中,必须保证drop table不能够操作
- 根据参考文档,InnoDB引擎在后续版本中对purge进行了优化,将purge操作从主线程中解放出来,同时可以开始多个purge线程,提高purge的效率
参考文档:
http://blogs.InnoDB.com/wp/2011/04/mysql-5-6-multi-threaded-purge/ –Mysql 5.6: multi threaded purge
purge测试续
目的:
在purge测试的基础上,做进一步的测试,问题是:
- 如果update操作仅仅修改了一个二级索引的部分字段,那么purge的时候,如何通过这部分字段,来定位需要purge的记录?
- 对于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辅助索引)
总结:
- undo日志量大。为了保证辅助索引上的过期记录的purge,InnoDB会在undo中记录所有的辅助索引涉及的字段,哪怕此字段并未被update;而对于delete操作,undo中同样会记录所有辅助索引涉及的字段。
- purge操作性能较差。InnoDB的purge操作,是一个复杂的过程,包括各索引记录的构造,根据构造的索引记录做unique scan(辅助索引上),定位到记录之后,还需要到clust_index中判断辅助索引项是否可以被purge(可能会涉及到clust_index上的记录undo)
建议:
- 考虑到InnoDB的索引覆盖扫描实现的较差,为了减少undo量,无用的字段,尽量不放在索引中
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 ->
总结:
- 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
参考文档:
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
1 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
2 测试二十六(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量,无用的字段,尽量不放在索引中
MySQL数据库InnoDB存储引擎源码解读系列技术文章的作者:何登成
返回导读目录页:MySQL数据库InnoDB存储引擎源代码调试跟踪分析
原创文章,转载请注明: 文章地址MySQL数据库上层加锁逻辑

Comments
One Response to “MySQL数据库上层加锁逻辑”Trackbacks
Check out what others are saying about this post...[...] 15 测试十四:insert ignore测试… 20 16 测试十五:auto_increment. 21 17 测试十六:数据格式转换… 23 18 测试十七:innodb加载表数据字典… 23 19 测试十八:scan测试… 24 20 测试十九:加锁等待… 26 21 测试二十:mysql定位table. 27 22 测试二十一:如何做join.. 28 23 测试二十二:latch & lock holding latch.. 28 24 测试二十三:MySQL上层加锁逻辑… 29 [...]