MySQL

MySQL服务器逻辑架构图

clipboard.png

它是由C和C++编写的,对于数据库链接还得明确如下两个概念:html

  • ODBC(Open Database Connectivity):是用于链接数据库管理系统的接口协议,与操做系统、编程语言、数据库系统无关,只须要使用该数据库提供的OBJC driver。
  • JDBC(Java DatabBase Connectivity):是由JAVA语言提供的数据库接口API,能够用于链接实现了JDBC driver的数据库,好比:MySQL提供的mysql-connector-java.jar

ACID特性

  • Atomicity - In a transaction involving two or more discrete pieces of information, either all of the pieces are committed or none are.
  • Consistency - A transaction either creates a new and valid state of data, or, if any failure occurs, returns all data to its state before the transaction was started.
  • Isolation - A transaction in process and not yet committed must remain isolated from any other transaction. Each transaction behaves as if it’s the only one that is executing in the system.
  • Durability - Once the system commits a transaction, that transaction must persist in the system, even if the system crashes or loses power

InnoDB存储引擎的行结构

clipboard.png
InnoDB表数据的组织方式为主键聚簇索引,二级索引(非聚簇索引)中采用的是(索引键值, 主键键值)的组合来惟一肯定一条记录。InnoDBl默认为每一个索引行添加了4个隐藏的字段:java

  • DB_ROW_ID:InnoDB引擎中一个表只能有一个主键,用于聚簇索引,若是表没有定义主键会选择第一个非Null的惟一索引做为主键,若是尚未,生成一个隐藏的DB_ROW_ID做为主键构造聚簇索引。
  • DB_TRX_ID:最近更改该行数据的事务ID。
  • DB_ROLL_PTR:undo log的指针,用于记录以前历史数据在undo log中的位置。
  • DELETE BIT:索引删除标志,若是DB删除了一条数据,是优先通知索引将该标志位设置为1,而后经过(purge)清除线程去异步删除真实的数据。

Undo Log

当咱们对记录作了变动操做时就会产生Undo记录,Undo记录默认被记录到系统表空间(ibdata)中,但从5.6开始,也可使用独立的Undo表空间。Undo记录中存储的是老版本数据,当一个旧的事务须要读取数据时,为了能读取到老版本的数据,须要顺着undo链找到知足其可见性的记录。当版本链很长时,一般能够认为这是个比较耗时的操做。
为了保证事务并发操做时,在写各自的undo log时不产生冲突,InnoDB采用回滚段(Rollback Segment,简称Rseg)的方式来维护undo log的并发写入。回滚段其实是一种 Undo 文件组织方式,每一个回滚段又有多个undo log slot。
结合行结构,若是有两个事务前后修改同一行数据,以下图所示:node

clipboard.png

Redo Log

例如:数据库中A=1,如今须要update A=3
那么整个步骤以下:mysql

  1. 事务开始
  2. 记录A=1到undo log
  3. 修改A=3
  4. 将undo log写入磁盘
  5. 将A=3数据写入磁盘
  6. 事务提交

这个就是undo log工做流程,也就是在数据库断电或者crash的时候,在进行恢复的时候,把undo log里面的数据写回到数据库,这样就让数据回滚了。这样实现了事务的原子性,同时保证了数据的一致性。可是,这样每一个操做都会进行磁盘IO的写入,频繁的磁盘IO对性能是很大的下降。算法

引入redo log实现持久性,这个时候就在考虑若是只须要将日志写入磁盘,将数据缓存在内存中,必定时间后再进行更新。
例如:数据库中A=1,B=2,须要update A=3,B=4sql

  1. 事务开始
  2. 记录A=1到undo log
  3. 修改A=3
  4. 记录A=3到redo log
  5. 记录B=2到undo log
  6. 修改B=4
  7. 记录B=4到redo log
  8. 将redo log顺序写入磁盘
  9. 事务提交

整个过程当中,数据修改都是在内存中,极大提高磁盘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进行比较:segmentfault

  • Binary Log是在Mysql server层实现,Redo Log是在Innodb存储引擎中实现
  • Binary Log是记录的SQL逻辑语句,Redo Log是以512字节的block记录的物理格式上的日志,是数据库中每一个页的修改
  • Binary Log只在提交的时候一次性写入,一次提交对应一次记录。redo log文件中同一个事务可能屡次记录,不一样事务之间的不一样版本的记录会穿插写入到redo log文件中,例如可能redo log的记录方式以下:T1-1,T1-2,T2-1,T2-2,T2,T1-3,T1

