<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>数据库 on yanceyのBlog</title>
        <link>https://lyblogs.xyz/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/</link>
        <description>Recent content in 数据库 on yanceyのBlog</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en-us</language>
        <copyright>yanceyのBlog</copyright>
        <lastBuildDate>Thu, 10 Sep 2026 09:36:10 +0800</lastBuildDate><atom:link href="https://lyblogs.xyz/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>全局事务</title>
        <link>https://lyblogs.xyz/p/%E5%85%A8%E5%B1%80%E4%BA%8B%E5%8A%A1/</link>
        <pubDate>Thu, 10 Sep 2026 09:36:10 +0800</pubDate>
        
        <guid>https://lyblogs.xyz/p/%E5%85%A8%E5%B1%80%E4%BA%8B%E5%8A%A1/</guid>
        <description>&lt;img src="https://lyblogs.xyz/p/%E5%85%A8%E5%B1%80%E4%BA%8B%E5%8A%A1/forest.jpg" alt="Featured image of post 全局事务" /&gt;&lt;p&gt;我们把全局事务（Global Transaction）或外部事务（External Transaction）限定为单个服务使用多个数据源场景的分布式解决方案。理论上真正的全局事务并没有“单个服务”的约束，它本来就是 DTP（Distributed Transaction Processing，分布式事务处理）模型中的概念，但是我们在这里讨论的是一种在分布式环境下仍然追求强一致性的事务处理方案，对于多节点且需要相互调用彼此服务的场景是极不合适的。这种方案几乎只适用于单服务多数据源的场合中。&lt;/p&gt;
