事务之串行化

1,003 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情

本系列主要是《数据密集型应用系统设计》阅读笔记,本文记录事务主题的笔记心得。

串行化

解决并发问题的直接有效方法是避免并发。 多线程并发在过去30年的时间一直被认为是提升性能的关键。那么现在也有逐渐转向单线程执行的趋势,是为什么呢?

  • 内存越来越便宜
  • OLTP事务通常非常快。而运行时间长的分析查询通常是只读的,可以在一致性快照上运行。 Redis等采用串行化执行事务。单线程有时比支持并发的系统效率还高,但吞吐量上线上单个CPU。

那么怎么串行化呢?

串行化方法:两阶段加锁

串行化方法基本只有一种,那就是两阶段加锁。 基本原则是多个事务可以同时读取同一对象,只要出现任何写操作,必须加锁以独占访问。

  • 事务A已经读取了某个对象,事务B想要修改该对象,则B必须等待A提交或终止才能继续
  • 事务A已经修改了某个对象,事务B想要读取该对象,则B必须等待A提交或终止才能继续

不会出现读到旧值的情况。 并发也出现在写读之间,也就不会出现丢失更新和幻读等问题。

实现两阶段加锁

可以使用读写锁来实现两阶段加锁。锁可以处于共享或独占模式,基本用法:

  • 事务如果要读取对象,必须以共享模式获得锁;
  • 事务如果要修改对象,则必须以独占模式获得锁。
  • 事务如果首先读取对象然后修改对象,则共享锁必须升级为独占锁
  • 事务获得锁之后,一直持有锁直到事务结束。 可见,这是简单的读写锁机制。至少更容易出现锁等待或者死锁现象。

两阶段加锁性能

自1970年以来不被所有人接受的原因主要是因为性能原因:其事务吞吐量和查询响应时间下降很多。不仅因为加解锁的开销,更容易出现锁等待或者死锁现象。传统的关系数据库不限制事务的执行时间,那么事务的等待可能是没有上限的,导致数据库的访问具有很大的不确定。不能达到稳定如一的性能。

可串行化是怎么防止幻读的?

可串行化隔离是怎么防止幻读的?假设会议室预定的例子:

BEGIN ;
select count(*) from bookings
where  room_id=1234
and end_time>'2022-06-06 12:00' and start_time<'2022-06-06 13:00';
//上面的条件返回0则执行
insert into  bookings(room_id,start_time,end_time,user_id)
values(123,'2022-06-06 12:00','2022-06-06 13:00',666);
commit

事务查询某个时间段一个房间的预定情况,则另一个事务不能去插入这个时间段这个房间的预定。因为没有具体的行,那么怎么加锁呢?串行化引入了一种谓词锁,类似于共享/独占锁,不属于表的某一行,而是作用于满足某些搜索条件的所有的查询对象。 例如

select * from bookings
where  room_id=1234
and end_time>'2022-06-06 12:00' and start_time<'2022-06-06 13:00';

谓词锁和两阶段锁结合使用,才可以防止所有形式的写倾斜,隔离真的变得可串行化。

其他

谓词锁性能不佳的优化有索引区间锁,而对串行化两阶段锁的性能低下的问题,乐观思想的可串行化的快照隔离策略被提出,虽然也有一定的局限性。