什么是事务:
事务就是由一组SQL组成,或者一个独立的工作单元。
1.事务ACID
原子性(atomicity):
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。因此事务操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响
一致性(consistency):
一致性是指事务必须使数据库从一个一致性状态转换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
隔离性(isolation):
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
持久性(durability)
持久性是指一个事物一旦被提交了,那么对数据库中数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
2.事务的隔离级别
REPEATABLE READ:这是mysql 默认的隔离级别,能不可重复读,其实在这种隔离级别下,脏读也能避免一部分。一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的
READ COMMITTED:一个事务提交之后,它做的变更才会被其他事务看到
READ UNCOMMITTED:一个事务还没提交时,它做的变更就能被别的事务看到。
SERIALIZABLE:串行化 ,可避免脏读、不可重复读、幻读的发生,后访问的事务必须等前一个事务执行完成,才能继续执行
3.多版本控制(MVCC):
一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,与Postgres在数据行上实现多版本不同,InnoDB是在undolog中实现的,通过undolog可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。
这边主要的两个组成是:
a.readview 全局的事务视图来判断可见性
b.undo日志,用于记录数据的变化
MVCC详解
1.实现原理
MVCC在默认的rr隔离级别下,就是对数据库做了一次快照,但是这个快照是记录的全局的事务ID。这个事务id是全局的,而且是递增的。每行记录也有自己的row_trx_id .
每个事务在启动的时候会,会构造一个事务数组,主要是记录这个开启瞬间的事务活跃情况。ID最小的是低水位,最大的值+1是高水位。这个数据其实就是我们所说的readview。
而每行记录会有2或者3个隐藏的字段,手册描述如下
Records in the clustered index contain fields for all user-defined columns. In addition, there is a 6-byte transaction ID field and a 7-byte roll pointer field.
If no primary key was defined for a table, each clustered index record also contains a 6-byte row ID field
包含一个行的事务ID 和undo的指针,如果是没有主键的表,还会包含row Id。
结构readview 和row_trx_id就可以判断数据对于当前事务的可见性:
如图所示:
1.如果是< m_up_limit_id,则不可见
2.如果>m_low_limit_id,则可见。
3.如果 m_up_limit_id<row_trx_id<m_low_limit_id, 在活跃的数组中,则,不可见,如果不在活跃的数据组中则可见。
其实我的理解,一般的高水位,应该就是本次的事务id+1。
更简单的理解就是,在事务开始前的未提交的不可见。已提交的可见。
2.什么时候创建readview
当然这些讨论都是在RR模式下的。
a. 开启一个事务后,第一个select,会创建的一个readview
b. start transaction with consistent snapshot开始事务时
3.一些源码
mysql-5.7.25\storage\innobase\include\read0types.h readview的类,里面一些方法和变量说明
mysql-5.7.26\storage\innobase\include\row0sel.h
private:
/** The read should not see any transaction with trx id >= this
value. In other words, this is the "high water mark". */
trx_id_t m_low_limit_id;
/** The read should see all trx ids which are strictly
smaller (<) than this value. In other words, this is the
low water mark". */
trx_id_t m_up_limit_id;
/** trx id of creating transaction, set to TRX_ID_MAX for free
views. */
trx_id_t m_creator_trx_id;
/** Set of RW transactions that was active when this snapshot
was taken */
ids_t m_ids;
/** 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 */
trx_id_t m_low_limit_no;
/** AC-NL-RO transaction view that has been "closed". */
bool m_closed;
typedef UT_LIST_NODE_T(ReadView) node_t;
/** List of read views in trx_sys */
byte pad1[64 - sizeof(node_t)];
node_t m_view_list;
};
函数 row_search_mvcc 是读取的入口
if (trx->isolation_level == TRX_ISO_READ_UNCOMMITTED) {
//如果是 READ UNCOMMITTED则什么都不错
/* Do nothing: we let a non-locking SELECT read the
latest version of the record */
} else if (index == clust_index) {
//是聚簇索引的情况,查看每条记录的row_trx_id,判断可见性
/* 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 (srv_force_recovery < 5
&& !lock_clust_rec_cons_read_sees(
rec, index, offsets,
trx_get_read_view(trx))) {
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, need_vrow ? &vrow : NULL,
&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;
prev_rec = rec;
}
} else {
/* We are looking into a non-clustered index,
and to get the right version of the record we
have to look also into the clustered index: this
is necessary, because we can only get the undo
information via the clustered index record. */
//这是非聚簇索引的情况,但是判断可见性也是需要通过主键里面的隐藏字段 行的trx_id 来判断
ut_ad(!dict_index_is_clust(index));
if (!srv_read_only_mode
&& !lock_sec_rec_cons_read_sees(
rec, index, trx->read_view)) {
/* We should look at the clustered index.
However, as this is a non-locking read,
we can skip the clustered index lookup if
the condition does not match the secondary
index entry. */
//拿到记录会先调用lock_sec_rec_cons_read_sees判断page上记录的最近一次修改trx id是否小于m_up_limit_id,
//如果小于即该page上数据可见,否则即调用row_search_idx_cond_check检查可见性,只对匹配进行回表,读取row trx_id,之后的判断一样
switch (row_search_idx_cond_check(
buf, prebuilt, rec, offsets)) {
case ICP_NO_MATCH:
goto next_rec;
case ICP_OUT_OF_RANGE:
err = DB_RECORD_NOT_FOUND;
goto ;
case ICP_MATCH:
goto requires_clust_rec;
}
ut_error;
}
}
srv_force_recovery 这个也很关键:
可以在storage\innobase\include\srv0srv.h
trx_get_read_view:是创建活跃事务的readview
找到对应说明:
/** Alternatives for srv_force_recovery. Non-zero values are intended
to help the user get a damaged database up so that he can dump intact
tables and rows with SELECT INTO OUTFILE. The database must not otherwise
be used with these options! A bigger number below means that all precautions
of lower numbers are included. */
enum {
SRV_FORCE_IGNORE_CORRUPT = 1, /*!< let the server run even if it
detects a corrupt page */
SRV_FORCE_NO_BACKGROUND = 2, /*!< prevent the main thread from
running: if a crash would occur
in purge, this prevents it */
SRV_FORCE_NO_TRX_UNDO = 3, /*!< do not run trx rollback after
recovery */
SRV_FORCE_NO_IBUF_MERGE = 4, /*!< prevent also ibuf operations:
if they would cause a crash, better
not do them */
SRV_FORCE_NO_UNDO_LOG_SCAN = 5, /*!< do not look at undo logs when
starting the database: InnoDB will
treat even incomplete transactions
as committed */
SRV_FORCE_NO_LOG_REDO = 6 /*!< do not do the log roll-forward
in connection with recovery */
};
一些说明:
这边小于5的意思 其实就是 innodb_force_recovery 参数,默认是0:执行所有的恢复
innodb_force_recovery可以设置为1-6,大的数字包含前面所有数字的影响。
当设置参数值大于0后,可以对表进行select,create,drop操作,但insert,update或者delete这类操作是不允许的。
1(SRV_FORCE_IGNORE_CORRUPT):忽略检查到的corrupt页
2(SRV_FORCE_NO_BACKGROUND):阻止主线程的运行,如主线程需要执行full purge操作,会导致crash
3(SRV_FORCE_NO_TRX_UNDO):不执行事务回滚操作。
4(SRV_FORCE_NO_IBUF_MERGE):不执行插入缓冲的合并操作。
5(SRV_FORCE_NO_UNDO_LOG_SCAN):不查看重做日志,InnoDB存储引擎会将未提交的事务视为已提交。
6(SRV_FORCE_NO_LOG_REDO):不执行前滚的操作。
对应的判断可见性的函数:
/*********************************************************************//**
Checks that a record is seen in a consistent read.
@return true if sees, or false if an earlier version of the record
should be retrieved */
bool
lock_clust_rec_cons_read_sees(
/*==========================*/
const 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) */
ReadView* view) /*!< in: consistent read view */
{
ut_ad(dict_index_is_clust(index));
ut_ad(page_rec_is_user_rec(rec));
ut_ad(rec_offs_validate(rec, index, offsets));
/* Temp-tables are not shared across connections and multiple
transactions from different connections cannot simultaneously
operate on same temp-table and so read of temp-table is
always consistent read. */
//临时表总是一致的。
if (srv_read_only_mode || dict_table_is_temporary(index->table)) {
ut_ad(view == 0 || dict_table_is_temporary(index->table));
return(true);
}
/* NOTE that we call this function while holding the search
system latch. */
//row_get_rec_trx_id获取行隐含列的row_trx_id,有兴趣可以看看这个怎么获得的。
trx_id_t trx_id = row_get_rec_trx_id(rec, index, offsets);
//真正判断可见性的是changes_visible,就是比较trx_id 和当前readview 数组
return(view->changes_visible(trx_id, index->table->name));
}
对应的changes_visible对应的是在 readview class里面:
bool changes_visible(
trx_id_t id,
const table_name_t& name) const
MY_ATTRIBUTE((warn_unused_result))
{
ut_ad(id > 0);
if (id < m_up_limit_id || id == m_creator_trx_id) {
//小于低水位或者是当前的事务,可见
return(true);
}
//检查看事务是否有效,但是这里的作用是什么不太明白
check_trx_id_sanity(id, name);
//如果大于高水位的不可见。
if (id >= m_low_limit_id) {
return(false);
} else if (m_ids.empty()) {
//其他情况,活跃数组中空,都可见
return(true);
}
const ids_t::value_type* p = m_ids.data();
//这一步应该判断在高水位和低水位中间,且m_ids存在活跃事务的情况,是否在行的trx_id是否在活跃事务数组中。如果在
//返回可见,不在则不可见
return(!std::binary_search(p, p + m_ids.size(), id));
}
readview的重要的结构:
private:
/** The read should not see any transaction with trx id >= this
value. In other words, this is the "high water mark". */
//高水位
trx_id_t m_low_limit_id;
/** The read should see all trx ids which are strictly
smaller (<) than this value. In other words, this is the
low water mark". */
//低水位
trx_id_t m_up_limit_id;
/** trx id of creating transaction, set to TRX_ID_MAX for free
views. */
//创建当前事务的 trx_id
trx_id_t m_creator_trx_id;
/** Set of RW transactions that was active when this snapshot
was taken */
//活跃事务id 列表
ids_t m_ids;
/** 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 */
trx_id_t m_low_limit_no;
/** AC-NL-RO transaction view that has been "closed". */
bool m_closed;
typedef UT_LIST_NODE_T(ReadView) node_t;
/** List of read views in trx_sys */
byte pad1[64 - sizeof(node_t)];
node_t m_view_list;
};
参考
https://www.cnblogs.com/zengkefu/p/5678361.html
http://mysql.taobao.org/monthly/2018/11/04/