&lt;h2 id=&#34;xa-规范与核心角色&#34;&gt;XA 规范与核心角色
&lt;/h2&gt;&lt;p&gt;为了解决单个服务操作多个数据源时，如何保证多个数据源的事务同时成功或者同时回滚的问题，X/Open 组织定义了 XA 规范。该规范是一个与编程语言无关的底层架构设计，确立了全局事务的两个核心角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Transaction Manager（TM/全局事务管理器）：&lt;/strong&gt; 全局事务的“总指挥”。它负责统筹全局，告诉大家什么时候提交，什么时候回滚。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource Manager（RM/局部资源管理器）：&lt;/strong&gt; 全局事务中的“执行者”。通常就是底层的数据库（如 MySQL、Oracle）或消息队列（如 ActiveMQ），它们负责执行本地的真实事务，并听从 TM 的指令。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;XA 接口：&lt;/strong&gt; TM 和 RM 之间沟通的“标准语言”（基于两阶段提交的底层协议）。XA 接口是双向的，能在一个事务管理器和多个资源管理器之间形成通信桥梁，通过协调多个数据源的一致动作，实现全局事务的统一提交或统一回滚。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;jtajava-中的-xa-实现&#34;&gt;JTA：Java 中的 XA 实现
&lt;/h2&gt;&lt;p&gt;因为 XA 规范提出时还没有 Java 语言，所以 Java 后续自己提出了一套 API，叫 JTA（Java Transaction API，JSR 907）。JTA 就是 XA 规范在 Java 中的官方标准接口，它有两个主要的接口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;事务管理器的接口：&lt;/strong&gt; &lt;code&gt;javax.transaction.TransactionManager&lt;/code&gt;，用于 Java EE 服务器提供容器事务（由容器自动负责事务管理）；JTA 还提供了另外一套 &lt;code&gt;javax.transaction.UserTransaction&lt;/code&gt; 接口，用于通过程序代码手动开启、提交和回滚事务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;满足 XA 规范的资源定义接口：&lt;/strong&gt; &lt;code&gt;javax.transaction.xa.XAResource&lt;/code&gt;，任何资源（JDBC、JMS 等）如果想要支持 JTA，只要实现 &lt;code&gt;XAResource&lt;/code&gt; 接口中的方法即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JTA 原本是 Java EE 中的技术，一般情况下应该由 JBoss、WebSphere、WebLogic 这些 Java EE 容器来提供支持，但现在 Bitronix、Atomikos 和 JBossTM（以前叫 Arjuna）都是以 JAR 包的形式实现了 JTA 的接口，称为 JOTM（Java Open Transaction Manager，Java 开源事务管理器），使得我们也能够在 Tomcat、Jetty 这样的 Java SE 环境下使用 JTA。&lt;/p&gt;
&lt;h2 id=&#34;两阶段提交2pc&#34;&gt;两阶段提交（2PC）
&lt;/h2&gt;&lt;p&gt;为了解决全局事务中数据一致性的问题，XA 将事务提交拆分为两个阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;准备阶段：&lt;/strong&gt; 又叫做投票阶段，在这一阶段，协调者询问事务的所有参与者是否准备好提交，参与者如果已经准备好提交则回复 Prepared，否则回复 Non-Prepared。对数据库来说，准备操作是在重做日志中记录全部事务提交操作所需要的内容，它与本地事务中真正提交的区别只是暂不写入最后一条 Commit Record 而已，这就意味着在做完数据持久化后并不立即释放隔离性，仍继续持有锁，维持数据对其他非事务内观察者的隔离状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提交阶段：&lt;/strong&gt; 又叫做执行阶段，协调者如果在上一阶段收到所有事务参与者回复的 Prepared 消息，则先自己在本地持久化事务状态 Commit，然后向所有参与者发送 Commit 指令，让所有参与者立即执行提交操作；否则，只要任意一个参与者回复 Non-Prepared 消息，或任意一个参与者超时未回复，协调者就会在自己完成事务状态为 Abort 的持久化后，向所有参与者发送 Abort 指令，让参与者立即执行回滚操作。对于数据库来说，这个阶段的提交操作很轻量，仅仅是持久化一条 Commit Record 而已，通常能够快速完成；只有收到 Abort 指令时，才需要根据回滚日志清理已经提交的数据，这可能是相对重负载的操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上这两个过程被称为“两段式提交”（2 Phase Commit，2PC）协议，而它要想成功保持一致性还需要一些其他的前提条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;必须假设网络在提交阶段的短时间内是可靠的，即提交阶段不会丢失消息；同时也假设网络通信在全过程都不会出现误差，即可以丢失消息，但不会传递错误的消息——XA 的设计目标并不是解决诸如拜占庭将军一类的问题。在两阶段提交中，投票阶段失败了可以补救（回滚），提交阶段失败了则无法补救（不再改变提交或回滚的结果，只能等崩溃的节点重新恢复），因此该阶段耗时应该尽可能短，这也是在尽量控制网络风险。&lt;/li&gt;
&lt;li&gt;必须假设因为网络分区、机器崩溃或者其他原因而导致失联的节点最终能够恢复，不会永久性地处于失联状态。由于在准备阶段已经写入了完整的重做日志，所以失联机器一旦恢复，就能够从日志中找出已经准备妥当但并未提交的事务数据，进而向协调者查询该事务的状态，确定下一步应该进行提交还是回滚操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面所说的协调者、参与者通常都是由数据库自己来扮演的，不需要应用程序介入，协调者一般是在参与者之间选举产生，而应用程序对于数据库来说只扮演客户端的角色。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://lyblogs.xyz/p/%E5%85%A8%E5%B1%80%E4%BA%8B%E5%8A%A1/2PC.jpg&#34;
	width=&#34;341&#34;
	height=&#34;343&#34;
	srcset=&#34;https://lyblogs.xyz/p/%E5%85%A8%E5%B1%80%E4%BA%8B%E5%8A%A1/2PC_hu_95f512af43d5c956.jpg 480w, https://lyblogs.xyz/p/%E5%85%A8%E5%B1%80%E4%BA%8B%E5%8A%A1/2PC_hu_305a40b049464ee7.jpg 1024w&#34;
	loading=&#34;lazy&#34;
	
		alt=&#34;两段式提交的交互时序示意图&#34;
	
	
		class=&#34;gallery-image&#34; 
		data-flex-grow=&#34;99&#34;
		data-flex-basis=&#34;238px&#34;
	
