CodeGym /Java 课程 /All lectures for ZH purposes /如何在应用程序中实现 ACID:实践

如何在应用程序中实现 ACID:实践

All lectures for ZH purposes
第 1 级 , 课程 889
可用

8.1 交易 ID

它被指定为 XID 或 TxID(如果有区别,请告诉我)。时间戳可以用作 TxID,如果我们想将所有操作恢复到某个时间点,它可以发挥作用。如果时间戳不够精细,可能会出现问题 - 然后事务可以获得相同的 ID。

因此,最可靠的选择是生成唯一的 UUID prod ID。在 Python 中这很容易:

>>> import uuid 
>>> str(uuid.uuid4()) 
'f50ec0b7-f960-400d-91f0-c42a6d44e3d0' 
>>> str(uuid.uuid4()) 
'd15bed89-c0a5-4a72-98d9-5507ea7bc0ba' 

还有一个选项可以对一组交易定义数据进行哈希处理,并将该哈希值用作 TxID。

8.2 重试

如果我们知道某个函数或程序是幂等的,那么这意味着我们可以而且应该尝试在出现错误时重复调用它。我们只需要为某些操作会出错这一事实做好准备 - 鉴于现代应用程序分布在网络和硬件上,错误不应被视为例外,而应被视为常态。该错误可能由于服务器崩溃、网络错误、远程应用程序拥塞而发生。我们的应用程序应该如何表现?没错,尝试重复操作。

由于一段代码可以说的不仅仅是一整页的话,让我们用一个例子来理解简单的重试机制应该如何理想地工作。我将使用 Tenacity 库对此进行演示(它设计得非常好,即使您不打算使用它,该示例也应该向您展示如何设计循环机制):

import logging
import random
import sys
from tenacity import retry, stop_after_attempt, stop_after_delay, wait_exponential, retry_if_exception_type, before_log

logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
logger = logging.getLogger(__name__)

@retry(
	stop=(stop_after_delay(10) | stop_after_attempt(5)),
	wait=wait_exponential(multiplier=1, min=4, max=10),
	retry=retry_if_exception_type(IOError),
	before=before_log(logger, logging.DEBUG)
)
def do_something_unreliable():
	if random.randint(0, 10) > 1:
    	raise IOError("Broken sauce, everything is hosed!!!111one")
	else:
    	return "Awesome sauce!"

print(do_something_unreliable.retry.statistics)

> 为了以防万一,我会说:\@retry(...) 是一种特殊的 Python 语法,称为“装饰器”。它只是一个 retry(...) 函数,它包装了另一个函数并在它执行之前或之后做一些事情。

正如我们所见,可以创造性地设计重试:

  • 您可以按时间(10 秒)或尝试次数 (5) 限制尝试。
  • 可以是指数的(即 2 ** 一些递增的数字 n )。或以其他方式(例如,固定的)增加不同尝试之间的时间。指数变体称为“拥塞崩溃”。
  • 您只能针对某些类型的错误 (IOError) 进行重试。
  • 日志中的一些特殊条目可以在重试尝试之前或完成。

现在我们已经完成了年轻战士课程并了解了在应用程序端处理事务所需的基本构建块,让我们熟悉两种允许我们在分布式系统中实现事务的方法。

8.3 交易爱好者的高级工具

我只会给出相当笼统的定义,因为这个主题值得单独写一篇大文章。

两阶段提交 (2pc)。2pc 有两个阶段:准备阶段和提交阶段。在准备阶段,将要求所有微服务为一些可以原子完成的数据更改做准备。一旦它们都准备就绪,提交阶段将进行实际更改。为了协调这个过程,需要一个全局协调器,它锁定必要的对象——也就是说,在协调器解锁它们之前,它们变得不可访问以进行更改。如果特定的微服务没有准备好进行更改(例如,不响应),协调器将中止事务并开始回滚过程。

为什么这个协议好?它提供原子性。此外,它还保证了写入和读取时的隔离。这意味着在协调器提交更改之前,对一个事务的更改对其他事务不可见。但是这些属性也有一个缺点:由于这个协议是同步的(阻塞的),它会减慢系统的速度(尽管 RPC 调用本身很慢)。再次,存在相互阻塞的危险。

