事务隔离等级及InnoDB实现简单总结

一、数据库中事务的隔离等级 这里首先要明确的是,这里的“隔离”都是在“事务”的基础上讨论的,通常的事务通过 start transaction开启,之后通过rollback或者commit来结束。由于大部分情况下对于mysql的操作都是单条语句的操作,我想大部分人在操作mysql的时候不是在操作测试数据就是在查询日志,除了一些专门做业务的开发来说,很少有人来关注事务方面的内容。对于那些对事务相对不是很熟悉的同学来说,通常应该对于系统常见的执行单位进程、线程、协程等应该是有些基础了解的。其实这个东西可以大致的类比为和这个像类似的概念,它们都涉及到了两个重要的基础概念:并发、隔离。对于数据库的隔离等级来说,是指当多个事务并发送执行的情况下不同事务对于数据库的修改对于其它数据库的影响。 下面是wikipedia对于事务隔离等级的说明

Isolation Levels, Read Phenomena and Locks[edit]

Isolation Levels vs Read Phenomena[edit]

Isolation level Dirty reads Non-repeatable reads Phantoms
Read Uncommitted may occur may occur may occur
Read Committed - may occur may occur
Repeatable Read - - may occur
Serializable - - -

由于wiki的说明已经非常详细了,所以这里只是做个简单的注释。 "Dirty reads":     由于基于MVCC机制的数据库都是实时修改数据库中的记录,同时加上了版本控制(也就是MVCC中version中的意义),从而可以让不同的事务看到各自特有的一个版本信息。作为对比其实可以看到,myisam引擎就没有这种问题,因为myisam并没有使用MVCC功能,而是在操作的时候直接使用了锁表功能,这个锁定粒度大,所以并发性有较大限制。由于每个记录都是实时写入的,所以如果不加任何限制,那么一个事务修改之后、事务提交之前的内容其实是可以被其它事务看到的,所以这里看到的就可以认为是一个脏数据,因为这个事务还没有提交,所以这个数据并不是最终结果。 "Non-repeatable reads":     和前一个对比,当一个事务提交之后,事务提交的修改都完整一致的落入数据库。此时如果允许读取已提交的内容,那么此时两次相同的select语句(可能包涵sum、avg等其它聚合操作)在另一个事务中可能有不同的结构。当然,对于MVCC来说,它实现的代价并不比避免"Dirty reads"的代价更高,所有这个也是INNODB的默认事务隔离等级。 "Phantoms":     前两个限制的其实都是每个记录本身数据的内容,也就是对于某个已存在记录本身数值修改的影响,而这里的“幻读”则是对于查询结果集的一个限制。或者说前面还都是“各扫门前雪”的模式,而这个幻读的要求就是一个联防甚至连坐的概念。这个phantom单词比较生僻,它的本意是指幽灵之类的“怪力乱神”之类的东西。就好像你照了几张照片,回去看的时候,发现里面突然多了一个鬼脸,所以你吓了一身冷汗,当时喊了一声“有妖气”。其实这里的意思就是说“幻读”这个翻译本身和很多专业术语翻译一样,有些莫名其妙,比较接地气的翻译可能是“神出鬼没读”、“幽灵读”之类的翻译,当然这个翻译感觉更low了。为了给大家一个直观的印象,就好像这个图片里圆圈里的现象一样(图片来源): 事务隔离等级及InnoDB实现简单总结   二、InnoDB事务在读取记录时的判断 mysql-5.1.61\storage\innobase\row\row0sel.c row_search_for_mysql /* We are ready to look at a possible new index entry in the result set: the cursor is now placed on a user record */   if (prebuilt->select_lock_type != LOCK_NONE) { …… } else { /* This is a non-locking consistent read: if necessary, fetch a previous version of the record */   if (trx->isolation_level == TRX_ISO_READ_UNCOMMITTED) {   /* Do nothing: we let a non-locking SELECT read the latest version of the record */   } else if (index == clust_index) {   /* Fetch a previous version of the row if the current one is not visible in the snapshot; if we have a very high force recovery level set, we try to avoid crashes by skipping this lookup */   if (UNIV_LIKELY(srv_force_recovery < 5)     && !lock_clust_rec_cons_read_sees(     rec, index, offsets, trx->read_view)) {   rec_t* old_vers; /* The following call returns 'offsets' associated with 'old_vers' */ err = row_sel_build_prev_vers_for_mysql( trx->read_view, clust_index, prebuilt, rec, &offsets, &heap, &old_vers, &mtr);   if (err != DB_SUCCESS) {   goto lock_wait_or_error; }   if (old_vers == NULL) { /* The row did not exist yet in the read view */   goto next_rec; }   rec = old_vers; } } 当对DB遍历读取到一条记录的时候,通过lock_clust_rec_cons_read_sees(rec, index, offsets, trx->read_view)判断当前事务是否可以看到当前(访问)记录,这个函数比较关键的就是trx->read_view(当然记录本身rec也很重要)。 三、记录对事务可见性的判断 lock_clust_rec_cons_read_sees===>>>   /************************************************************************* Checks that a record is seen in a consistent read. */   ibool lock_clust_rec_cons_read_sees( /*==========================*/ /* out: TRUE if sees, or FALSE if an earlier version of the record should be retrieved */ rec_t* rec, /* in: user record which should be read or passed over by a read cursor */ dict_index_t* index, /* in: clustered index */ const ulint* offsets,/* in: rec_get_offsets(rec, index) */ read_view_t* view) /* in: consistent read view */ { …… /* NOTE that we call this function while holding the search system latch. To obey the latching order we must NOT reserve the kernel mutex here! */   trx_id = row_get_rec_trx_id(rec, index, offsets);   return(read_view_sees_trx_id(view, trx_id)); } 其中row_get_rec_trx_id函数的功能非常简单,就是读取一个记录中的trxid字段,这个字段是InnoDB中所有记录都存在的一个系统(控制)字段,这个字段虽然对于用户不可见的,但是它本身对于事务可见性的实现非常重要,另外也是隐式锁(implicit lock)的实现基础。这个字段表示的就是最后一个修改该记录的事务ID,而事务ID是可以保证在任意时间空间唯一的,即使重启之后新分配的事务ID也不会和之前的事务ID重复,所以这个事务ID本身也可以认为是一个记录的版本号信息,这个版本号直观的理解就是SVN的版本号,而这个版本号也就是InnoDB MVCC的实现基础。 lock_clust_rec_cons_read_sees函数其实就是判断当前事务的readview是否可见这个记录, ===>>>lock_clust_rec_cons_read_sees===>>>read_view_sees_trx_id===>>> mysql-5.1.61\storage\innobase\include\read0read.ic /************************************************************************* Checks if a read view sees the specified transaction. */ UNIV_INLINE ibool read_view_sees_trx_id( /*==================*/ /* out: TRUE if sees */ read_view_t* view, /* in: read view */ dulint trx_id) /* in: trx id */ { ulint n_ids; int cmp; ulint i;   if (ut_dulint_cmp(trx_id, view->up_limit_id) < 0) {   return(TRUE); }   if (ut_dulint_cmp(trx_id, view->low_limit_id) >= 0) {   return(FALSE); }   /* We go through the trx ids in the array smallest first: this order may save CPU time, because if there was a very long running transaction in the trx id array, its trx id is looked at first, and the first two comparisons may well decide the visibility of trx_id. */   n_ids = view->n_trx_ids;   for (i = 0; i < n_ids; i++) {   cmp = ut_dulint_cmp( trx_id, read_view_get_nth_trx_id(view, n_ids - i - 1)); if (cmp <= 0) { return(cmp < 0); } }   return(TRUE); } 四、事务readview的创建 mysql-5.1.61\storage\innobase\read\read0read.c   /************************************************************************* Opens a read view where exactly the transactions serialized before this point in time are seen in the view. */   read_view_t* read_view_open_now( /*===============*/ /* out, own: read view struct */ dulint cr_trx_id, /* in: trx_id of creating transaction, or (0, 0) used in purge */ mem_heap_t* heap) /* in: memory heap from which allocated */ { read_view_t* view; trx_t* trx; ulint n;   ut_ad(mutex_own(&kernel_mutex));   view = read_view_create_low(UT_LIST_GET_LEN(trx_sys->trx_list), heap);   view->creator_trx_id = cr_trx_id; view->type = VIEW_NORMAL; view->undo_no = ut_dulint_create(0, 0);   /* No future transactions should be visible in the view */   view->low_limit_no = trx_sys->max_trx_id; view->low_limit_id = view->low_limit_no;   n = 0; trx = UT_LIST_GET_FIRST(trx_sys->trx_list);   /* No active transaction should be visible, except cr_trx */   while (trx) { if (ut_dulint_cmp(trx->id, cr_trx_id) != 0     && (trx->conc_state == TRX_ACTIVE || trx->conc_state == TRX_PREPARED)) {   read_view_set_nth_trx_id(view, n, trx->id);   n++;   /* NOTE that a transaction whose trx number is < trx_sys->max_trx_id can still be active, if it is in the middle of its commit! Note that when a transaction starts, we initialize trx->no to ut_dulint_max. */   if (ut_dulint_cmp(view->low_limit_no, trx->no) > 0) {   view->low_limit_no = trx->no; } }   trx = UT_LIST_GET_NEXT(trx_list, trx); }   view->n_trx_ids = n;   if (n > 0) { /* The last active transaction has the smallest id: */ view->up_limit_id = read_view_get_nth_trx_id(view, n - 1); } else { view->up_limit_id = view->low_limit_id; }     UT_LIST_ADD_FIRST(view_list, trx_sys->view_list, view);   return(view); } 这个地方是遍历系统所有(除了自己)活跃/准备事务,对于这些事务来说,这个readview是不可见的,因为它们还没有提交完成,所以等待它们提交之后,它们修改的数据对于readview不可见。明显地,对于大于trx_sys->max_trx_id(存储在view->low_limit_id中)的事务,readview均不可见(由于这些事务在该readview之后创建);另外,比所有活跃事务中最小事务ID(存储在view->up_limit_id中)还要小的事务肯定是已经提交完成了,所以肯定是可见的。对于在两者之间的事务,除了这里遍历的事务之外都已经提交,所以也是可见的;或者说view->low_limit_id和view->up_limit_id之间,除了这里遍历的事务之外都是可见的。这里其实也就是read_view_sees_trx_id中的逻辑流程。 举个栗子:假设系统中最大事务ID为10,当前活跃事务ID为 2、4、6,那么此时说明事务ID小于10的事务除了2、4、6之外都已经提交完成(所以都是对readview可见),反之,对于事务ID大于等于10的事务readview不可见。对于这个判定方法如何表示呢?这里使用了一个区间加上一个枚举集合的表示方法,其中的区间就是2和10,而枚举的内容就是{2、4、6},判断方法就是trxid小于2的都可见,trxid大于10的都不可见,trxid在这个区间内的只要不和枚举集合相等就可见。 五、可见记录内容的回滚 row_sel_build_prev_vers_for_mysql===>>>row_vers_build_for_consistent_read===>>>trx_undo_prev_version_build roll_ptr = row_get_rec_roll_ptr(rec, index, offsets); old_roll_ptr = roll_ptr;   *old_vers = NULL;   if (trx_undo_roll_ptr_is_insert(roll_ptr)) {   /* The record rec is the first inserted version */   return(DB_SUCCESS); }   rec_trx_id = row_get_rec_trx_id(rec, index, offsets);   err = trx_undo_get_undo_rec(roll_ptr, rec_trx_id, &undo_rec, heap);   if (err != DB_SUCCESS) {   return(err); }   ptr = trx_undo_rec_get_pars(undo_rec, &type, &cmpl_info,     &dummy_extern, &undo_no, &table_id);   ptr = trx_undo_update_rec_get_sys_cols(ptr, &trx_id, &roll_ptr,        &info_bits); ptr = trx_undo_rec_skip_row_ref(ptr, index);   ptr = trx_undo_update_rec_get_update(ptr, index, type, trx_id,      roll_ptr, info_bits,      NULL, heap, &update); 这里的实现同样是利用了记录中对于用户不可见的ROLLPTR字段,这个字段记录了对于这个记录的修改历史,同样和svn类比,就是一个记录的修改历史,或者更学术化的说法就是"操作子",有了这些信息就可以逐步回滚该记录,回滚出的记录中同样包涵了前面说到的最后一个修改事务的trxid,这样问题就转变成了一个循环问题。大致如此。 六、简单例子 1、测试数据 创建一个非常简单的table,里面只有一行数据 mysql> insert tsecer values(1,1); Query OK, 1 row affected (0.00 sec)   mysql> select * from tsecer; +------+------+ | k    | v    | +------+------+ |    1 |    1 | +------+------+ 1 row in set (0.00 sec)   mysql>  2、三个终端 前两个分别设置为READ UNCOMMITTED和READ COMMITTED,最后一个在事务中更新字段,可以看到事务隔离等级为READ UNCOMMITTED马上看到了修改之后的数值,而READ COMMITTED看到的依然是事务开始前的数值。   终端1: mysql> start transaction; Query OK, 0 rows affected (0.00 sec)   mysql> update tsecer set v=2 where k=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1  Changed: 1  Warnings: 0   mysql>  终端2: mysql> set session transaction isolation level READ UNCOMMITTED; Query OK, 0 rows affected (0.00 sec)   mysql> start transaction; Query OK, 0 rows affected (0.00 sec)   mysql> select * from tsecer;                                     +------+------+ | k    | v    | +------+------+ |    1 |    2 | +------+------+ 1 row in set (0.00 sec)   mysql>  终端3: mysql> set session transaction isolation level READ COMMITTED; Query OK, 0 rows affected (0.00 sec)   mysql> start transaction; Query OK, 0 rows affected (0.00 sec)   mysql> select * from tsecer; +------+------+ | k    | v    | +------+------+ |    1 |    1 | +------+------+ 1 row in set (0.00 sec) 3、InnoDB的默认事务隔离级别 mysql> show session variables like 'tx_isolation'; +---------------+----------------+ | Variable_name | Value          | +---------------+----------------+ | tx_isolation  | READ-COMMITTED | +---------------+----------------+ 1 row in set (0.00 sec)   mysql> show global variables like 'tx_isolation';  +---------------+-----------------+ | Variable_name | Value           | +---------------+-----------------+ | tx_isolation  | REPEATABLE-READ | +---------------+-----------------+ 1 row in set (0.00 sec)   mysql>  从这个地方看,InnoDB的默认隔离级别是REPEATABLE-READ。 mysql-5.1.61\sql\mysqld.cc static int mysql_init_variables(void) { ……   global_system_variables.table_plugin= NULL;   global_system_variables.tx_isolation= ISO_REPEATABLE_READ;   global_system_variables.select_limit= (ulonglong) HA_POS_ERROR; …… } 也就是说,这个ISO_REPEATABLE_READ是系统默认的事务隔离级别。
上一篇:bzoj1052-HAOI2007 覆盖问题


下一篇:从Btree的一个小特性看innodb的页面分裂