事务其实是数据库的一个很重要的一个特性。
场景:
在遇到连表或者批量操作的时候,我们期望的是这个操作要么成功,要么不成功,其中在失败的时候,中间执行的一些数据需要进行回退,即使部分成功了,也要保持初始状态,以保证我们这一次整体的一致。
基于如此,事务就有了这么几个特性:
原子性(Atomic),一致性(Consitency),隔离性(Isolation),持久性(Durability)
特性:
原子性
表示的是这次事务要么执行,要么就失败,对外表现是呈现一个不可再分的原则
一致性
执行完和执行后的状态是要一致的
隔离性
这里表示的是多个事务之间操作是互不影响的,或者说是互不感知的,这里是有一些隔离级别的,但是最终的结果就是为了达到隔离的效果,其中不同的隔离级别对应的数据库的性能是不一样的。
持久性
一旦事务被提交了,那么这个结果是永远不可更改的。
隔离性
对于事务的一些特性而言,其中隔离性是实现起来比较复杂的,首先我们这里在考虑到没有事务的情况下,会出现哪些问题:
- 脏读:就是读取到了另外一个事务没有提交的数据。
- 不可重复读:就是在事务开启中间,多次读取到的数据不一样
- 幻读:开启事务后,多次读取到的数据个数不同(跟不可重复读的区别,在于幻读是数量增加或减少,不可重复度在于数据本身发生了变化)
针对上面的三个问题,数据库提出来四种隔离级别
- Read uncommit:(读未提交)有脏读问题
- Read commit:(读已提交)解决脏读问题,有不可重复读问题
- Repeatable read:(可重复读)解决不可重复读问题,有幻读问题
- Serializable:(串行化)解决幻读问题,所有问题都解决
可以这样来看对应的解决方案和存在的问题
由于事务可以分为三个阶段:读取、修改和提交,那么下面根据事务的三阶段,通过进行分析对应问题(脏读等问题)和采用对应的策略(行级锁等策略)来学习。如下图所示,其中我们后面就将读取阶段简称为1,修改阶段简称为2,提交节点简称为3。横轴表示时间线
下面我们用两个事务的时间线来讲解没有事务的一些隔离级别的时候存在的一些问题。
事务A:橘色
事务B:绿色
脏读
问题:
如果事务B的读取阶段位于事务A的修改和提交阶段(其实就是读取到了其他事务修改但是没有提交的数据),紧接着这个时候事务A发生回滚,那么事务B读取到的数据就是脏数据——脏读。
解决:
在事务修改和提交(也就是2和3阶段)之间增加写锁(就是排它锁,在读取期间不允许其他事务读取事务),这样就解决了脏读问题——其实就是Read commit(提交读)。
不可重复度
问题:
脏读问题解决了,但是在脏读问题之上又存在新的问题,比如在事务B第一次读取和第二次读取期间,事务A发生了修改和提交(这里我们站在解决脏读的问题上来看,事务A的2和3加了排它锁,我这里将两者合在一起),导致事务B第一次和第二次读取到的数据不一样(数据本身的值发生了变化)——不可重复读。
解决:
1.通过添加行级共享锁
通过在事务B的读取到提交(就是阶段1~3,上图我们就只显示了读取阶段)期间加上读锁,在读锁这个共享锁加锁期间,事务A是不能添加排它锁的,也就是不能进行修改,进而每次事务B读取到的都是最新提交的数据。但是这种仍然存在幻读(下面介绍)。
2.通过用mvcc策略
mvcc 可参考相关文档
幻读
问题:
不可重复读问题解决了,但是这里还有幻读问题,就是事务第一次读取和第二次读取,发现读取到的数据量不一样——幻读(这个跟不可重复读相似,但是这个强调的是数量的变化)。
下图我用灰色线表示两个事务处理的是不同的数据行(因为可重复读的隔离级别要求了一个事务读取到结束都加上了行级共享锁,其他事务是无法在中间修改)。
当其中事务A在某个时间修改了其他行的数据,而此修改之前事务B读取了一次,但是这个数据修改后却满足了事务B的查询要求,就会引起事务B在第一次和第二次查询期间数据量不一样,就产生了幻读的问题。
解决:
在一个事务的执行中,添加表级锁,读添加表级共享锁,写添加表级排它锁,这样事务之间的执行就会完全无交叉。但是这种表级锁对应引起的性能会比较低,因为所有的事务就是串行化的执行(这也许就是叫串行化的原因)。下面我们会讲解下,一个新的解决方法:mvcc。就是在解决幻读的情况下,能让隔离级别保持在不可重复度的阶段。