持续创作,加速成长!这是我参与「掘金日新计划 · 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';
谓词锁和两阶段锁结合使用,才可以防止所有形式的写倾斜,隔离真的变得可串行化。
其他
谓词锁性能不佳的优化有索引区间锁,而对串行化两阶段锁的性能低下的问题,乐观思想的可串行化的快照隔离策略被提出,虽然也有一定的局限性。