传奇。在此模式中,分布式事务由跨所有关联微服务的异步本地事务执行。微服务通过事件总线相互通信。如果任何微服务未能完成其本地事务,其他微服务将执行补偿事务以回滚更改。

Saga 的优点是没有对象被阻塞。但当然也有缺点。

Saga 很难调试,尤其是当涉及到很多微服务时。Saga 模式的另一个缺点是它缺乏读取隔离。也就是说,如果ACID中表示的属性对我们很重要,那么Saga就不太适合我们。

我们从这两种技术的描述中看到了什么?事实上,在分布式系统中,原子性和隔离的责任在于应用程序。使用不提供 ACID 保证的数据库时也会发生同样的事情。也就是说,冲突解决、回滚、提交和释放空间等事情落在了开发人员的肩上。

8.4 我怎么知道什么时候需要 ACID 保证?

当一组特定的用户或进程很可能同时处理同一数据时

很抱歉很平庸,但一个典型的例子是金融交易。

当交易执行的顺序很重要时。

想象一下,您的公司即将从 FunnyYellowChat 信使切换到 FunnyRedChat 信使,因为 FunnyRedChat 允许您发送 gif,但 FunnyYellowChat 不能。但是,您不仅仅是在更换 Messenger——您是在将贵公司的信件从一个 Messenger 迁移到另一个 Messenger。你这样做是因为你的程序员懒得在某个地方集中记录程序和过程,而是他们在 Messenger 的不同频道中发布了所有内容。是的,您的销售人员在同一个地方公布了谈判和协议的细节。简而言之,您公司的整个生命都在那里,并且由于没有人有时间将整个事情转移到文档服务,而且即时通讯工具的搜索效果很好,您决定不清除废墟而是简单地复制所有消息到新位置。消息的顺序很重要

顺便说一下,对于 Messenger 中的信件,顺序通常很重要,但是当两个人同时在同一个聊天中写东西时,一般来说谁的消息先出现就不那么重要了。因此,对于这种特定情况,不需要 ACID。

另一个可能的例子是生物信息学。我完全不明白这一点,但我认为在破译人类基因组时顺序很重要。但是,我听说生物信息学家通常会使用他们的一些工具来处理所有事情——也许他们有自己的数据库。

当您无法为用户提供或处理过时数据时。

再次 - 金融交易。老实说,我想不出任何其他的例子。

当未决交易与重大成本相关时。想象一下当医生和护士都更新患者记录并同时删除彼此的更改时可能出现的问题,因为数据库无法隔离事务。医疗保健系统是除金融之外的另一个领域,其中 ACID 保证往往至关重要。

8.5 什么时候不需要 ACID?

当用户只更新他们的一些私人数据时。

例如,用户在网页上留下评论或便签。或在任何服务提供商的个人帐户中编辑个人数据。

当用户根本不更新数据,而只是补充新数据(追加)时。

例如,一个正在运行的应用程序可以保存您的跑步数据:您跑了多少、跑了多少时间、路线等。每次新运行都是新数据,旧的完全没有编辑。也许,基于数据,您可以获得分析 - 只有 NoSQL 数据库适合这种情况。

当业务逻辑不确定是否需要执行事务的特定顺序时。

或许,对于一个在下一次直播中为制作新素材募捐的 Youtube 博主来说,是谁、在什么时候、以什么顺序向他投钱并不重要。

当用户将在同一个网页或应用程序窗口停留几秒钟甚至几分钟,因此他们会以某种方式看到过时的数据。

理论上,这些是任何在线新闻媒体,或相同的 Youtube。或“哈布尔”。 当不完整的交易可以暂时存储在系统中对您来说无关紧要时,您可以忽略它们而不会造成任何损害。

如果您正在聚合来自多个来源的数据,并且更新频率很高的数据——例如,至少每 5 分钟更改一次的城市停车位占用率数据,那么理论上这不会是一个大问题对你来说,如果某个停车场的交易在某个时候无法完成。虽然,当然,这取决于你想用这些数据做什么。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION