之前提到了不同的数据库隔离级别,有一些问题需要提出:
- 隔离级别不容易理解,在各个数据库中有不同的实现和保证;
- 从应用代码角度很难判断一段代码在特定隔离级别下是否安全;
- 竞态条件不容易检测到;
从应用开发者角度来看,串行化隔离可以说是最高的隔离级别了。串行化指并行事务执行结果和串行执行结果一致的保证(避免各种形式的竞态条件)。
实际串行执行
最简单的串行化保证就是真实的串行执行操作。这种方式看似牺牲了多线程执行的并行能力,实际上在最近,这种想法已经被很严肃的考虑:
- 内存价格下降,使得内存全量数据存储变得可能,大大降低了单个事务的执行时间;
- OLTP型事务通常不会有过多读写操作;
这种思想已经被VoltDB/H-Store,Redis和Datomic所采用。
串行执行有以下特点:
- 每个事务需要短小快速,以防止阻塞其它所有事务;
- 写操作执行速度由cpu来决定,或者通过数据分片来提升多核单机的并发能力;
- 如果执行了分片策略(几乎所有分布式数据存储都需要分片),跨片的事务执行效率会很低;
过于单线程串行化的数据库,Redis和VoltDB都非常值得学习。
两段锁
之前说过,锁可以有效避免脏读脏写。两段锁要求多事务可以同时访问某条没有被写锁加锁的数据,如果被写锁加锁,则:
- 如果A事务要求读X数据,后来的B事务要求写X数据,则B必须在A完成或者回滚之后才能执行;
- 如果A事务要求写X数据,后来的B事务要求读X数据,则B必须在A完成或者回滚之后执行;
在两段锁的具体实现上:
- 事务读取数据需要获取数据的共享锁(shared lock),一条数据允许同时发放多个共享锁,除非它已经被互斥锁(exclusive lock)锁定;
- 事务更新数据需要获取数据的互斥锁(exclusive lock),一条数据同一时间只能发放单个互斥锁;
- 如果一个事务先读后写,则需要把共享锁升级成互斥锁;
- 事务获取锁之后,只有在事务完成或者回滚之后才会释放锁(这就是两段锁的含义:一个阶段获取锁,一个阶段释放锁),需要注意的是,两段锁并不能避免死锁,死锁发生时一般会选择先回滚一个事务;
串行化快照隔离(Serializable Snapshot Isolation)
乐观并发控制VS悲观并发控制:
上面提到的两段锁就是典型的悲观锁。串行化执行等价于为每一个事务加上互斥锁,为了减少单个事务持有锁的时间,只能把事务拆分成较小的粒度。与之相反,串行快照隔离是乐观锁。串行快照隔离,字面上来讲还是在读取阶段保证数据来自于一个稳定版本的快照,加上对于线性执行的冲突检测来决策是否应该回滚某一个事务。为了避免可能出现的写倾斜,有下面两种方法:
-
检测过去MVCC读取(Detecting stale MVCC reads)
根据MVCC所带有的版本号来检测读取之后发生的数据变更:
Detecting stale MVCC reads
如果检测到了这种情况,需要事务回滚重试。以此避免写倾斜。
-
检测影响先前读取的写入
这种情况如图所示:
图示
网友评论