Atomicity

Mysql的原子性由Undo Log机制保证,若是事务失败就用Undo Log进行回滚缓存

Isolation

隔离性是为了应对多个事务并发进行时的问题,能够分为如下几个隔离级别:

  • READ UNCOMMITTED(读未提交):在此级别里,事务的修改,即便没有提交,对其余事务也都是可见的。事务能够读取未提交的数据,也就是会产生脏读,还有后面提到的不可重复读和幻读问题,在实际应用中通常不多使用。
  • READ COMMITTED(读已提交):大多数数据库系统的默认隔离级别都是它,可是MySQL不是。它可以避免脏读问题,可是在一个事务里对同一条数据的屡次查询可能会获得不一样的结果,也就是会产生不可重复读问题。
  • REPEATABLE READ(重复读):该隔离级别是MySQL默认的隔离级别,看名字就知道它可以防止不可重复读问题,可是在一个事务里对一段数据的屡次读取可能会出现多行或少行的状况,也就是会有幻读的问题。
  • SERIALIZABLE(串行化):该隔离级别是级别最高的,它经过锁来强制事务串行执行,避免了前面说的全部问题。在高并发下,可能致使大量的超时和锁争用问题。实际应用中也不多用到这个隔离级别,由于RR级别解决了全部问题。

Mysql在实现READ COMMITTED和REPEATABLE READ时用到了MVCC:

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来判断是否对这行数据可见:

  • 当检索到的数据的事务ID小于事务链表中的最小值(数据行的DB_TRX_ID < m_up_limit_id)表示这个数据在当前事务开启前就已经被其余事务修改过了,因此是可见的。
  • 当检索到的数据的事务ID表示的是当前事务本身修改的数据(数据行的DB_TRX_ID = m_creator_trx_id) 时,数据可见。
  • 当检索到的数据的事务ID大于事务链表中的最大值(数据行的DB_TRX_ID >= m_low_limit_id) 表示这个数据在当前事务开启后到下一次查询之间又被其余的事务修改过,那么就是不可见的。
  • 若是事务链表为空,那么也是可见的,也就是当前事务开始的时候,没有其余任意一个事务在执行。
  • 当检索到的数据的事务ID在事务链表中的最小值和最大值之间,从m_low_limit_id到m_up_limit_id进行遍历,取出DB_ROLL_PTR指针所指向的回滚段的事务ID,把它赋值给trx_id_current,而后从步骤1从新开始判断,这样总能最后找到一个可用的记录。

在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锁的分类:

clipboard.png

  • 共享锁(读锁):容许事务读一行数据。
  • 排他锁(写锁):容许事务删除或更新一行数据。
  • 意向共享锁(IS):事务想要在得到表中某些记录的共享锁,须要在表上先加意向共享锁。
  • 意向排他锁(IX):事务想要在得到表中某些记录的互斥锁,须要在表上先加意向互斥锁。

Innodb实现了行级锁,可是这是基于索引的,若是一条SQL语句用不到索引的话就会使用表锁。有了意向锁,若是有人尝试对全表进行修改就不须要判断表中的每一行数据是否被加锁了,只须要经过等待意向锁被释放就能够了。意向锁其实不会阻塞全表扫描以外的任何请求,它们的主要目的是为了表示是否有人请求锁定表中的某一行数据。有关Latch的介绍参加关于MySQL latch争用深刻分析与判断
InnoDB存储引擎有3种行锁的算法,其分别是:

  • Record Lock:单个行记录上的锁。
  • Gap Lock:间隙锁,锁定一个范围,但不包含记录自己。
  • Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,而且锁定记录自己。

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.

上面说的并发事务隔离都是只有一个事务在修改数据,另一个事务在读取数据,可是若是两个事务都须要修改数据,且是先查询再更新就会出现丢失更新问题:

clipboard.png

要么采用悲观加锁的方式: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;

Consistency

数据库的一致性分为外部一致性和内部一致性,外部一致性由应用开发人员保证,如将A到B帐户转帐的多个操做放到一个事务中进行,内部一致性是由原子性和隔离性共同保证的。

Durability

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)

相关文章
相关标签/搜索