&gt;&lt;/p&gt;
&lt;h3 id=&#34;两阶段提交存在的问题&#34;&gt;两阶段提交存在的问题
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单点问题：&lt;/strong&gt; 协调者在两段式提交中具有举足轻重的作用。协调者等待参与者回复时可以有超时机制，允许参与者宕机，但参与者等待协调者指令时无法做超时处理——一旦宕机的不是某个参与者，而是协调者本身，所有参与者都会受到影响。如果协调者一直没有恢复，没有正常发送 Commit 或 Rollback 指令，那所有参与者都必须一直等待。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能问题：&lt;/strong&gt; 在两段式提交过程中，所有参与者相当于被绑定为一个统一调度的整体，期间要经过两次远程服务调用、三次数据持久化（准备阶段写重做日志，协调者做状态持久化，提交阶段在日志写入提交记录），整个过程要持续到参与者集群中最慢的那一个处理操作结束为止，这决定了两段式提交的性能通常较差。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一致性风险：&lt;/strong&gt; 前面已经提到，两段式提交的成立是有前提条件的，当网络稳定性和宕机恢复能力的假设不成立时，仍可能出现一致性问题。“FLP 不可能原理”证明了如果宕机最终不能恢复，那就不存在任何一种分布式协议可以正确地达成一致性结果。网络稳定性带来的一致性风险指的是：尽管提交阶段时间很短，但这仍是一段明确存在的危险期——如果协调者在发出准备指令后，根据收到的各参与者回复确定事务状态是可以提交的，于是先持久化事务状态并提交了自己的事务，这时候如果网络忽然断开，无法再向所有参与者发出 Commit 指令，就会导致部分数据（协调者的）已提交，但部分数据（参与者的）未提交，并且没有办法回滚，产生数据不一致的问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;三阶段提交3pc&#34;&gt;三阶段提交（3PC）
&lt;/h2&gt;&lt;p&gt;为了缓解两阶段提交中协调者的单点问题和准备阶段的性能问题，后续又发展出了“三段式提交”（3 Phase Commit，3PC）协议。三段式提交把原本两段式提交的准备阶段再细分为两个阶段，分别称为 CanCommit、PreCommit，把提交阶段改称为 DoCommit 阶段。其中新增的 CanCommit 是一个询问阶段，即协调者让每个参与的数据库根据自身状态，评估该事务是否有可能顺利完成。将准备阶段一分为二的理由是：准备阶段是重负载的操作，一旦协调者发出开始准备的消息，每个参与者都将马上开始写重做日志，其所涉及的数据资源即被锁住，如果此时某一个参与者宣告无法完成提交，相当于大家都做了一轮无用功。所以增加一轮询问阶段，如果都得到了正面的响应，那事务能够成功提交的把握就比较大，这也意味着因某个参与者提交时发生崩溃而导致大家全部回滚的风险相对变小。因此，在事务需要回滚的场景中，三段式提交的性能通常比两段式提交要好很多；但在事务能正常提交的场景中，两者性能都很差，甚至三段式因为多了一次询问，还要稍微更差一些。&lt;/p&gt;
&lt;p&gt;同样也是由于事务失败回滚的概率变小，在三段式提交中，如果 PreCommit 阶段之后发生了协调者宕机，即参与者没有等到 DoCommit 消息，默认操作策略将是提交事务而不是回滚事务或继续等待，这就相当于避免了协调者单点问题的风险。三段式提交对单点问题和回滚时的性能问题有所改善，但对一致性风险问题并未有任何改进，甚至是略有增加。譬如，进入 PreCommit 阶段之后，协调者发出的指令不是 Ack 而是 Abort，而此时因网络问题，有部分参与者直至超时都未能收到协调者的 Abort 指令，这些参与者将会错误地提交事务，这就产生了不同参与者之间数据不一致的问题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://lyblogs.xyz/p/%E5%85%A8%E5%B1%80%E4%BA%8B%E5%8A%A1/3PC.jpg&#34;
	width=&#34;432&#34;
	height=&#34;546&#34;
	srcset=&#34;https://lyblogs.xyz/p/%E5%85%A8%E5%B1%80%E4%BA%8B%E5%8A%A1/3PC_hu_82fda82fa8d2e457.jpg 480w, https://lyblogs.xyz/p/%E5%85%A8%E5%B1%80%E4%BA%8B%E5%8A%A1/3PC_hu_a036d49c1858d86.jpg 1024w&#34;
	loading=&#34;lazy&#34;
	
		alt=&#34;三段式提交的交互时序示意图&#34;
	
	
		class=&#34;gallery-image&#34; 
		data-flex-grow=&#34;79&#34;
		data-flex-basis=&#34;189px&#34;
	
