带你一文了解分布式事务~

书接上文,如果不明白我之前写了什么的话,可以看下之前我文章:

序列化隔离级别在数据库系统中可以解决其他隔离级别无法处理的问题,这是因为它提供了最高的隔离性和数据一致性。

目前大多数提供可序列化的数据库都使用了三种技术之一

  • 串行顺序执行事务(完全意义上的串行)
  • 两阶段锁定(2PL)
  • 乐观并发控制技术,例如可序列化的快照隔离(serializable snapshot isolation)

串行执行

避免并发问题的最简单方法就是完全不要并发:在单个线程上按顺序一次只执行一个事务。

大家的可能第一反应就是这种设计效率一定很低下!

但是,大家耳熟能详的Redis,就是单线程设计,因为它可以避免锁的协调开销,所以有时可以比支持并发的系统更好。我们自然可以想到,单线程的系统是有一定条件的,那条件是什么呢?

第一点是RAM足够便宜了,许多场景现在都可以将完整的活跃数据集保存在内存中。当事务需要访问的所有数据都在内存中时,事务处理的执行速度要比等待数据从磁盘加载时快得多。第二点是通常事务都比较短,而且只进行少量的读写操作,如果是长时间运行的只读类的分析查询,可以让他们在串行执行循环之外的一致快照(使用快照隔离)上运行

我们可以总结一下完全串行的条件:

  • 每个事务都必须小而快,只要有一个缓慢的事务,就会拖慢所有事务处理。
  • 活跃数据集甚至全部数据可以放入内存的情况,在单线程执行的事务中访问磁盘,系统就会变得非常慢 。
  • 写入吞吐量必须低到能在单个CPU核上处理,如若不然,事务需要能划分至单个分区, 且不需要跨分区协调(这种主要是考虑分布式数据分区的情况)

两阶段锁定(2PL)

两阶段的意思是第一阶段(当事务正在执行时)获取锁,第二阶段(在事务结束时)释放所

注意两阶段锁定不是两阶段提交,两阶段锁定(Two-Phase Locking)是一种并发控制机制,旨在确保数据库事务的一致性和隔离性。它分为两个阶段:加锁阶段和解锁阶段。在加锁阶段,事务会获取所需的所有锁,执行数据库操作。而在解锁阶段,事务会释放先前获取的所有锁。这种机制的关键是保证事务在获取锁之后不会释放锁,直到事务完成为止。这样可以防止数据不一致和并发冲突的发生。两阶段提交(Two-Phase Commit)是一种分布式事务处理协议,用于确保多个数据库节点上的事务操作的原子性和一致性。它同样分为两个阶段:准备阶段和提交阶段。在准备阶段,事务协调者会询问各个参与者节点是否可以提交事务。如果所有参与者都同意提交,那么在提交阶段,协调者会发出提交命令,所有节点都会执行提交操作。如果任何一个参与者无法提交,协调者会发出回滚命令,所有节点都会执行回滚操作,以确保事务的一致性。

简单比喻一下,就是加入你要准备做菜,你先让人帮你买菜、洗菜,做好这些前置准备工作,然后你才能开始做菜,这种准备-提交就是两阶段提交。现在你准备做菜了,但还有其他厨师跟你一起,你不想他们用你的菜,否则你做的菜就不好吃了,所以你再做菜时先把菜给盖上,昨晚了在打开盖子,这样别人就拿不了你的菜了,这就是两阶段锁定

两阶段锁定定类似,但使锁的要求更强。只要没有写入,就允许多个事务同时读取同一个对象。但对象只要有写入(修改或删除),就需要独占访问(类比于读写锁)

  • 如果事务A读取了一个对象,并且事务B想要写入该对象,那么B必须等到A提交或中止才能继续
  • 如果事务A写入了一个对象,并且事务B想要读取该对象,则B必须等到A提交或中止才能继续

快照隔离使得读不阻塞写,写也不阻塞读,这是2PL和快照隔离之间的关键区别。

那么如何实现两阶段锁呢?

实现原理跟读写锁极其类似:通过共享锁与独占锁

  • 若事务要读取对象,则须先以共享模式获取锁。允许多个事务同时持有共享锁。但如果另一个事务已经在对象上持有排它锁,则这些事务必须等待。
  • 若事务要写入一个对象,它必须首先以独占模式获取该锁。没有其他事务可以同时持有锁(无论是共享模式还是独占模式),所以如果对象上存在任何锁,该事务必须等待。

但代价就是,复杂锁机制的引入,可能更容易触发死锁,并且性能也降低很多,系统的延迟也很不稳定,高百分位点处的响应可能会非常的慢,这种现象的主要原因是由于如果有一个事务比较慢,就会影响后续所有的事务响应

