lxypeter 2019-11-17
它是由C和C++编写的,对于数据库连接还得明确以下两个概念:
InnoDB表数据的组织方式为主键聚簇索引,二级索引(非聚簇索引)中采用的是(索引键值, 主键键值)的组合来唯一确定一条记录。InnoDBl默认为每个索引行添加了4个隐藏的字段:
当我们对记录做了变更操作时就会产生Undo记录,Undo记录默认被记录到系统表空间(ibdata)中,但从5.6开始,也可以使用独立的Undo表空间。Undo记录中存储的是老版本数据,当一个旧的事务需要读取数据时,为了能读取到老版本的数据,需要顺着undo链找到满足其可见性的记录。当版本链很长时,通常可以认为这是个比较耗时的操作。
为了保证事务并发操作时,在写各自的undo log时不产生冲突,InnoDB采用回滚段(Rollback Segment,简称Rseg)的方式来维护undo log的并发写入。回滚段实际上是一种 Undo 文件组织方式,每个回滚段又有多个undo log slot。
结合行结构,如果有两个事务先后修改同一行数据,如下图所示:
例如:数据库中A=1,现在需要update A=3
那么整个步骤如下:
这个就是undo log工作流程,也就是在数据库断电或者crash的时候,在进行恢复的时候,把undo log里面的数据写回到数据库,这样就让数据回滚了。这样实现了事务的原子性,同时保证了数据的一致性。但是,这样每个操作都会进行磁盘IO的写入,频繁的磁盘IO对性能是很大的降低。
引入redo log实现持久性,这个时候就在考虑如果只需要将日志写入磁盘,将数据缓存在内存中,一定时间后再进行更新。
例如:数据库中A=1,B=2,需要update A=3,B=4
整个过程中,数据修改都是在内存中,极大提升磁盘IO速度,而且将redo log提前写入磁盘,这就是Write-Ahead-Logging(WAL)。
如果整个事务执行的过程系统崩溃或者断电了,在系统重启的时候,恢复机制会将redo log中已提交的事务重做,保证事务的持久性;而undo log中未提交的事务进行回滚,保证事务的原子性。
Redo Log由两部分组成,一是内存中的redo log buffer,是易失的。二是redo log file,是持久的。参数innodb_flush_log_at_trx_commit用来控制redo log buffer刷新到redo log file的策略:
0:表示事务提交时不进行写redo log file的操作,这个操作仅在master thread中完成(master thread每隔1秒进行一次fsync操作)。
1:默认值,表示每次事务提交时进行写redo log file的操作。
2:表示事务提交时将redo log写入文件,不过仅写入文件系统的缓存中,不进行fsync操作
与Mysql中Binlog进行比较:
Mysql的原子性由Undo Log机制保证,如果事务失败就用Undo Log进行回滚
隔离性是为了应对多个事务并发进行时的问题,可以分为以下几个隔离级别:
Mysql在实现READ COMMITTED和REPEATABLE READ时用到了MVCC:
Multiversion concurrency control,在每次事务开始时,Mysql会根据当前事务链表(当一个事务开始的时候,会将当前数据库中正在活跃的所有事务--执行begin但是还没有commit的事务--保存到一个叫trx_sys的事务链表中,事务链表中保存的都是未提交的事务,当事务提交之后会从其中删除)创建一个ReadView:
// Friend declaration class MVCC; /** Read view lists the trx ids of those transactions for which a consistent read should not see the modifications to the database. */ ... class ReadView { ... private: // Prevent copying ids_t(const ids_t&); ids_t& operator=(const ids_t&); private: /** Memory for the array */ value_type* m_ptr; /** Number of active elements in the array */ ulint m_size; /** Size of m_ptr in elements */ ulint m_reserved; friend class ReadView; }; public: ReadView(); ~ReadView(); /** Check whether transaction id is valid. @param[in] id transaction id to check @param[in] name table name */ static void check_trx_id_sanity( trx_id_t id, const table_name_t& name); /** Check whether the changes by id are visible. @param[in] id transaction id to check against the view @param[in] name table name @return whether the view sees the modifications of id. */ 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(); return(!std::binary_search(p, p + m_ids.size(), id)); } private: // Disable copying ReadView(const ReadView&); ReadView& operator=(const ReadView&); private: // 事务链表中最大的id /** 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; // 事务链表中最小的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; };
通过该ReadView,新的事务可以根据查询到的每行数据最近的DB_TRX_ID来判断是否对这行数据可见:
在READ COMMITTED事务隔离级别下,每次语句执行都关闭ReadView,然后重新创建一份ReadView。
在REPEATABLE READ事务隔离级别下,事务开始后第一个读操作创建ReadView,一直到事务结束关闭。所以每次看到的数据是一样的
Mysql在REPEATABLE READ事务隔离级别下默认使用MVCC,又叫一致性非锁定读,但是会有幻读的问题:A事务开始之后,B事务插入了一行,本来A事务看不到,但是如果A事务执行了update,并且update的筛选条件里又包含了B插入的那一行,那么这一行数据最近修改的事务就会变成A,这样下次A事务再进行前文所述的可见性判断时就会包含这一行数据。又或者A事务开始后查询表发现没有要插入的数据,然后B事务插入了一条同样的数据,等A事务插入数据时会出现Duplicate entry(如果有unique index),这样就需要显示的使用一致性锁定读:
SELECT … FOR UPDATE (排它锁) SELECT … LOCK IN SHARE MODE (共享锁)
下面先来介绍一下Mysql锁的分类:
Innodb实现了行级锁,但是这是基于索引的,如果一条SQL语句用不到索引的话就会使用表锁。有了意向锁,如果有人尝试对全表进行修改就不需要判断表中的每一行数据是否被加锁了,只需要通过等待意向锁被释放就可以了。意向锁其实不会阻塞全表扫描之外的任何请求,它们的主要目的是为了表示是否有人请求锁定表中的某一行数据。有关Latch的介绍参加关于MySQL latch争用深入分析与判断
InnoDB存储引擎有3种行锁的算法,其分别是:
Mysql在REPEATABLE READ事务隔离级别下默认使用Next-Key Lock(在查询的列是唯一索引(包含主键索引)的情况下,Next-key Lock会降级为Record Lock),所以在使用一致性锁定读时,A事务执行select后,会阻止B事务的插入。
For locking reads (SELECT with FOR UPDATE or LOCK IN SHARE MODE),UPDATE, and DELETE statements, locking depends on whether the statement uses a unique index with a unique search condition, or a range-type search condition. For a unique index with a unique search condition, InnoDB locks only the index record found, not the gap before it. For other search conditions, InnoDB locks the index range scanned, using gap locks or next-key (gap plus index-record) locks to block insertions by other sessions into the gaps covered by the range.
上面说的并发事务隔离都是只有一个事务在修改数据,另外一个事务在读取数据,但是如果两个事务都需要修改数据,且是先查询再更新就会出现丢失更新问题:
要么采用悲观加锁的方式:select quantity from items where id = 1 for update阻止事务B的修改
要么采用乐观锁(CAS:compare and swap):select quantity,version from items where id = 1;a=q-1,v2=v1+1; update items set quantity=a, version=v2 where id = 1 and version = v1;(使用递增version避免ABA问题)
不过最好还是在一条语句中完成原子操作:update items set quantity=quantity-1 where id = 1 and quantity - 1 >= 0;
数据库的一致性分为外部一致性和内部一致性,外部一致性由应用开发人员保证,如将A到B账户转账的多个操作放到一个事务中进行,内部一致性是由原子性和隔离性共同保证的。
Mysql的持久性由Redo Log机制保证,数据在commit之前会持久化到Redo Log中,如果系统crash,可以用它来恢复
谈谈MySQL InnoDB存储引擎事务的ACID特性
谈谈MySQL的锁
详细分析MySQL事务日志(redo log和undo log)
Mysql中的MVCC
Mysql如何保证事务性?
超全面的MySQL语句加锁分析
后面会分析如何利用MyBatis来管理MySQL的连接:
MyBatis连接管理(1)
MyBatis连接管理(2)