&gt;&lt;/p&gt;
</description>
        </item>
        <item>
        <title>数据库如何实现隔离性</title>
        <link>https://lyblogs.xyz/p/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E9%9A%94%E7%A6%BB%E6%80%A7/</link>
        <pubDate>Mon, 07 Sep 2026 10:07:54 +0800</pubDate>
        
        <guid>https://lyblogs.xyz/p/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E9%9A%94%E7%A6%BB%E6%80%A7/</guid>
        <description>&lt;img src="https://lyblogs.xyz/p/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E9%9A%94%E7%A6%BB%E6%80%A7/mariya.jpg" alt="Featured image of post 数据库如何实现隔离性" /&gt;&lt;p&gt;隔离性保证了每个事务各自读、写的数据互相独立，不会彼此影响。只从定义上就能嗅出隔离性肯定与并发密切相关——如果没有并发，所有事务都是串行执行的，那就不需要任何隔离，或者说这样的访问天然具备了隔离性。而在并发情况下要实现串行般的数据访问效果，就需要靠加锁来同步。现代数据库均提供了以下三种锁：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;写锁（Write Lock，也叫排他锁，eXclusive Lock，简写为X-Lock）：&lt;/strong&gt; 数据一旦被加上写锁，就只有持有写锁的事务才能对其进行写入操作；数据被加持着写锁期间，其他事务既不能写入，也不能再施加读锁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;读锁（Read Lock，也叫共享锁，Shared Lock，简写为S-Lock）：&lt;/strong&gt; 多个事务可以对同一份数据同时加上多个读锁，数据一旦被加上读锁就不能再被加写锁，因此其他事务不能写入该数据，但仍然可以读取。对于持有读锁的事务而言，如果该数据当前只有它自己一个事务加了读锁，则允许直接将其升级为写锁，再写入数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;范围锁（Range Lock）：&lt;/strong&gt; 直接对某个范围加排他锁，范围内的数据都不能被写入。下面是一个典型的加范围锁的例子：
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-SQL&#34; data-lang=&#34;SQL&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;books&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;price&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FOR&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;UPDATE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;需要注意“范围不能被写入”与“一批数据不能被写入”的差别，不要把范围锁理解成一组排他锁的集合。加了范围锁后，不仅不能修改该范围内已有的数据，也不能在该范围内新增或删除任何数据——而后者是一组排他锁的集合无法做到的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;两阶段锁协议与可串行化&#34;&gt;两阶段锁协议与可串行化
&lt;/h2&gt;&lt;p&gt;串行化访问提供了最高强度的隔离性，SQL-92 中定义的最高等级隔离级别便是可串行化（Serializable）。可串行化的直观理解完全符合普通程序员对数据竞争加锁的认知：在不考虑性能优化的条件下，对事务所有读、写涉及的数据全都加上读锁、写锁和范围锁即可做到可串行化。但实际上数据库不能在事务执行过程中随心所欲地加锁、解锁，必须严格遵循“两阶段”原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;加锁阶段（Expanding）：&lt;/strong&gt; 事务刚开始时，只能不断申请新锁，绝对不能释放任何已持有的锁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解锁阶段（Shrinking）：&lt;/strong&gt; 一旦开始释放第一个锁，就只能继续释放，绝对不能再申请任何新锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样做是为了防止多个并发事务在交叉加锁时产生逻辑错乱或死锁，确保最终执行的结果真正等价于“串行排队”。&lt;/p&gt;
&lt;h2 id=&#34;隔离级别在隔离性与吞吐量之间取舍&#34;&gt;隔离级别：在隔离性与吞吐量之间取舍
&lt;/h2&gt;&lt;p&gt;数据库不考虑性能显然是不行的，并发控制（Concurrency Control）理论决定了隔离程度与并发能力是相互抵触的——隔离程度越高，并发访问时的吞吐量就越低。&lt;/p&gt;
&lt;p&gt;正因如此，现代数据库一定会提供可串行化以外的其他隔离级别供用户选用，让用户自主调节数据库的加锁方式，在隔离性与吞吐量之间取得平衡。&lt;/p&gt;
&lt;h3 id=&#34;可重复读幻读问题&#34;&gt;可重复读：幻读问题
&lt;/h3&gt;&lt;p&gt;可串行化的下一个隔离级别是可重复读（Repeatable Read）。可重复读对事务所涉及的数据加读锁和写锁，且一直持有至事务结束，但不再加范围锁。它比可串行化弱化的地方在于幻读问题（Phantom Read）——指事务执行过程中，两个完全相同的范围查询得到了不同的结果集。下面是一个例子：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;count&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;books&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;price&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;cm&#34;&gt;/* 时间顺序：1，事务：T1  */&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;INSERT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;INTO&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;books&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;price&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;VALUE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;深入理解Java虚拟机&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;90&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;cm&#34;&gt;/* 时间顺序：2  事务： T2 */&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;count&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;books&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;price&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;cm&#34;&gt;/* 时间顺序：3 事务：T1 */&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;假如一条 SQL 语句在同一个事务中重复执行了两次，且这两次执行之间恰好有另一个事务向数据库插入了一本小于 100 元的书，这是被允许的，于是这两次相同的 SQL 查询就会得到不一样的结果。原因是可重复读没有范围锁来禁止在该范围内插入新数据，这正是一个事务受到其他事务影响、隔离性被破坏的表现。&lt;/p&gt;
&lt;p&gt;需要注意的是，这里介绍的是以 ARIES 理论为讨论目标，具体的数据库不一定要完全遵照理论去实现。一个例子是 MySQL/InnoDB 的默认隔离级别为可重复读，但它在只读事务中可以完全避免幻读问题——譬如上面例子中若事务 T1 只是一个只读事务，上述问题在 MySQL 中并不会出现。但在读写事务中，MySQL 仍然会出现幻读问题：譬如例子中事务 T1 如果在其他事务插入新书后，不是重新查询一次数量，而是将所有小于 100 元的书改名，那就依然会受到插入书的影响。&lt;/p&gt;
&lt;h3 id=&#34;读已提交不可重复读问题&#34;&gt;读已提交：不可重复读问题
&lt;/h3&gt;&lt;p&gt;可重复读的下一个隔离级别是读已提交（Read Committed）。读已提交对事务涉及的数据加的写锁会一直持续到事务结束，但加的读锁在查询操作完成后会马上释放。它比可重复读弱化的地方在于不可重复读问题（Non-Repeatable Read）——指事务执行过程中，对同一数据的两次查询得到了不同的结果。譬如想要获取《深入理解Java虚拟机》这本书的售价，同样执行了两条 SQL 语句，在这两条语句执行之间，恰好有另一个事务将这本书的价格从 90 元修改为 110 元，如下所示：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;books&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;cm&#34;&gt;/*时间顺序：1， 事务：T1 */&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;UPDATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;books&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SET&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;price&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;110&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;COMMIT&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;cm&#34;&gt;/*时间顺序：2，事务：T2*/&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;books&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;COMMIT&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;cm&#34;&gt;/*时间顺序：3，事务：T1 */&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果隔离级别是读已提交，这两次重复执行的查询结果就会不一样，原因是读已提交的隔离级别缺乏贯穿整个事务周期的读锁，无法禁止读取过的数据发生变化，于是事务 T2 中的更新语句可以马上提交成功，这同样是一个事务受到其他事务影响、隔离性被破坏的表现。假如隔离级别是可重复读，由于数据已被事务 T1 施加了读锁且读取后不会马上释放，事务 T2 便无法获取到写锁，更新就会被阻塞，直至事务 T1 提交或回滚后才能提交。&lt;/p&gt;
&lt;h3 id=&#34;读未提交脏读问题&#34;&gt;读未提交：脏读问题
&lt;/h3&gt;&lt;p&gt;读已提交的下一个级别是读未提交（Read Uncommitted），它只会对事务涉及的数据加写锁，且一直持续到事务结束，但完全不加读锁。它比读已提交弱化的地方在于脏读问题（Dirty Read）——指事务执行过程中，一个事务读取到了另一个事务尚未提交的数据。譬如下面的例子：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;books&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;cm&#34;&gt;/*时间顺序： 1，事务：T1 */&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cm&#34;&gt;/*注意这里没有COMMIT*/&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;UPDATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;books&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SET&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;price&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;90&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;cm&#34;&gt;/*时间顺序：2，事务：T2 */&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;books&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;cm&#34;&gt;/*时间顺序：3，事务：T1 */&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ROLLBACK&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt;/*时间顺序：4，事务：T2 */&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在这个例子中，事务 T1 两次读取的数据并不一致。原因是读未提交在数据上完全不加读锁，这反而让 T1 能读到其他事务加了写锁的数据，即事务 T1 中两条查询语句得到的结果并不相同。这里要注意写锁的定义：写锁禁止其他事务施加读锁，而不是禁止事务读取数据——如果事务 T1 读取数据前并不需要加读锁，就会导致事务 T2 尚未提交的数据也马上能被事务 T1 读到。这同样是一个事务受到其他事务影响、隔离性被破坏的表现。假如隔离级别是读已提交，由于事务 T2 持有数据的写锁，事务 T1 的第二次查询就无法获得读锁，而读已提交级别要求先加读锁后读数据，因此 T1 中的查询就会被阻塞，直至事务 T2 提交或回滚后才能得到结果。&lt;/p&gt;
&lt;p&gt;理论上还存在更低的隔离级别，即“完全不隔离”——读、写锁都不加。读未提交会有脏读问题，但不会有脏写（Dirty Write），也就是一个事务提交之前的修改被另一个事务的修改覆盖掉的问题。脏写已经不单纯是隔离性上的问题了，它会导致事务的原子性都无法实现，所以一般谈论隔离级别时不会将完全不隔离纳入讨论范围，而是把读未提交视为最低级别的隔离级别。&lt;/p&gt;
&lt;p&gt;其实不同隔离级别以及幻读、不可重复读、脏读等问题都只是表面现象，是各种锁在不同加锁时机上组合应用所产生的结果——以锁为手段来实现隔离性，才是数据库表现出不同隔离性的根本原因。&lt;/p&gt;
&lt;h2 id=&#34;mvcc无锁的读优化&#34;&gt;MVCC：无锁的读优化
&lt;/h2&gt;&lt;p&gt;除了都以锁来实现之外，以上四种隔离级别还有另一个共同特点：幻读、不可重复读、脏读等问题都是由于一个事务在读数据的过程中，受到另一个写数据的事务影响而破坏了隔离性。针对这种“一个事务读 + 另一个事务写”的隔离问题，近年来有一种名为“多版本并发控制”（Multi-Version Concurrency Control, MVCC）的无锁优化方案被主流商业数据库广泛采用。&lt;/p&gt;
&lt;p&gt;MVCC 是一种读取优化策略，它的“无锁”特指读取时不需要加锁。其基本思路是：对数据库的任何修改都不会直接覆盖之前的数据，而是产生一个新版本与老版本共存，以此达到读取时可以完全不加锁的目的。这里的“版本”可以理解为数据库中每一行记录都存在两个看不见的字段——&lt;code&gt;CREATE_VERSION&lt;/code&gt; 和 &lt;code&gt;DELETE_VERSION&lt;/code&gt;，这两个字段记录的值都是事务 ID（一个全局严格递增的数值），并按以下规则写入数据：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;插入数据时：&lt;/strong&gt; &lt;code&gt;CREATE_VERSION&lt;/code&gt; 记录插入数据的事务 ID，&lt;code&gt;DELETE_VERSION&lt;/code&gt; 为空。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;删除数据时：&lt;/strong&gt; &lt;code&gt;DELETE_VERSION&lt;/code&gt; 记录删除数据的事务 ID，&lt;code&gt;CREATE_VERSION&lt;/code&gt; 为空。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修改数据时：&lt;/strong&gt; 将修改视为“删除旧数据，插入新数据”的组合，即先将原有数据复制一份：原有数据的 &lt;code&gt;DELETE_VERSION&lt;/code&gt; 记录修改数据的事务 ID，&lt;code&gt;CREATE_VERSION&lt;/code&gt; 为空；复制出的新数据则是 &lt;code&gt;CREATE_VERSION&lt;/code&gt; 记录修改数据的事务 ID，&lt;code&gt;DELETE_VERSION&lt;/code&gt; 为空。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此时，如果另有一个事务要读取这些发生了变化的数据，将根据隔离级别决定到底应该读取哪个版本：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;隔离级别是可重复读：&lt;/strong&gt; 总是读取 &lt;code&gt;CREATE_VERSION&lt;/code&gt; 小于或等于当前事务 ID 的记录，在此前提下如果数据仍有多个版本，则取最新（事务 ID 最大）的一个。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隔离级别是读已提交：&lt;/strong&gt; 直接取最新版本即可，即最近被提交的那个版本的数据记录。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外两个隔离级别都没有必要用到 MVCC：读未提交直接修改原始数据即可，其他事务查看数据时可以立刻看到，根本无需版本字段；可串行化本来的语义就是要阻塞其他事务的读取操作，而 MVCC 是做读取时无锁优化的，两者自然不会放在一起用。&lt;/p&gt;
&lt;h2 id=&#34;乐观锁与悲观锁写--写场景的选择&#34;&gt;乐观锁与悲观锁：写 + 写场景的选择
&lt;/h2&gt;&lt;p&gt;MVCC 只是针对“读 + 写”场景的优化，如果是两个事务同时修改数据，即“写 + 写”的情况，就没有多少优化空间了，此时加锁几乎是唯一可行的解决方案。稍微有点讨论余地的是加锁策略该选“乐观锁”（Optimistic Locking）还是“悲观锁”（Pessimistic Locking）。&lt;/p&gt;
&lt;p&gt;前面介绍的加锁策略都属于悲观加锁策略，即认为如果不先加锁再访问数据，就肯定会出问题。相对地，乐观加锁策略认为事务之间数据存在竞争只是偶然情况，没有竞争才是普遍情况，因此不应该在一开始就加锁，而应该在出现竞争时再采取补救措施，这种思路也被称为“乐观并发控制”（Optimistic Concurrency Control, OCC）。最后，没有必要迷信“乐观锁比悲观锁更快”的说法——这纯粹取决于竞争的激烈程度，如果竞争激烈，乐观锁反而会更慢。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;参考自《凤凰架构：构建可靠的大型分布式系统》周志明&lt;/p&gt;
&lt;/blockquote&gt;
</description>
        </item>
        <item>
        <title>数据库如何实现原子性和持久性</title>
        <link>https://lyblogs.xyz/p/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%8E%9F%E5%AD%90%E6%80%A7%E5%92%8C%E6%8C%81%E4%B9%85%E6%80%A7/</link>
        <pubDate>Mon, 31 Aug 2026 17:48:36 +0800</pubDate>
        
        <guid>https://lyblogs.xyz/p/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%8E%9F%E5%AD%90%E6%80%A7%E5%92%8C%E6%8C%81%E4%B9%85%E6%80%A7/</guid>
        <description>&lt;img src="https://lyblogs.xyz/p/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%8E%9F%E5%AD%90%E6%80%A7%E5%92%8C%E6%8C%81%E4%B9%85%E6%80%A7/pexels.jpg" alt="Featured image of post 数据库如何实现原子性和持久性" /&gt;&lt;p&gt;数据库中的原子性保证了事务的多个操作要么全部生效，要么全部不生效，不会存在中间状态；持久性则保证了事务一旦生效，就不会因为任何原因导致其修改的内容被撤销或丢失。&lt;/p&gt;
