并发事务的问题
当同一个数据(对象)被并发事务访问的时候,会有什么问题?
上面的场景可以理解为:同一个账户两笔转账并发发生会怎么样?假设X初始值为100,那么最终X会变为多少?
其原因在于X=X+1并不是一个原子操作,而是Read(X)和Write(X)这两个原子操作的集合。这就导致 同一个对象上的并发事务 可能因为原子操作的不同顺序而导致最终结果不同。
并发事务的正确性
如何判断一组 并发事务 正确执行?
存在一个顺序,按照这个顺序依次串行执行这些事务,得到的结果与并行执行的结果相同。我们就说这个组并发事务是可串行化的。这也是判断一组并行事务是否正确执行的标准。如果一组并行事务的执行结果找不到任何一组串行执行的结果与其相同,那么可以说这组并发事务没有正确的执行。比如在上一节的情景中,如果2个并发事务的执行结果为102则正确执行,如果为101则没有正确执行。
并发事务的4种隔离级别
数据冲突引起的问题
- read uncommited data(读脏数据):在T2提交之前,T1读了T2已经修改了的数据
- unrepeatable reads(不可重复读):在T2提交之前,T1写了T2已经读的数据。如果T2再次读同一个数据,那么将发现两次读取的值不同。因此叫做不可重复读。
- overwrite uncommitted data(更新丢失):在T2提交之前,T1重写了T2已经修改的数据。
| | 读未提交(脏读) | 不可重复读 | 更新丢失 |
| :-----: | :-----------: | :-------: | :------: |
| 序列化 | no | no | no |
| 可重复读 | no | no | possible |
| 读已提交 | no | possible | possible |
| 读未提交 | possible | possible | possible |
MySQL事务默认的隔离层级是可重复读,不会出现脏读和幻读现象,但是有可能会有一个事务的更新被另外一个事务的更新覆盖导致更新丢失的现象。
完全序列化大绝大多数情况下是没有必要的,因为很多并发事务并不会同时访问同一个对象(即没有数据竞争)。使用完全序列化这个隔离级别会极大降低并发的性能。
针对上面问题的举例:
针对并发事务的解决方案
针对并发事务,主要是两大类解决方案。
悲观并发控制
使用悲观并发控制的前提假设是:数据竞争可能经常出现,即并发事务可能经常访问同一个对象。
悲观并发控制的主要手段是 防止,即采用某种机制确保数据竞争不会出现。
如果一个事务T可能和正在运行的其他事务有冲突,那么就让这个T等待,一直等到有冲突的其他所有事务都完成为止,才开始执行事务T。
一般,悲观并发控制使用加锁协议来实现,对事务中的读写数据进行加锁,通常采用 两阶段加锁(2 Phase Locking,2PL)。
2PL:
- 对每个访问的数据都要加锁之后才能访问
- 在事务开始的时候,对每个需要访问的数据加锁。如果不能加锁,就等待,直到加锁成功。加锁成功之后,执行事务内容。在事务提交之前,集中进行解锁。最后,事务提交。
- 有一个集中的加锁阶段和一个集中的解锁阶段,两阶段加锁由此得名。
如上图,使用2PL之后,两个会访问同一个对象的并发事务不会同一时刻执行。
悲观锁为什么一定要集中在事务开始和事务提交两个阶段呢?
上面这个图,在保证没有脏读和幻读的情况下,就会出现事务T2更新丢失的情况。
悲观锁的实现
- Shared lock(共享锁,S):保护读操作
- Exclusive lock(排它锁,X):保护写操作
一个对象被一个读锁占用,可以同时被另外一个读锁占用,但是不能被写锁占用。一个对象被一个写锁占用,不能被其他的读锁或写锁占用。
数据库锁的实现粒度也是不同的。锁的对象可以是表、行(记录)、索引等。
Intent lock(意向锁)是为了提高封锁子系统的效率:
- 意向读锁 IS(a):将对a下面更细粒度的数据元素进行读
- 意向写锁 IX(a):将对a下面更细粒度的数据元素进行写
为了得到S、IS,所有祖先必须为IS或IX。为了得到X、IX,所有祖先必须为IX(不能为IS,如果祖先是IS,那么别的事务就可以对祖先申请S锁,这与那个子孙对象是X锁显然是相矛盾的)。
乐观并发控制
使用乐观并发控制的前提是:数据竞争很少,即并发事务很少会访问到同一个对象上。
乐观并发控制的主要手段是 检查。具体过程为:允许所有的事务都直接执行。但是事务不直接修改数据,而是将修改保留到这个事务的私有工作区。当事务结束时,检查这些修改是否有数据竞争。如果没有冲突(即没有其他并发事务访问到同一个对象),检查通过,将私有工作区的修改复制到数据库公共数据中,成功结束;如果有竞争,清空私有工作区,重试事务。
MVCC(Multiversion concurrency control)是一种非常常见的乐观并发控制的实现方式,以加时间戳验证的方式实现。