事务:一组逻辑操作单元,使数据从一种状态变换到另一种状态。
事务处理的原则:保证所有事务都作为 一个工作单元 来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交( commit ),那么这些修改就 永久地保存下来;要么数据库管理系统将 放弃 所作的所有 修改 ,整个事务回滚( rollback )到最初状态。
事务的特性
原子性(atomicity): 原子性是指事务是一个不可分割的工作单位,要么全部提交,要么全部失败回滚。
一致性(consistency): (根据定义,一致性是指事务执行前后,数据从一个 合法性状态 变换到另外一个 合法性状态 。这种状态 是 语义上 的而不是语法上的,跟具体的业务有关。 那什么是合法的数据状态呢?满足 预定的约束 的状态就叫做合法的状态。通俗一点,这状态是由你自己 来定义的(比如满足现实世界中的约束)。满足这个状态,数据就是一致的,不满足这个状态,数据就 是不一致的!如果事务中的某个操作失败了,系统就会自动撤销当前正在执行的事务,返回到事务操作 之前的状态。
隔离型(isolation): 事务的隔离性是指一个事务的执行 不能被其他事务干扰 ,即一个事务内部的操作及使用的数据对 并发 的 其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
持久性(durability): 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是 永久性的 ,接下来的其他操作和数据库 故障不应该对其有任何影响。 持久性是通过 事务日志 来保证的。日志包括了 重做日志 和 回滚日志 。当我们通过事务对数据进行修改 的时候,首先会将数据库的变化信息记录到重做日志中,然后再对数据库中对应的行进行修改。这样做 的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执 行,从而使事务具有持久性。
如何使用事务
开启事务
start transation (默认 read write)
或者
begin
START TRANSACTION 语句相较于 BEGIN 特别之处在于,后边能跟随几个 修饰符 :
① READ ONLY :标识当前事务是一个 只读事务 ,也就是属于该事务的数据库操作只能读取数据,而不 能修改数据。
② READ WRITE :标识当前事务是一个 读写事务 ,也就是属于该事务的数据库操作既可以读取数据, 也可以修改数据。
③ WITH CONSISTENT SNAPSHOT :启动一致性读。
提交事务
commit
回滚事务
rollback
隐式事务
MySQL中有一个系统变量 autocommit :
当然,如果我们想关闭这种 自动提交 的功能,可以使用下边两种方法之一:
显式的的使用 START TRANSACTION 或者 BEGIN 语句开启一个事务。这样在本次事务提交或者回 滚前会暂时关闭掉自动提交的功能。
把系统变量 autocommit 的值设置为 OFF
set session autocommit=0
事务的隔离级别
脏写
对于两个事务 Session A、Session B,如果事务Session A 修改了 另一个 未提交 事务Session B 修改过 的数 据,那就意味着发生了 脏写
脏读
对于两个事务 Session A、Session B,Session A 读取 了已经被 Session B 更新 但还 没有被提交 的字段。 之后若 Session B 回滚 ,Session A 读取 的内容就是 临时且无效 的。
不可重复读
对于两个事务Session A、Session B,Session A 读取 了一个字段,然后 Session B 更新 了该字段(B提交了)。 之后 Session A 再次读取 同一个字段, 值就不同 了。那就意味着发生了不可重复读。
幻读
对于两个事务Session A、Session B, Session A 从一个表中 读取 了一个字段, 然后 Session B 在该表中 插 入 了一些新的行(B提交了)。 之后, 如果 Session A 再次读取 同一个表, 就会多出几行。那就意味着发生了幻读。
SQL中的四种隔离级别
严重性
脏写 > 脏读 > 不可重复读 > 幻读
mysql的事务日志
事务的隔离性由 锁机制 实现。
而事务的原子性、一致性和持久性由事务的 redo 日志和undo 日志来保证。
REDO LOG 称为 重做日志 ,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性。
UNDO LOG 称为 回滚日志 ,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。
redo log是记录了实际在什么位置将原本的值更新成了什么,数据库在宕机,重启之后读取redo log将数据库的数据恢复为最新数据
undo log 记录的是事务的相反操作,是逻辑日志,在一个事务中比如执行了一条insert语句,undo log中就记录了一条delete语句,事务回滚的时候,只要执行undo log 就可以
redo log的组成
Redo log可以简单分为以下两个部分:
重做日志的缓冲 (redo log buffer) ,保存在内存中,是易失的。
redo log buffer 大小,默认 16M ,最大值是4096M,最小值为1M。
重做日志文件 (redo log file) ,保存在硬盘中,是持久的。 (默认叫 ib_logfile0 和 ib_logfile1)

第四步:定期将内存中的数据同步到磁盘中
这个过程叫做刷脏页
刷脏页的时机
- redo log写满的时候
- 内存(buffer pool)不足
- mysql空闲的时候
- mysql正常关闭前
undo log
redo log是事务持久性的保证,undo log是事务原子性的保证。在事务中 更新数据 的 前置操作 其实是要 先写入一个 undo log 。 undo log也会产生redo log ,undo log也需要持久化保护
- 作用1:回滚数据
- 作用2:MVCC
锁机制
多个线程如果全是读操作,不需要加锁,全为写操作,可以排队加锁保障隔离性,有读有写,可以通过读写全部加锁,也可以 读操作利用多版本并发控制,写操作进行加锁 。
意向锁
Innodb 支持行级锁和表级锁,为了平衡行级锁和表级锁,Innodb设置了意向锁,当数据表中的某一行有了排他锁,Innodb会自动给行的上一级(表,页)添加意向排他锁,告知其他线程这个表上有排他锁
- 如果相对某些行加上排他锁,就会自动在上一级添加意向排他锁
- 如果相对某些行加上共享锁,就会自动在上一级添加意向共享锁
加读意向锁(s锁)
SELECT column FROM table ... LOCK IN SHARE MODE;
加写意向锁(x锁)
SELECT column FROM table ... FOR UPDATE;
意向锁不会与行级的共享 / 排他锁互斥!正因为如此,意向锁并不会影响到多个事务对不同数据行加排 他锁时的并发性。(不然我们直接用普通的表锁就行了) 
元数据锁
MySQL5.5引入了meta data lock,简称MDL锁,属于表锁范畴。MDL 的作用是,保证读写的正确性。比 如,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更 ,增加了一 列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。 因此,当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加 MDL写 锁。
记录锁
记录锁也就是仅仅把一条记录锁上,官方的类型名称为: LOCK_REC_NOT_GAP 。比如我们把id值为8的 那条记录加一个记录锁的示意图如图所示。仅仅是锁住了id值为8的记录,对周围的数据没有影响。 
- 当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可 以继续获取X型记录锁;
- 当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不 可以继续获取X型记录锁。
间隙锁
MySQL 在 REPEATABLE READ 隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用 MVCC 方 案解决,也可以采用 加锁 方案解决。但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读 取操作时,那些幻影记录尚不存在,我们无法给这些 幻影记录 加上 记录锁 。InnoDB提出了一种称之为 Gap Locks 的锁,官方的类型名称为: LOCK_GAP ,我们可以简称为 gap锁 。 
图中id值为8的记录加了gap锁,意味着 不允许别的事务在id值为8的记录前边的间隙插入新记录 ,其实就是 id列的值(3, 8)这个区间的新记录是不允许立即插入的。比如,有另外一个事务再想插入一条id值为4的新 记录,它定位到该条新记录的下一条记录的id值为8,而这条记录上又有一个gap锁,所以就会阻塞插入 操作,直到拥有这个gap锁的事务提交了之后,id列的值在区间(3, 8)中的新记录才可以被插入。
gap锁的提出仅仅是为了防止插入幻影记录而提出的。
不同于写锁相互之间是互斥的原则,间隙锁之间不是互斥的,如果一个事务A获取到了(5,10)之间的间隙锁,另一个事务B也可以获取到(5,10)之间的间隙锁。这时就可能会发生死锁问题,如下案例。
事务A获取到(5,10)之间的间隙锁不允许其他的DDL操作,在事务提交,间隙锁释放之前,事务B也获取到了间隙锁(5,10),这时两个事务就处于死锁状态
临键锁
既想 锁住某条记录 ,又想 阻止 其他事务在该记录前边的 间隙插入新记录 ,所以InnoDB就提 出了一种称之为 Next-Key Locks 的锁,官方的类型名称为: LOCK_ORDINARY ,我们也可以简称为 next-key锁 。Next-Key Locks是在存储引擎 innodb 、事务级别在 可重复读 的情况下使用的数据库锁, innodb默认的锁就是Next-Key locks。
临键锁同时有间隙锁和记录锁的功能
MVCC
MVCC (Multiversion Concurrency Control),多版本并发控制。顾名思义,MVCC 是通过数据行的多个版 本管理来实现数据库的 并发控制 。这项技术使得在InnoDB的事务隔离级别下执行 一致性读 操作有了保 证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样 在做查询的时候就不用等待另一个事务释放锁。
MVCC 的实现依赖于:隐藏字段、Undo Log、Read View
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理 读-写冲突 ,做到 即使有读写冲突时,也能做到 不加锁 , 非阻塞并发读 ,而这个读指的就是 快照读 , 而非 当前读 。当前 读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。
隐藏字段
对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必 要的隐藏列。
- trx_id :每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的 事务id 赋值给 trx_id 隐藏列
- roll_pointer :每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo日志 中,然 后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

read view
事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照。
记录并维护系统当前活跃事务的ID(没有commit,当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以越新的事务,ID值越大),是系统中当前不应该被本事务看到的其他事务id列表。
这个ReadView中主要包含4个比较重要的内容,分别如下:
1. creator_trx_id ,创建这个 Read View 的事务 ID。
只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为 事务分配事务id,否则在一个只读事务中的事务id值都默认为0。
- trx_ids ,表示在生成ReadView时当前系统中活跃的读写事务的 事务id列表 。
- up_limit_id ,活跃的事务中最小的事务 ID。
- low_limit_id ,表示生成ReadView时系统中应该分配给下一个事务的 id 值。low_limit_id 是系 统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务ID。
low_limit_id并不是trx_ids中的最大值,事务id是递增分配的。比如,现在有id为1, 2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时, trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是4。
ReadView的规则
- 如果被访问版本的trx_id属性值与ReadView中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值小于ReadView中的 up_limit_id 值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值大于或等于ReadView中的 low_limit_id 值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
- 如果被访问版本的trx_id属性值在ReadView的 up_limit_id 和 low_limit_id 之间,那就需要判断一下trx_id属性值是不是在 trx_ids 列表中。
- 如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。
- 如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问
MVCC整体操作流程
- 首先获取事务自己的版本号,也就是事务 ID;
- 获取 ReadView;
- 查询得到的数据,将得到数据的事务d然后与 ReadView 中的事务版本号进行比较;
- 如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
- 最后返回符合规则的数据。
不同隔离级别下的MVCC
读已提交
一个事务中的每一次 SELECT 查询都会重新获取一次 Read View
不可重复读
一个事务中的只在第一次 SELECT 查询都会获取一次 Read View