&lt;p&gt;数据必须成功写入磁盘、磁带等持久化存储器之后才拥有持久性，只存储在内存中的数据，一旦遇到程序崩溃，或者数据库、操作系统崩溃，甚至机器突然断电宕机，就会丢失。&lt;/p&gt;
&lt;h2 id=&#34;问题的根源写入并非原子操作&#34;&gt;问题的根源：写入并非原子操作
&lt;/h2&gt;&lt;p&gt;实现原子性和持久性最大的困难在于，“写入磁盘”这个操作本身并不是原子的：它不仅有“已写入”与“未写入”两种状态，客观上还存在“正在写”的中间状态。由于这种中间状态与崩溃都无法消除，所以不做额外保障措施的话，仅仅把内存中的数据写入磁盘，并不能保证原子性与持久性。&lt;/p&gt;
&lt;p&gt;举个例子，当我们购买一本书时需要修改三处数据：在用户账户中扣减货款、在商家账户中增加货款、在商品仓库中将这本书标记为配送状态。由于写入存在中间状态，可能出现以下情形：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;未提交事务，写入后崩溃：&lt;/strong&gt; 程序还没修改完三处数据，但数据库已经把其中一两处的变动写入了磁盘。此时若发生崩溃，重启后数据库必须能够识别出崩溃前存在一次不完整的购物操作，把已经修改过的数据从磁盘中恢复成未修改前的样子，以保证原子性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;已提交事务，写入前崩溃：&lt;/strong&gt; 程序已经修改完三处数据，但数据库还没把全部变动写入磁盘。此时若发生崩溃，重启后数据库必须能够识别出崩溃前存在一次完整的购物操作，把还没来得及写入磁盘的那部分数据重新写入，以保证持久性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于写入的中间状态与崩溃都无法避免，要保证原子性和持久性，就只能在崩溃发生后采取恢复的补救措施，这种数据恢复操作被称为“崩溃恢复”（Crash Recovery，也有资料称作 Failure Recovery 或 Transaction Recovery）。&lt;/p&gt;
&lt;h2 id=&#34;commit-logging先写日志再改数据&#34;&gt;Commit Logging：先写日志，再改数据
&lt;/h2&gt;&lt;p&gt;为了顺利完成崩溃恢复，向磁盘写入数据就不能像程序修改内存变量那样，直接改变某表某行某列的值，而是必须把修改数据这一操作所需的全部信息——包括修改了什么数据、数据物理上位于哪个内存页和磁盘块、从什么值改成什么值等——以日志的形式（即仅进行顺序追加的文件写入）先记录到磁盘中。只有当日志记录全部安全落盘，数据库在日志中看到代表事务成功提交的“提交记录”（Commit Record）后，才会依据日志信息对真正的数据进行修改；修改完成后，再在日志中追加一条“结束记录”（End Record），表示该事务已完成持久化。这种事务实现方法被称为“提交日志”（Commit Logging）。&lt;/p&gt;
&lt;p&gt;Commit Logging 保障数据持久性、原子性的原理是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;日志一旦成功写入 Commit Record，整个事务就宣告成功。即便真正修改数据时发生崩溃，重启后也可以根据已经落盘的日志信息恢复现场、继续完成数据修改，这保证了&lt;strong&gt;持久性&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果日志没有成功写入 Commit Record 就发生了崩溃，那么整个事务就是失败的。系统重启后会看到一部分没有 Commit Record 的日志，将这部分日志标记为回滚状态即可，整个事务就像完全没有发生过一样，这保证了&lt;strong&gt;原子性&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Commit Logging 的缺陷在于：所有对数据的真实修改都必须发生在事务提交（即日志写入 Commit Record）之后。在此之前，无论磁盘 I/O 多么空闲，无论某个事务修改的数据量有多庞大、占用了多少内存缓冲区，都绝不允许提前把数据改到磁盘上——这是 Commit Logging 成立的前提，却也对数据库性能十分不利。&lt;/p&gt;
&lt;h2 id=&#34;write-ahead-loggingforce-与-steal&#34;&gt;Write-Ahead Logging：FORCE 与 STEAL
&lt;/h2&gt;&lt;p&gt;为了解决这个问题，ARIES 提出了“提前写入日志”（Write-Ahead Logging）的改进方案。所谓“提前写入”（Write-Ahead），就是允许在事务提交之前写入变动数据。Write-Ahead Logging 按照事务提交的时点，把何时写入变动数据划分为 FORCE 和 STEAL 两类情况。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FORCE：&lt;/strong&gt; 当事务提交后，要求变动数据必须同时完成写入，称为 FORCE；不强制同时完成写入，则称为 NO-FORCE。现实中绝大多数数据库采用的都是 NO-FORCE 策略，因为只要有了日志，变动数据随时都可以持久化，从优化磁盘 I/O 性能的角度出发，没有必要强制数据写入立即完成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;STEAL：&lt;/strong&gt; 在事务提交前，允许变动数据提前写入，称为 STEAL；不允许则称为 NO-STEAL。从优化磁盘 I/O 性能的角度出发，允许数据提前写入有利于利用空闲 I/O 资源，也有利于节省数据库缓冲区的内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Commit Logging 允许 NO-FORCE，但不允许 STEAL：因为一旦事务提交前就有部分变动数据写入磁盘，那么事务回滚或崩溃发生时，这些提前写入的数据就成了错误数据。&lt;/p&gt;
&lt;p&gt;Write-Ahead Logging 则同时允许 NO-FORCE 和 STEAL，它给出的解决办法是增加了另一种日志类型——Undo Log。当变动数据写入磁盘前，必须先记录 Undo Log，注明修改了哪个位置的数据、从什么值改成什么值等，以便在事务回滚或崩溃恢复时依据 Undo Log 把提前写入的数据变动擦除掉。Undo Log 现在一般译作“回滚日志”，此前用于崩溃恢复时重演数据变动的日志相应被命名为 Redo Log，一般译作“重做日志”。&lt;/p&gt;
&lt;h2 id=&#34;崩溃恢复的三个阶段&#34;&gt;崩溃恢复的三个阶段
&lt;/h2&gt;&lt;p&gt;由于引入了 Undo Log，Write-Ahead Logging 在崩溃恢复时会经历以下三个阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;分析阶段（Analysis）：&lt;/strong&gt; 从最后一次检查点（Checkpoint，可理解为在此之前所有应该持久化的变动都已安全落盘）开始扫描日志，找出所有没有 End Record 的事务，组成待恢复的事务集合。这个集合至少包含事务表（Transaction Table）和脏页表（Dirty Page Table）两部分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重做阶段（Redo）：&lt;/strong&gt; 依据分析阶段产生的待恢复事务集合来重演历史（Repeat History），具体做法是找出包含 Commit Record 的日志，把这些日志所修改的数据写入磁盘，写入完成后在日志中追加一条 End Record，然后将该事务移出待恢复集合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回滚阶段（Undo）：&lt;/strong&gt; 处理经过分析、重做阶段后剩余的恢复事务集合。此时剩下的都是需要回滚的事务，称为 Loser，根据 Undo Log 中的信息，把已经提前写入磁盘的数据改写回去，以达到回滚这些 Loser 事务的目的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;重做阶段和回滚阶段的操作都应设计为幂等的。为了追求高 I/O 性能，以上三个阶段不可避免地涉及非常繁琐的概念和细节（如 Undo Log、Redo Log 的具体数据结构等）。Write-Ahead Logging 是 ARIES 理论的一部分，整套 ARIES 拥有严谨、高性能等诸多优点，但这些都是以高度复杂为代价换来的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;参考自《凤凰架构：构建可靠的大型分布式系统》周志明&lt;/p&gt;
&lt;/blockquote&gt;
</description>
        </item>
        
    </channel>
</rss>
