事务定义

一个事务会涉及大量的cpu操作和IO操作,这些操作会被打包成一个执行单元,要么同时完成,要么同时都不完成。
事务是一组原子性的sql命令或者说是一个独立的工作单元,如果其中任何一条sql语句因为崩溃或者其他原因执行失败,那么该组所有的sql语句都不会执行。如果没有显示启动事务,数据库会根据autocommit的值,默认每条sql操作都会自动提交。

事务的特性

原子性(A)

一个事务中的所有操作,要么都完成,要么都不执行,对于一个事务来说,不可能只执行其中的一部分。

一致性(C)

数据库总是从一个一致性的状态转换到另一个一致性的状态

隔离性(I)

一个事务所做的修改在最终提交之前,对其他事务是不可见的。多个事务之间的操作相互不影响。每降低一个事务的隔离级别都能提高数据库的并发,但同时不安全性就增加了。关于事务的隔离级别后续还要详细讨论。这里简单介绍一下:

  • 读未提交:其他事务未提交就可以读。
  • 读已提交:其他事务只有提交了才能读。
  • 可重复读:只管自己启动事务时候的状态,不接受其他事务的影响。
  • 串行化:按照顺序提交事务保证了数据的安全性,但无法实现并发。

    持久性(D)

    事务一旦提交,就要更新到数据库中,不能回滚。就算服务器宕机,仍然需要在下次启动的时候自动执行事务中的sql命令,体现到数据库中。

数据库事务隔离界别

介绍

脏读:一个事务在处理过程中读取了另外一个事务未提交的数据
不可重复读:是指在一个事务内,多次读同一数据,却得到不同的结果 (被其它事务修改过)
幻读:是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。

隔离界别

隔离界别 脏读 不可重复读 幻读
未提交读(Read uncommited) 可能 可能 可能
已提交读(Read commited) 不可能 可能 可能
可重复读(Repeatable read) 不可能 不可能 可能
串行化(Serializable) 不可能 不可能 不可能
  • 未提交读(Read uncommited):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据。
  • 已提交读(Read commited):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别。
  • 可重复读(Repeatable read):可重复读。在同一事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,改级别消除了不可重复读,但还是存在幻象读,但InnoDB解决了幻读。
  • 串行化:完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞

MVCC

概念

MVCC(Multi-Version Concurrency Control)是一种多版本并发控制机制。
与隔离级别紧密联系的另外一个东西是并发调度,通过并发调度实现隔离级别。对于并发调度,不同的数据库厂商有不同的实现机制,但基本原理类似,都是通过加锁来保护数据对象不同时被多个事务修改。
多版本的并发控制(MVCC)相对于传统的基于锁的并发控制主要特点是读不上锁,这种特性对于读多写少的场景,大大提高了系统的并发度,因此大部分关系型数据库都实现了MVCC。
锁机制可以控制并发操作,但是其系统开销较大,而MVCC可以在大多数情况下代替行级锁,使用MVCC,能降低其系统开销。
MVCC是通过保存数据在某个时间点的快照来实现的. 不同存储引擎的MVCC. 不同存储引擎的MVCC实现是不同的,典型的有乐观并发控制和悲观并发控制

实现

InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列,基于ReadView版本链以及Undo日志实现的

  • trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。
  • roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息


INSERT

InnoDB为新插入的每一行保存当前系统版本号作为版本号.

SELECT

InnoDB会根据以下两个条件检查每行记录:
a.InnoDB只会查找版本早于当前事务版本的数据行
(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的.
b.行的删除版本要么未定义,要么大于当前事务版本号
这可以确保事务读取到的行,在事务开始之前未被删除.
只有a,b同时满足的记录,才能返回作为查询结果.

DELETE

InnoDB会为删除的每一行保存当前系统的版本号(事务的ID)作为删除标识.

UPDATE

InnoDB执行UPDATE,实际上是新插入了一行记录,并保存其创建时间为当前事务的ID,同时保存当前事务ID到要UPDATE的行的删除时间.
image.png

如何避免长事务对业务的影响

首先,从应用开发端来看:

  1. 确认是否使用了set autocommit=0。这个确认工作可以在测试环境中开展,把MySQL的general_log开起来,然后随便跑一个业务逻辑,通过general_log的日志来确认。一般框架如果会设置这个值,也就会提供参数来控制行为,你的目标就是把它改成1。
  2. 确认是否有不必要的只读事务。有些框架会习惯不管什么语句先用begin/commit框起来。我见过有些是业务并没有这个需要,但是也把好几个select语句放到了事务中。这种只读事务可以去掉
  3. 业务连接数据库的时候,根据业务本身的预估,通过SET MAX_EXECUTION_TIME命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。(为什么会意外?在后续的文章中会提到这类案例)

其次,从数据库端来看:

  1. 监控 information_schema.Innodb_trx表,设置长事务阈值,超过就报警/或者kill
  2. Percona的pt-kill这个工具不错,推荐使用;
  3. 在业务功能测试阶段要求输出所有的general_log,分析日志行为提前发现问题;
  4. 如果使用的是MySQL 5.6或者更新版本,把innodb_undo_tablespaces设置成2(或更大的值)。如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便。