MySQL数据库上层加锁逻辑

十二月 5, 2011 by · 1 Comment 

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) ->

总结:

  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

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量,无用的字段,尽量不放在索引中

       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

    参考文档:

    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处理最多20undo log pages(purge_sys->n_pages_handled +20) ->

 

总结:

1.         20s调用一次purge操作,主线程调用

2.         每次purge扫描最多20undo 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中的事务idroll_ptr,以及更新过的字段) -> trx_undo_rec_get_partial_row(对于辅助索引,undo记录了辅助索引包含的所有字段,需要解析出来,用于purge辅助索引)

 

总结:

1.         undo日志量大。为了保证辅助索引上的过期记录的purgeInnoDB会在undo中记录所有的辅助索引涉及的字段,哪怕此字段并未被update;而对于delete操作,undo中同样会记录所有辅助索引涉及的字段。

2.         purge操作性能较差。InnoDBpurge操作,是一个复杂的过程,包括各索引记录的构造,根据构造的索引记录做unique scan(辅助索引上),定位到记录之后,还需要到clust_index中判断辅助索引项是否可以被purge(可能会涉及到clust_index上的记录undo)

建议:

1.         考虑到InnoDB的索引覆盖扫描实现的较差,为了减少undo量,无用的字段,尽量不放在索引中

MySQL数据库InnoDB存储引擎源码解读系列技术文章的作者:何登成

返回导读目录页:MySQL数据库InnoDB存储引擎源代码调试跟踪分析

原创文章,转载请注明: 文章地址MySQL数据库上层加锁逻辑

本文标题:MySQL数据库上层加锁逻辑
本文链接:http://www.mysqlops.com/2011/12/05/mysql-logic.html
订阅本站:http://feed.mysqlops.com   转载请注明来源,如果喜欢本站可以Feed 订阅本站。

About admin

Comments

One Response to “MySQL数据库上层加锁逻辑”

Trackbacks

Check out what others are saying about this post...
  1. [...] 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 [...]



Speak Your Mind

Tell us what you're thinking...
and oh, if you want a pic to show with your comment, go get a gravatar!

知识共享许可协议
作品采用知识共享署名 2.5 中国大陆许可协议进行许可。