1. 事务是什么?

    事务:访问并可能更新数据库中数据的一个程序执行单元。

    在进一步聊事务时有必要先了解下事务的存储引擎,数据库的存储引擎可以通过show engines来查询,这里只谈常用的两种:
    MyISAM:不支持事务,只支持表锁,不支持行锁。
    InnoDB(MySQL默认存储引擎):支持事务,支持表锁和行锁,因此并发情况下性能较MyISAM性能要好。

可以发现InnoDB才支持事务,所以下面主要是基于InnoDB的事务来进一步了解事务中的ACID特性及其实现原理。

2. 事务的ACID特性

    1)原子性(Atomicity):事务中的操作要么全部执行,要么全部失败。一个事务中:张三给李四转账500元。张三的balance-500,李四的balance+500,这两个操作要么同时成功,要么同时失败回滚。
    2)一致性(Consistency):事务开始和结束,数据库的完整性约束没有被破坏。基于上面的例子:张三给李四转账,无论是否成功,两者的余额总和都是一致的。不能是张三转账扣了钱,而李四的余额却没有增加。
    3)隔离性(Isolation):事务内部的操作与其他事务时隔离的,并发执行的各个事务之间互相不影响。并发情况下数据库的读写操作各种问题,这个在下面会单独列一个小点来解释。
    4)持久性(Durability):事务完成后,对数据库的插入、更新、删除操作最终都会保存到磁盘上。一个事务更新A的余额为5000,那么这个事务完成后,这个结果就会持久化到磁盘中。

接着上面基于事务隔离性,并发情况下读写操作会遇到的问题:
    1)写操作对写操作的影响:
        事务的隔离性让多个事务之间写操作互不影响,InnoDB是通过锁机制来实现。比如事务A修改张三的余额balance=5000,事务B修改张三的余额balance=6000,此时事务A先执行,会给张三余额所在的行上锁,事务B想要对该行的数据做修改,只能等到事务A释放锁。
    2)写操作对读操作的影响
        (1)脏读:事务A读取了事务B中未提交的数据,事务B可能会因为某些原因回滚,此时读到的是脏数据。
        (2)不可重复读:事务A先后两次读取同一个数据,两次读取的结果不一样,这种现象称之为不可重复读。
        (3)幻读:事务A按照某个条件先后查询数据库(中间事务B插入一条记录),两次查询结果条数不一样,好像发生了幻觉,称为幻读。

    对于并发下写操作对读操作的影响,MySQL有四种隔离级别来解决脏读、不可重复读、幻读的问题。

3. 事务的隔离级别

    1)读未提交(read uncommitted):可以读到其他事务未提交的数据。
    2)读已提交(read committed):只能读到其他事务已经提交的数据,未提交的事务对于本事务不可见。
    3) 可重复读(repeatable read):多次读取读到的数据一致。
    4) 可串行化(serializable):所有事务串行执行。

    MySQL可以通过下面的语句查看事务的隔离级别,MySQL默认级别是repeatable read.
        select @@global.tx_isolation; //全局隔离级别
        select @@tx_isolation; //本次会话的隔离级别
    设置全局的事务隔离属性:
        set global transaction isolation level read committed; //设置事务的隔离级别

MySQL事务各个隔离级别在并发情况下可能出现脏读、不可重复读、幻读现象的关系表:
 

隔离级别

脏读    

不可重复读

幻读

读未提交

可能

可能

可能

读已提交    

不可能

可能

可能

可重复读

不可能

不可能

可能

可串行化 

不可能

不可能

不可能

4.  事务隔离级别的实现原理

    MySQL中事务的隔离级别主要是靠MVCC(多版本并发控制)和锁实现的。

    1) MVCC(Multi-Version Concurrency Control)
        MySQL中的MVCC会在每一列中额外维护三个字段:
        (1)DB_TRX_ID(6byte):事务ID/事务版本号。每处理一个事务,其值自动加1。
        (2)DB_ROLL_PTR(7byte): 回滚指针。指向写到rollback segment(回滚段)的一条undo log记录。
        (3)DB_ROW_ID(6byte):该值每插入新的一行就加1。

            每一个新的事务开始,事务版本号都会加1,每一个事务的读只会找到小于或等于当前版本号的事务,当前事务会从undo log中找到对象A的历史快照数据,从而实现了非锁一致性读。

    2) 锁的分类
        (1)Exclusive Locks(排它锁/X锁):阻止其他读写操作对上X锁数据加锁,但是对于不加锁的操作不会排斥。
        (2)Shared Locks(共享锁/x-locks):事务A对数据对象B加了S锁,其他事务只能对B加S锁,不能加X锁,直到事务A释放B上的S锁,其他事务才能对B加X锁,保证加了S锁的数据其他事务可以读,但是不可以修改。
        (3)Record Locks(行锁):加在索引行上。通过select * from stu where id = 1 for update可以给id=1的索引行加行锁。
        (4)Gap Locks(间隙锁):锁住两个索引行之间的区域。通过select * from stu where id > 1 and id < 10 for update可以在id为(1,10)的索引区间上加gap锁。
        (5)Next-Key Locks(间隙锁):通过行锁和gap锁形成的闭区间锁。通过select * from stu where id >= 1 and id =< 10 for update可以给id为[1,10]之间的索引区间加next-key lock锁.

    通常SQL的DML操作(insert、update、delete)都是会加排他锁的,普通的查询是不会加锁的,除非显式加锁:
        select ... for update; //加排它锁
        select ... lock in share mode; //加共享锁

5. Innodb中与持久性、一致性相关的日志:

    1)Bin Log:是mysql服务层的日志,用于数据恢复、数据库复制,MySQL的主从复制(读写分离)slave同步master的binlog实现的。
    2)Redo Log:记录了数据操作在物理层面的修改,mysql中使用了大量缓存,修改操作时会直接修改内存,而不是直接写入磁盘,事务进行中时会不断的产生redo log,在事务提交时再通过flush操作保存到磁盘中。当数据库宕机重启,会根据redo log进行数据的恢复,如果redo log中有事务提交,则进行事务提交修改数据。
    3)Undo Log: 当修改数据时除了会记录redo log外,同时会将数据快照版本记录在undo log中,undo log用于数据的撤回操作,通过undo log可以实现事务回滚,并且可以根据undo log回推到某个事务版本的数据,MySQL MVCC正是根据undo log来实现非阻塞读。