事务的引入
提到事务,我们肯定会不陌生,在和数据库打交道的时候,我们通常会用到事务。最经典的例子就是转账,你要给朋友小王转100块钱,但是你此时银行卡上只有100块。转账的过程具体到程序中就会有一系列的操作,比如查询余额,做加减法,更新余额等,这些操作必须保证是一体的,不然等程序查完之后,还没有做加减法之前,你这100块钱,完全可以借助时间差再查一次,然后再给另一个朋友转账,如果银行这么整,不久全乱了吗?这个时候,就要提到事务的概念了。
简单来说,事务的作用是保证一组数据库的操作,要么全部成功,要么全部失败。在mysql中,事务支持是在引擎层实现的,但是并不是所有的引擎都支持事务,这也是Myisam被InnoDB取代的重要原因。
隔离性与隔离级别
提到事务,你肯定会想到ACID(原子性,一致性,隔离性,持久性),今天所提到的是其中的I,也就是隔离性。
当数据库上有多个事务同时执行的时候,就可能出现脏读,不可重复读,幻读的问题,为了解决这些问题,就有了隔离级别的概念。
在谈到隔离级别之前,你首先要知道,你隔离得越严实,效率就会越低,这个通常体现在不同隔离级别下的并发效率有关。因此我们需要在其中找到一个平衡点。SQL的事务隔离级别主要包括:读未提交,读已提交,可重复读,串行化。
- 读未提交是指,一个事务还没有提交的时候,它所做的变更就能被别的事物看到。
- 读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
- 可重复读是指,一个事务执行过程中所看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读的隔离级别下,未提交的变更对其他事务也是不可见的。
- 串行化,对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
引入一个例子进行解释:
这是两个事务在针对同一行记录进行修改,在不同的隔离级别下,所得到的V1, V2, V3的值是不一样的。
- 如果隔离级别是“读未提交”,V1 = 2,V2 = 2,V3 = 2,在读未提交下,事务A在查询得到值V1的操作是在事务B将值1改成了2之后,虽然事务B还没有提交,但是在这种隔离级别下,事务A能读到事务B还没有提交事务之前所做的修改。
- 如果隔离级别是“读已提交”,V1 = 1,V2 = 2,V3 = 2,在读已提交下,事务A在查询得到值V1的操作是在事务B执行提交事务之前,所以V1的值还未改变,还是1,等到事务B提交之后,得到的值V2,V3均为2。
- 如果隔离级别是“可重复读”,V1 = 1,V2 = 1,V3 = 2,在可重复读下,事务A在开启事务时,开启一个视图,这个视图里所有的数据的属性是不会发生改变的,启动事务时读取到的值是1,因此在事务结束之前,读取到的值均为1,在提交事务A之后,再次查询得到的值V3便为2了。
- 如果隔离级别是“串行化”,V1 = 1,V2 = 1,V3 = 2,在串行化下,事务A在开启事务时,给该条记录上了读锁,事务B在执行时候,必须要等该条记录的读锁被释放之后才能进行写操作,所以在事务A提交之前,得到的值V1,V2均未变化。在提交事务A之后,读锁释放,事务B给该条记录上了写锁,查询得到值V3等待事务B写锁释放,得到修改后的值为2。
实际上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在“可重复读”的隔离级别下,这个视图是在事务启动的时候创建的,整个事务存在期间所获取的数据均从这个视图中获得。在“读提交”的隔离级别下,这个视图是在每个SQL语句开始执行的时候创建的。这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图的概念;而“串行化”隔离级别下直接用加锁的方式来避免并行访问。
所以我们看到在不同的隔离级别下,数据库的行为是不同的。Oracle数据库的默认隔离级别就是读提交,Mysql数据库的默认隔离级别是可重复读,因此对于一些从Oracle迁移到Mysql的应用,为保证数据库隔离级别的一致,也需要把Mysql的隔离级别设置为读已提交。
mysql中配置事务隔离级别的方式是设置启动参数transaction-isolation的值。可以用show variables 来查看当前的值。
总之,没有规定必须使用某种隔离级别,根据不同的业务场景来区分使用什么隔离级别。
假设你在管理一个个人银行账户表。一个表存了账户余额,一个表存了账单明细。到了月底你要做数据校对,也就是判断上个月的余额和当前余额的差额,是否与本月的账单明细一致。你一定希望在校对过程中,即使有用户发生了一笔新的交易,也不影响你的校对结果。这时候使用“可重复读”隔离级别就很方便。事务启动时的视图可以认为是静态的,不受其他事务更新的影响。
事务隔离的实现
理解事务的隔离级别,我们来看看事务隔离是怎么实现的。
在Mysql中,实际上每一条记录在进行更新的时候都会同时记录一条回滚操作。记录上最新的值,通过回滚操作,都可以得到前一个状态的值。
假设一个值从1被按照顺序改成了2,3,4,在回滚日志中就会有类似下面的记录
当前值是4,在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。如在图中看到的,在视图A,B,C中,这个记录的值分别是1,2,4,同一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制(MVCC)。对于read-view A,要得到1,就必须依照图中依次执行的操作进行回滚,回到当时值为1的版本。如果现在同时有一个事务正在将4改成5,这个事务跟read-view A,B,C对应的事务也是不会发生冲突的。
回滚日志不能一直保留,什么时候删除呢?答案是,在不需要的时候就进行删除,也就是说系统中会判断,当没有事务再需要这些回滚日志的时候,回滚日志就会被删除。什么时候不需要呢?当系统里没有比这个回滚日志更早的read-view的时候。
所以基于以上说明,我们尽量不要使用长事务。长事务意味着系统里面会存在很老的视图。由于这些事务随时可能访问数据库里面的任何数据,所以在这个事务提交之前,数据库里它可能用到的回滚记录都必须保留,就会导致占用大量的存储空间。
事务的启动方式
mysql事务启动的方式有以下几种:
1.显式启动事务语句,begin 或 start transaction。配套的提交语句是commit,回滚语句是rollback。
2.set autocommit = 0,这个命令会将这个线程的自动提交关闭掉。意味着如果你只执行一个select语句,这个事务就启动了,而且并不会提交。这个事务会持续存在直到你主动执行commit或rollback语句,或者断开连接。
有些客户端连接框架会默认连接成功之后先执行一个set autocommit = 0的命令。这就导致了接下来的查询都会在一个事务中,如果是长连接,就导致了意外的长事务。因此,使用set autocommit = 1,通过显式语句的方式来启动事务。
我们可以通过show varibles like "autocommit"的命令来查看事务自动提交是否打开。
在autocommit开启的情况下,用begin显式启动的事务,如果执行commit则提交事务。如果执行commit wok and chain,则是提交事务并自动启动下一个事务,这样也省去了再次执行begin语句的开销。同时带来的好处是从程序开发的角度明确地知道了每个语句是否处于事务中。