mysql本地事务控制
场景:以转账为例
一个正常的转账过程,我们分下面两步执行(左图)
但如果第一步执行成功,第二步执行失败(如右图),最后出现的情况就是A的余额被莫名扣减,所以我们希望当第二步执行失败时,第一步应该被恢复原状。
mysql 的解决方案:
首先,mysql会在每个表中增加3个隐藏字段
-
DB_ROW_ID:一个随机的行号(和当前话题无关)
-
DB_TRX_ID:最近一次操作该行数据的事务id
-
DB_ROLL_PTR:指向上一次被修改之前的数据
void dict_table_add_system_columns(dict_table_t *table, mem_heap_t *heap) {
// ......
dict_mem_table_add_col(table, heap, "DB_ROW_ID", DATA_SYS, DATA_ROW_ID | DATA_NOT_NULL, DATA_ROW_ID_LEN, false, phy_pos, v_added, v_dropped);
dict_mem_table_add_col(table, heap, "DB_TRX_ID", DATA_SYS, DATA_TRX_ID | DATA_NOT_NULL, DATA_TRX_ID_LEN, false, phy_pos, v_added, v_dropped);
if (!table->is_intrinsic()) {
dict_mem_table_add_col(table, heap, "DB_ROLL_PTR", DATA_SYS, DATA_ROLL_PTR | DATA_NOT_NULL, DATA_ROLL_PTR_LEN, false, phy_pos, v_added, v_dropped);
}
}
当数据发生变更时,mysql并不会直接覆盖掉当前记录,而是将当前记录迁移至回滚日志,然后再修改数据,刷新DB_TRX_ID,并将DB_ROLL_PTR指向刚刚被迁移至回滚日志的那条记录。形成一个版本链表。
存在的问题:
一般来说,mysql不会同时只存在一个客户端读写数据,如果两个客户端同时开启事务并按下面的时间顺序执行的话,(右面的事务)查询到的B的余额是不正确的。
解决办法:
最容易想到的办法就是加锁:
但是加锁会使mysql的事务读写变为串行化操作,若其中一个事务执行时间过长,其他事务阻塞等,导致响应时间变慢。所以mysql采用的并不是这种方式。
mysql采取的方式是在每次select执行时生成一个readview,readview会记录下此刻的状态,主要包含的信息:
- m_low_limit_id:当前系统里面已经创建过的事务 ID 的最大值加 1
- m_up_limit_id:所有存活的(没有提交的)事务ID中最小值
- m_creator_trx_id:创建这个readView的事务ID
- m_ids:存活的事务ID,就是在创建readView 没有提交的事务的ID集合
typedef uint64_t ib_id_t; typedef unsigned long int ulint;
typedef ib_id_t trx_id_t;
class ReadView {
// ......
class ids_t {
trx_id_t *data() { return (m_ptr); }
ulint size() const { return (m_size); }
private:
value_type *m_ptr;
ulint m_size;
friend class ReadView;
};
private:
trx_id_t m_low_limit_id; // 当前系统里面已经创建过的事务 ID 的最大值加 1,记为高水位
trx_id_t m_up_limit_id; // 所有存活的(没有提交的)事务ID中最小值,即低水位
trx_id_t m_creator_trx_id; // 创建这个readView的事务ID
ids_t m_ids; // 存活的事务ID,就是在创建readView 没有提交的事务的ID集合
public:
// ......
[[nodiscard]] bool changes_visible(trx_id_t id, const table_name_t &name) const {
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); // 在生成readview的时刻,最后操作该条数据的事务还未开启
} else if (m_ids.empty()) {
return (true);
}
const ids_t::value_type *p = m_ids.data();
return (!std::binary_search(p, p + m_ids.size(), id));
}
// ......
}
根据上述代码我们可以看到,当查询数据时,mysql会将该条数据的DB_TRX_ID字段带入到changes_visible函数中判断该字段是否可见,若不可见,则沿着DB_ROLL_PTR指针向后遍历直到找出可见的数据。