我们首先得理解什么是分布式事务呢?分布式事务是指在分布式系统中,涉及多个计算机或服务器的操作序列,这些操作需要满足一致性和可靠性的要求。每个操作要么全部成功执行,要么全部回滚,以保持数据的一致性和完整性。
OK,概念讲完,我们知道一个东西的出现一定是为了解决一些问题的,那么分布式事务解决了我们什么样的难题呢,而它又是如何解决的呢?
- 数据一致性问题:当数据分布在不同的节点上,多个并发操作可能会导致数据不一致。分布式事务通过提供原子性和一致性,确保在分布式系统中进行的操作保持数据一致性,避免数据错误和不一致。
- 并发冲突问题:在分布式环境中,多个并发操作可能会相互干扰,导致数据竞争和冲突。分布式事务的隔离性能力确保并发操作之间相互隔离,防止数据冲突,确保操作的正确执行。
- 故障容忍和持久性问题:分布式系统中的节点可能面临故障或断电等问题。分布式事务通过持久性的特性,确保一旦事务成功提交,其结果将被永久保存,即使在系统故障或网络中断的情况下也不会丢失。
所以说,事务通过原子性、一致性、隔离性、持久性这几大特点,保障数据一致性和完整。性防止并发操作导致的数据竞争和冲突 提供故障容忍和持久性,确保操作的安全性和可靠性
如果你还对这些东西感到晕头转向,那么让我给你一个简单的场景吧,让你对它有更加深刻的理解
当你在网上购物时,使用信用卡进行支付涉及多个步骤和参与者,包括你、商家、支付网关和银行系统。在这个过程中,分布式事务的功能起到以下作用:
- 原子性:如果交易成功,整个支付过程要么全部成功执行,要么全部回滚。例如,如果商家没有库存或者支付网关出现问题,整个支付过程将被取消,避免了部分支付导致的数据不一致性。
- 一致性:分布式事务确保支付过程中的各个环节符合预定义的一致性规则。例如,商家应该从库存中减少商品数量,支付网关应该扣除相应的金额,并向银行发起付款请求。这种一致性保证了交易的正确执行,并避免了数据错误或矛盾的发生。
- 隔离性:分布式事务确保你的支付操作与其他用户的支付操作相互隔离,不会相互干扰。这意味着你的支付过程不会与其他用户的支付过程产生冲突,每个支付操作都独立执行,保证了数据的正确性和独立性。
- 持久性:一旦支付成功提交,分布式事务确保支付结果被永久保存。无论在支付过程中发生了什么,一旦支付成功,金额将从你的账户扣除并转入商家的账户。这种持久性保证了支付数据的安全存储,即使在网络故障或系统崩溃的情况下也能够恢复数据并保持一致性。
你看!一个我们日常中最常见的购物操作,都会涉及到事务的处理,所以话不多说,赶紧来了解一下事务到底是什么吧!!
事务的隔离级别
隔离级别是指在并发环境下,数据库系统为了处理事务之间可能发生的相互干扰问题而采取的一种机制。
如果想了解事务的隔离级别这个概念,让我们先来问几个问题,这些问题可能你们在生产中也会遇到
1、你是否遇到过在一个事务中读取到了另一个事务尚未提交的数据?比如发送一个消息,后端需要先更新消息明细列表数据,并且在统计表中对用户未读数的统计+1,但查询时,发现明细列表有数据了,但未读数怎么没+1呢
2、你是否曾经在同一个事务中多次读取同一数据时得到了不一致的结果?
3、你是否遇到过在同一个事务中执行了一次范围查询,然后再次执行相同查询时结果却不一样?
上面这几个问题就代表着我们可能遇到;脏读、不可重复读、幻读
那么什么又是脏读、不可重复读、幻读呢?
- 脏读(Dirty Read):某个事务读取了另一个事务尚未提交的数据。这可能导致读取到不正确的数据,因为尚未提交的数据可能会被回滚,从而造成脏读。
- 不可重复读(Non-repeatable Read):在同一个事务中,某个数据多次读取的结果不一致。这是由于其他事务在该事务读取数据期间修改了数据,导致不一致的读取结果。
- 幻读(Phantom Read):在同一个事务中,某个范围查询多次执行的结果不一致。这是由于其他事务在该事务范围查询期间插入、删除或更新了符合查询条件的数据,导致出现新增或消失的数据行。
了解这些问题,我们再来看下事务隔离是如何解决这些问题的:
- 读未提交(Read Uncommitted):最低级别的隔离级别,允许事务读取尚未提交的数据。它无法解决脏读、不可重复读和幻读问题。
- 读已提交(Read Committed):在事务提交后才允许其他事务读取数据。它解决了脏读问题,但仍可能遇到不可重复读和幻读问题。
- 可重复读(Repeatable Read):事务执行期间保持一致的快照视图,其他事务无法修改已读取的数据。它解决了脏读和不可重复读问题,但仍可能遇到幻读问题。
- 串行化(Serializable):最高级别的隔离级别,确保事务串行执行,避免了脏读、不可重复读和幻读问题。它通过对事务加锁来实现串行化执行,但可能影响系统的并发性能。
每个隔离级别在提供数据一致性和隔离性方面都有不同的权衡和性能影响。根据应用程序的需求和对数据一致性的要求,我们可以选择合适的隔离级别来平衡并发性能和数据的正确性。
读已提交
最基本的事务隔离级别是读已提交(Read Committed),它提供了两个保证:
- 从数据库读时,只能看到已提交的数据(没有脏读(dirty reads))。
- 写入数据库时,只会覆盖已经写入的数据(没有脏写(dirty writes))。
没有脏读
在读已提交隔离级别运行的事务必须防止脏读,就是一个事物只能读取另一提交的事务。这意味着事务的任何写入操作只有在该事务提交时才能被其他人看到。
为什么要防止脏读呢?
- 如果事务需要更新多个对象,脏读取意味着另一个事务可能会只看到一部分更新。就像我前文中举的例子,用户看到了未读数的差异
- 如果数据库允许脏读,那就意味着一个事务可能会看到稍后需要回滚的数据,即从未实际提交给数据库的数据。 想想后果就让人头大。
没有脏写
如果先前的写入是尚未提交事务的一部分,又会发生什么情况,后面的写入会覆盖一个尚未提交的值?这被称作脏写。
举一个简单的例子,Alice和Bob两个人同时试图购买同一辆车。购买汽车需要两次数据库写入:网站上的商品列表需要更新,以反映买家的购买,销售发票需要发送给买家。在 图7-5的情况下,销售是属于Bob的(因为他成功更新了商品列表),但发票却寄送给了 Alice(因为她成功更新了发票表)。读已提交会阻止这样这样的问题。
如何实现读已提交呢?
最常见的情况是,数据库通过使用行锁(row-level lock)来防止脏写:当事务想要修改特定对象(行或文档)时,它必须首先获得该对象的锁。然后必须持有该锁直到事务被提交或中止。
但用锁的问题也很明显,可能会因为长事务导致其他事务等待,导致响应时间增加。
出于以上的原因,所以有了一种新的方案:对于写入的每个对象,数据库都会记住旧的已提交值,和由当前持有写入锁的事务设置的新值。 当事务正在进行时,任何其 他读取对象的事务都会拿到旧值。 只有当新值提交后,事务才会切换到读取新值。(类比于CAS无锁化)
我们如何用代码实现呢?
- 显式设置隔离级别:可以在数据库连接或事务开始时显式设置隔离级别为读已提交。具体的方法取决于你使用的数据库系统和编程语言。比如,在SQL Server中,可以使用下面的语句设置隔离级别为读已提交:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
在Java中,可以使用以下代码设置隔离级别为读已提交:
Connection connection = DriverManager.getConnection(url, username, password);
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
快照隔离和可重复读
然而读已提交并不是完美的,我们来看另外一种场景
Alice在银行有1000美元的储蓄,分为两个账户,每个500美元。现在一笔事务从她的一个账户中转移了100美元到另一个账户。如果她在事务处理的同时查看其账户余额列表,不幸地在 转账事务完成前看到收款账户余额(余额为500美元),而在转账完成后看到另一个转出账户 (已经转出100美元,余额400美元)。对Alice来说,现在她的账户似乎只有900美元—— 看起来100美元已经消失了。
当然,你可能会说只需要等一小会,重新刷新一下,就会好了,但对于用户来说,这种场景还是蛮惊悚的
要解决这个问题,我们可以想到,让Alice在查询数据时,保持“数据静止”不就行了,这就是我们快照隔离的解决方案:
每个事务都从数据库的一致快照(consistent snapshot)中读取——也就是说,事务可以看到事务开始 时在数据库中提交的所有数据。即使这些数据随后被另一个事务更改,每个事务也只能看到 该特定时间点的旧数据。
那么,我们改如何去实现快照隔离呢?
从性能的角度来看,快照隔离的一个关键原则是:读不阻塞写,写不阻塞读。数据库在处理一致性快照上的长时间查询时,可以正常地同时处理写入操作。且两者间没有任何锁定争用。
我们的答案是多版本并发控制(MVCC, multi-version concurrentcy control)
我们用一个简单的比喻来描述什么是快照隔离,来更好理解MVCC:
假设你和你的朋友共享一个电子相册,其中包含了你们的旅行照片。每当你上传新的照片时,系统会为该照片创建一个时间戳,以便记录照片的版本。现在,假设你的朋友在某个时间点查看了你们的相册,并记住了这个时间点。然后你添加了一张新照片到相册中。
在MVCC机制下,当你的朋友再次查看相册时,他只能看到在他最初查看相册时已经存在的照片,而看不到你后来添加的照片。这是因为他的时间点早于新照片的时间戳,所以他只能看到旧版本的相册。
通过这个例子,我们可以更好地理解MVCC机制的工作原理。每个事务(每个人查看相册)使用自己的时间点(时间戳)来选择合适的数据版本(相册的内容),以保证数据的一致性和隔离性。这样,即使其他事务(其他人)对数据进行了修改,只要它们的时间点晚于当前事务开始的时间点,当前事务仍然可以读取到旧版本的数据,从而实现了可重复读。
持快照隔离的存储引擎通常也使用MVCC来实现读已提交隔离级别。很简单,已提交隔离级别保留一个对象的两个版本就足够了:提交的版本和被覆盖但尚未提交的版本。所以说MVCC是一种通用的实现方案
我们来看一下,用MVCC机制实现时,什么时候对象对于我们是可见的呢?
- 读事务开始时,创建该对象的事务已经提交。
- 对象未被标记为删除,或如果被标记为删除,请求删除的事务在读事务开始时尚未提交。
接下来还有一个问题:我们实现的数据的隔离,但是别忘了我们还有索引,数据的改变也会引起索引的变动,那么索引是如何在多版本数据库中工作的?
我们思考一下,有以下几种思路
1、索引简单地指向对象的所有版本,并且需要索引查询来过滤掉当前事务不可见的任何对象版本。当垃圾收集删除任何事务不再可见的旧 对象版本时,相应的索引条目也可以被删除。这种实现方式想想就工作量巨大,事务的状态变更将同时要修改索引,这些索引甚至大概率不在一个地方存储,带来的性能损耗可想而知
2、有些数据库(比如CouchDB,Datomic和LMDB),它们使用的是一种仅追加/写时拷贝(append-only/copy-on-write)的B树,它们在更新时不覆盖树的页 面,而为每个修改页面创建一份副本。从父页面直到树根都会级联更新,以指向它们子页面 的新版本。任何不受写入影响的页面都不需要被复制,并且保持不变。使用仅追加的B树,每个写入事务(或一批事务)都会创建一颗新的B树,当创建时,从该特定树根生长的树就是数据库的一个一致性快照。没必要根据事务ID过滤掉对象,因为后续写入不能修改现有的B树;它们只能创建新的树根。但这种方法也需要一个负责压缩和垃圾收集 的后台进程。
丢失更新问题
我们现在考虑这样的一种场景:
如果应用从数据库中读取一些值,修改它并写回修改的值(读取-修改-写入序列),则可能会发生丢失更新的问题。如果两个事务同时执行,则其中一个的修改可能会丢失,因为第二个 写入的内容并没有包括第一个事务的修改。
一些简单的场景就是:统计数值的并发更新、两个用户同时编辑wiki页面
上面的这种问题就是丢失更新问题
那么,针对这种问题,我们应该如何解决呢?我们肯定想到只要保证操作具有‘锁’的特性就行
1、原子写。这个要依赖数据库的支持。比如下面mysql的示例,还有MongoDB提供原子操作,redis的一些原子操作命令
UPDATE counters SET value = value + 1 WHERE key = 'foo';
2、显式锁定。就是加锁啰,FOR UPDATE加行锁
BEGIN TRANSACTION;
SELECT * FROM figures
WHERE name = 'robot' AND game_id = 222
FOR UPDATE;
UPDATE figures SET position = 'c4' WHERE id = 1234;
COMMIT;
3、CAS。加锁效率太低,CAS就能提供无锁化操作
UPDATE wiki_pages SET content = '新内容' WHERE id = 1234 AND content = '旧内容';
写入偏差与幻读
幻读是指在一个事务内,多次执行相同的查询操作,但结果却出现了新增或删除的数据行,就好像出现了幻觉一样。这是因为在这期间有其他事务插入、删除或修改了符合查询条件的数据,导致查询结果发生变化。
举一个简单的例子:
假设你去超市购买水果,你想购买的是超市里所有的苹果。你进入超市时,你看到苹果架上有10个苹果。于是你把苹果放进购物车,准备结账。但在你结账的时候,超市员工往苹果架上添加了5个苹果。当你回头看苹果架时,你发现有15个苹果。这就好像发生了幻觉,因为在你的视野中苹果的数量突然增加了。
在数据库中,幻读问题的发生方式类似。当一个事务在执行范围查询时,其他事务在这期间插入了新的符合查询条件的数据,导致了查询结果的变化,就像出现了幻觉一样。
写偏差指的是在一个事务内,多次执行相同的写操作,但结果却出现了不一致的情况。这是因为在这期间有其他事务对相同的数据进行了修改,导致写操作的结果不一致。幻读则是在一个事务内多次执行相同的范围查询操作时,结果出现了新增或删除的数据行。这是因为在这期间有其他事务对符合查询条件的数据进行了插入或删除,导致查询结果的变化。写偏差和幻读之间存在关系,即写偏差可能引发幻读问题。当一个事务在执行写操作时,如果另一个事务在同一时间对相同的数据进行了修改,那么在之后执行相同的范围查询操作时,可能会出现新增或删除的数据行,产生幻读。
快照隔离可以避免只读查询中幻读,但是在像读写事务中,幻影会导致特别棘 手的写歪斜情况。
后面的文章中,我会带你继续了解可序列化相关内容,可以关注一下哦~