谓词锁与间隙锁

在数据库中,谓词是一个用于确定数据行是否符合特定条件的表达式。谓词锁是针对满足特定谓词条件的数据行进行的锁定机制。你可以将它想象成一个保卫,它会在数据库中某个特定数据行上站岗并确保只有满足特定条件的查询才能访问这些数据。

让我们来打个比方,假设你是一个旅馆的前台工作人员,而数据库就像是你存放客人信息的大档案柜。现在,有个客人想要查询所有年龄大于等于 18 岁的客人的信息。这个查询就是一个谓词,因为它限定了年龄条件。作为前台工作人员,你会检查每个档案文件,并在年龄满足条件的文件上放上一个锁,确保只有满足条件的客人信息才能被查询到。这样,其他客人的信息就被安全地锁定在柜子里,不会被访问到。

谓词锁甚至适用于数据库中尚不存在,但将来可能会添加的对象(幻象)。如果两阶段锁定包含谓词锁,则数据库将阻止所有形式的写入偏差和其他竞争条件,因此其隔离实现了可串行化。

但谓词锁性能不佳,如果活跃事务持有很多锁,检查匹配的锁会非常耗时。因此,大多数使用2PL的数据库实际上实现了索引范围锁(也称为间隙锁(next-key locking)),这是一个简化的近似版谓词锁

间隙锁是数据库管理系统中用于保护数据行之间“间隙”(即不存在的数据行的区域)的一种锁定机制。它防止其他事务在某个范围内插入新数据,从而确保数据的连续性和一致性。

让我们再用一个类比来理解间隙锁。假设你是一个书店的老板,书架上摆放着按照书名字母顺序排序的书籍。现在,有个顾客想要购买一本书,但他只记得这本书的标题的前三个字母,比如"App"。这时,他会从书架上寻找这本书,并将手放在"A"和"B"字母之间的间隙处,这个间隙就是间隙锁的概念。这样,其他顾客就不能在同样的间隙处插入一本新书,保证了书架上书籍的有序性。

能够有效防止幻读和写入偏差。索引范围锁并不像谓词锁那样精确(它们可能会锁定更大范围的对象,而不是维持可串行化所必需的范围),但是由于它们的开销较低,所以是一个很好的折衷。

但如果没有可以挂载间隙锁的索引,数据库可以退化到使用整个表上的共享锁。这对性能不利,因为它会阻止所有其他事务写入表格。

序列化快照隔离(SSI)

ssl的出现主要是减少序列化的隔离级别和高性能的相互矛盾。它提供了完整的可序列化隔离级别,但与快照隔离相比只有只有很小的性能损失。

两阶段锁是一种所谓的悲观并发控制机制:它是基于这样的原则:如果有事情可能出错(如另一个事务所持有的锁所表示的),最好等到情况安全后再做任何事情。这 就像互斥,用于保护多线程编程中的数据结构。串行化相比之下就更悲观了。相比之下,序列化快照隔离是一种乐观的并发控制技术。在这种情况下,乐观意味着,如果存在潜在的危险也不阻止事务,而是继续执行事务,希望一切都会好起来。当个事务想要提交时,数据库检查是否有什么不好的事情发生

SSI基于快照隔离——也就是说,事务中的所有读取都是来自数据库的一致性快照。与早期的乐观并发控制技术相比这是主要的区别。在快照隔离的基础上,SSI添加了一种算法来检测写入之间的序列化冲突,并确定要中止哪些事务。

基于过时前提的决策

事务中的查询与写入可能存在因果依赖。简单的来说,假如存在一个事务A,先进行了selcet操作,获取了数据集a1,然后再a1的基础上进行update操作;但中间有一个事务B,做了一些写入操作,对于数据集a1来说,此时变成了a2,如果事务A提交了,就会有有问题。

所以关键在于如何检测事务之间的这种读写的依赖关系

数据库如何知道查询结果是否可能已经改变?有两种情况需要考虑:

  • 检测对旧MVCC对象版本的读取(读之前存在未提交的写入)。打个比方,这种方式就相当于有几个人拥有一把钥匙,初始情况下钥匙都能打开同一扇门,当你准备用钥匙打开时,你扯着嗓子问前面已经开过门的人:“喂,你们有没有换过钥匙啊,有的话把新钥匙给我”
  • 检测影响先前读取的写入(读之后发生写入)。这种就相当于,你用钥匙开门后,你还换了一把钥匙,然后你扯着嗓子对后面的人喊:“后面的兄弟!门的钥匙我换了,你们重新拿下钥匙”

与两阶段锁定相比,可序列化快照隔离的最大优点是一个事务不需要阻塞等待另一个事务所持有的锁。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值