事务隔离
简介
事务的隔离性是事务的四大特性之一,原子性,持久性,隔离性,一致性。
隔离性定义了事务在并发过程中事务操作的结果以及过程是否对并发中的其他事务可见。
事务的隔离级别一般分为读未提交,读已提交,可重复度以及串行化等四个级别。
除了串行化因为定义了对同一条数据的读写只能串行完成,所以不存在对同一条数据的并发问题,显而易见也就不会出现数据可见性的问题。但是串行化因为对于热点数据的锁定严重影响了数据库的性能,随着数据库产品的完善,一般的数据库产品都会提供多种隔离级别让用户选择。
不同隔离级别下可能会出现的数据异常情景分为下列几类:
概念 | 名次解释 |
---|---|
脏读 | 脏读表示查询到了其他用户还未提交的数据。 |
不可重复读 | 不可重复读是指因为其他事物的修改删除操作导致对数据两次查询结果不同。 |
幻读 | 幻读和不可重复读类似,也是因为并行的其他事务导致的,但是一般情况下幻读特定范围查询会出不同的结果集。
不可重复读可以使用MVCC解决,但是幻读不可以。 |
脏写 | 脏写的问题是因为事务写入过程中不加锁的情况下会导致的一些异常情况。
例如事务1希望把x,y都修改为1,事务2希望把x,y都修改为2,并发场景下可能会出现x=1,y=2的情况,不满足任何一个事务想要的结果。 |
当前常见的隔离级别对上述异象情况的解决如下:
隔离级别 | 脏写 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
读未提交 | ❌ | ✅ | ✅ | ✅ |
读已提交 | ❌ | ❌ | ✅ | ✅ |
可重复读 | ❌ | ❌ | ❌ | ✅ |
串行化 | ❌ | ❌ | ❌ | ❌ |
原理
实现隔离级别一般使用到的技术有互斥锁,读写锁,乐观锁,MVCC。
互斥锁
互斥锁模型,在不同的数据库事务进行数据临界区的时候,需要首先执行加锁操作,只有成功获取锁的事务才能继续执行,否则会先进入等待直到获取锁的事务执行结束,重新请求加锁操作。
互斥锁在串行化隔离级别以及实现写操作的过程中被广泛应用。
读写锁
读写锁分为读锁和写锁,写锁是互斥锁,也就是在写操作执行的过程中,不允许其他操作包括读操作执行,但是读锁是共享的,在没有请求的场景,多个事务可以同时查询同样的数据。
读写锁可以实现读已提交。
乐观锁
相较悲观锁,乐观锁使用了更加乐观的并发机制,或者将事务按照依赖关系进行拓扑排序,或者在提交的时候再延迟校验事务是否可以提交。
基于乐观程度的不同,可以分为以下三个等级:
- 基于Lock:最悲观的实现,需要在操作开始前,甚至是事务开始前,对要访问的数据库对象加锁,对冲突操作Delay;
- 基于Timestamp:乐观的实现,每个事务在开始时获得全局递增的时间戳,期望按照开始时的时间戳依次执行,在操作数据库对象时检查冲突并选择Delay或者Abort;
- 基于Validation:更乐观的实现,仅在Commit前进行Validate,对冲突的事务Abort
Lock的作用范围在事务开启前,Timestamp的作用范围是事务中,Validation仅在提交前校验。
MVCC
多版本(MVCC)的实现机制可以并行化处理读写事务和只读事务,进一步提升性能。
根据乐观程度多版本的机制也分为三类:
- Two-phase Locking (MV2PL)
与单版本的2PL方式类似,同样需要Lock Table跟踪当前的加锁及等待信息,另外给数据库对象增加了多版本需要的begin-ts和end-ts信息。写操作需要对最新的版本加写锁,并生成新的数据版本。读操作对找到的最新的可见版本加读锁访问。
- Timestamp Ordering (MVTO)
对比单版本的Timestamp方式对每个数据库对象记录的Read TimeStamp(RT),Write TimeStamp(WT),Commited flag(C)信息外增加了标识版本的begin-ts和end-ts,同样在事务开始前获得唯一递增的Start TimeStamp(TS),写事务需要对比自己的TS和可见最新版本的RT来验证顺序,写入是创建新版本,并用自己的TS标记新版本的WT,不同于单版本,这个WT信息永不改变。读请求读取自己可见的最新版本,并在访问后修改对应版本的RT,同样通过判断C flag信息避免Read Uncommitted。
- Optimistic Concurrency Control (MVOCC)
对比单版本的Validataion(OCC)方式,同样分为三个阶段,Read阶段根据begin-ts,end-ts找到可见最新版本,不同的是在多版本下Read阶段的写操作不在私有空间完成,而是直接生成新的版本,并在其之上进行操作,由于其commit前begin-ts为INF,所以不被其他事务看见;Validation阶段分配新的Commit TID,并以之判断是否可以提交;通过Validation的事务进入Write阶段将begin-ts修改为Commit TID。
MVCC可以实现读已提交和可重复读,区别在于读已提交每次请求查询都生成新的ReadView,这样就可以读取最新的已提交数据,而可重复读只在事务开启的时候生成ReadView。
间隙锁
并发写问题的解决方式就是行锁,而解决幻读用的也是锁,叫做间隙锁,MySQL 把行锁和间隙锁合并在一起,解决了并发写和幻读的问题,这个锁叫做 Next-Key锁。
实现
避免脏写
四种隔离级别都需要杜绝脏写,避免脏写的方式是写操作进行加锁。对于数据库的写操作而言,可以使用悲观锁或者乐观锁。
读未提交
读未提交是最低级别的隔离,该隔离级别下事务可以查询其他事务没有commit的数据,也就意味着对于读操作不需要进行加锁或者其他操作。
读已提交
读已提交可以通过两种方式实现。
读写锁当数据被修改的时候禁止读操作,所以读请求会延迟到事务提交之后执行,也就实现了读已提交。
MVCC也可以实现读已提交。
读已提交在使用MVCC的过程中,每次查询操作都会生成新的ReadView,这样就可以读取到最新提交的数据了。
可重复读
可重复读一般使用MVCC实现。
可重复读在使用MVCC的过程中,只在事务开启的时候生成ReadView,这样事务过程中读取的数据就可以保持一致。
串行化
串行化在执行过程中,无论读写操作都是用互斥锁进行隔离。