事务是最常接触的数据库概念,它有四大特性:原子性、一致性、隔离性、持久性。
在 MySQL 中事务的支持是在引擎层实现的,但是并不是所有引擎都支持事务,这就是推荐使用 InnoDB 的原因之一。
隔离性与隔离级别
当数据库有多个事务在同时执行的时候,就可能出现脏读(dirty read)、不可重复度(non-repeatable read)、幻读(phantom read)的问题,由此引出隔离级别的概念。
隔离的越严效率就会越低,SQL标准的隔离级别有:
- 读未提交(read uncommitted)一个事务还未提交时,它做的变更就能被别的事务看到。
- 读提交(read committed)一个事务提交后,它做的变更才能被别的事务看到。
- 可重复读(repeatable read)一个事务的执行过程中读到的数据,总是保持一致的,不受其他事务的提交而受到影响(MySQL默认是这个级别)。
- 串行化(serializable)对于同一条数据,只允许一个事务读或写,读锁、写锁。
例子:
┌————————┬————————┐
事务A 事务B
├————————┼————————┤
启动事务 启动事务
查询得到值1
├————————┼————————┤
查询得到值1
将1改为2
├————————┼————————┤
查询得到值V1
├————————┼————————┤
提交事务B
├————————┼————————┤
查询得到值V2
├————————┼————————┤
提交事务A
├————————┼————————┤
查询得到值V3
└————————┴————————┘
- 读未提交:V1,V2,V3=2
- 读提交:V1=1;V2,V3=2
- 可重复读:V1,V2=1;V3=2
- 串行化:V1,V2=1;V3=2
在实现上,数据库会创建一个读视图,访问的时候以视图逻辑为准。在可重复读的隔离级别下,这个视图是在事务启动的时候创建的,整个事务期间都用这个视图。在读提交的隔离级别下,这个视图是在 SQL 开始执行的时候创建的。读未提交则直接读原始数据,没有视图的概念。串行化直接以加锁的形式防止并发的发生。
不同的数据库,默认的事务隔离级别也是不一样的。所以如果有迁移数据库的需求一定要保证数据库的隔离级别保持一致。可以通过show variables like 'transaction_isolation';来查看当前的值。也可以通过修改启动参数 transaction_isolation 的值来改变隔离级别。
事务隔离的实现
在 MySQL 中,每条记录在更新操作时都会在 undo log(回滚日志)记录版本数据,记录上的新值都可以通过回滚操作得到前一个状态的值。
例:可重复读,某个值从1被按顺序改成了2、3、4,在 undo log 里面就会有类似这样的记录
read-view A read-view B read-view C row
| | | |
将2改成1 <—— 将3改成2 <—— 将4改成3 <—— 当前值4
└———————————————————┘
回滚日志记录
当前值被事务C改成4,但在可重复读的隔离级别下,不同事务会有不同的 read-view。对于事务A对应的 read-view A,要的得到1就必须将当前值依次执行所有的回滚操作得到。同一条记录在系统中存在多个版本,就是数据库的多版本并发控制(MVCC)。
那么这条记录的 undo log 什么时候删除呢?答案是,系统会判断当没有事务再需要这些回滚日志的时候才会被删除。所以不建议使用长事务,长事务就意味着系统里面会保存很老的事务视图,在这个事务提交前数据库需要保留所有的回滚日志,导致大量占用存储空间。
在 MySQL 5.5 及以前的版本,undo log 会跟数据字典一起放在 ibdata 文件里,即使事务提交日志清除,文件也不会变小。
事务的启动方式
- 显式的启动事务,
begin或start transaction,配套的提交语句是commit,回滚语句是rollback。(该方式创建的事务,事务在第一个操作表的 SQL 语句执行时才真正启动。意味着 read-view 的创建也在这个时候。要想立即创建 read-view 也可用start transaction with consistent snapshot命令)。 set autocommit=0;这个命令会将这个线程的自动提交关闭。意味着你只执行一个 select 语句这个事务就启动了,且不会自动提交。事物会一直持续到手动执行commit或rollback或断开连接。有些客户端连接框架会默认连接成功后先执行一个set autocommit=0;而很多数据库连接池维护可复用的长链接,这就意味这会导致长事务。
在 autocommit=1 的情况下如果开发需要对事务进行操作,则用begin显式的方式启动事务,如果执行commit则提交事务,如果执行commit work and chain则提交事务并自动启动下一个事务,这样省去了再次begin的开销。
长事务的查询:select * from information_schema.innodb_trx;
