- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
苛刻的数据存储系统中,很多可能出错的case:
为实现高可靠,系统必须处理这些问题。但完善容错机制工作量巨大,要仔细考虑所有可能出错的事情,并充分测试。
十年来,事务一直是简化这些问题的首选机制。事务将应用程序的多个读、写操作组合成一个逻辑单元。即事务中的读、写操作是个执行的整体:整个事务要么成功(提交),要么失败(中止或回滚)。若失败,程序可安全地重试。如此,便无需再担心部分失败的情况,应用层的错误处理就简单很多。
也许你觉得事务就这么简单了,但细究起来也许不止于此。事务不是先天存在的;它是为简化应用层的编程模型而人为创造的。通过事务,应用程序可忽略某些潜在的错误和复杂的并发问题,因为DB会替应用处理好(称之为安全保证,safety guarantees)。
并非所有应用都需要事务,有时可弱化事务处理或完全放弃事务(如为获得更高性能或更高可用性)。一些安全相关属性也可能会避免引入事务。
先要确切理解事务能为我们提供什么安全保障及其代价。
本文将研究许多出错案例,并探索DB防范这些问题的算法和设计。尤其是并发控制领域,深入讨论各种竞争条件及DB的隔离级别。
本文同时适用于单机DB与分布式DB。
目前几乎所有关系型DB和一些非关系DB都支持事务。大多遵循IBM System R(第一个SQL数据库)在1975年的设计。50年来,尽管一些细节实现变化,但总体思路大同小异。MySQL、PostgreSQL、Oracle 和 SQL Server 等DB中的事务支持与 System R 极为相似。
2000年后,NoSQL普及,目标在关系DB现状上,通过提供新数据模型和内置的复制和分区改进传统的关系模型。然而,事务成了这变革的受害者:新一代DB完全放弃事务或重新定义,即替换为比以前弱得多的保证。
随新型分布式DB炒作,人们普遍认为事务是可扩展性的对立面,大型系统都必须放弃事务以获得更高性能和高可用性。但另一方面,还有一些DB厂商坚称事务是 “关键应用” 和 “高价值数据” 所必备的重要功能。这两种观点都有些夸张。
事务有其优势和局限性。为理解事务权衡,来看看正常运行和各种极端case,看看事务到底能给我们什么。
事务所提供的安全保证即ACID:
它由 TheoHärder 和 Andreas Reuter 于 1983 年为精确描述DB的容错机制。
但实际上不同DB的 ACID 实现不尽相同。仅隔离性含义就有很多争议。当一个系统声称自己 “兼容ACID” 时,实际上能提供什么保证并不清楚。ACID现在几乎已经变成一个营销术语。
不符合ACID的系统有时被称为BASE:
听起来比 ACID 还含糊不清,BASE唯一能确定的是 “它不是 ACID”,此外没有承诺任何东西。
事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
这个术语在计算机不同领域意味着相似但却微妙的差异。多线程编程中,若某线程执行一个原子操作,这意味着其它线程无法看到该操作的中间结果。系统只能处于操作前或操作后的状态,而非两者之间状态。
而ACID的原子性并并不关系到多个操作的并发。它并未描述多个线程试图同时访问相同的数据会怎样,后者其实由ACID的隔离性所定义。
ACID原子性其实描述客户端发起一个包含多个写操作的请求时可能发生的情况。如在完成部分写入后,系统就发生诸如进程崩溃,网络中断,磁盘变满或违反某种完整性约束。把多个写操作纳入到一个原子事务,万一出现这些故障而导致无法完成最终提交,则事务会中止,且DB须丢弃或撤销那些局部完成的更改。
若无原子性,当多个更新操作中间发生错误,就得知道哪些更改已生效,哪些未生效,这寻找过程会很麻烦。或许应用程序可以重试,但情况类似,并且可能导致重复更新或错误的结果。原子性大大简化了这个问题:若事务已中止,应用程序可确定它没有改变任何东西,所以应用能安全重试。
因此,ACID的原子性的定义特征:出错时中止事务,并将部分完成的写入全部丢弃。 或许 可中止性(abortability)是更恰当的术语。
在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持完整性。事务结束时,所有内部数据结构(如B树索引或双向链表)也都必须正确。
一致性在不同场景有着不同含义:
ACID一致性主要是对数据有特定的预期状态,任何数据更改必须满足这些状态约束(或恒等条件)。例账单系统中,所有账户必须借贷相抵。若某事务从一个有效的状态开始,且事务处理期间任何写操作都没有违背约束,则最后结果依然符合有效状态。
这种一致性本质要求应用层来维护状态一致,应用程序负责正确定义事务来保持一致性。这不是DB能保证的:即若你提供的数据违背恒等条件,DB也很难检测进而阻止该操作。DB 能完成针对某些特定类型的恒等约束检查,如外键约束或唯一性约束。但主要还是靠应用程序定义数据的有效/无效状态,DB 主要还是负责存储。
原子性,隔离性和持久性是DB 本身属性,而ACID的一致性更多是应用层的属性。应用可能借助DB的原子性和隔离属性来达到一致性,但一致性本身并不源于DB。因此,字母C其实不应属于ACID 1。
一个事务所做的修改在最终提交前对其他事务不可见。
大多DB都支持同时被多个客户端访问。若读、写的是不同数据,肯定没问题,但若访问相同记录,则可能会遇到并发问题。
图-1的简单案例,假设两个客户端同时增加DB中的一个计数器。这里假设DB不支持自增。每个客户端先读取当前值,加1 ,再写回新值。两次增长,计数器应从42增至44,但由于竞态条件,最终结果是43 。
ACID的隔离性意味着并发执行的多个事务相互隔离:互不交叉。传统DB教科书将隔离性定义为串行化,这意味着可以假装它是DB上运行的唯一事务。虽然实际上它们可能同时运行,但DB系统要确保当事务提交时,其结果与串行执行完全相同。
然而实践中,由于性能问题,很少使用串行化的隔离。Oracle 11甚至不实现它,Oracle虽有个名为 “可串行的” 隔离级别,但本质上实现的快照隔离,提供了比串行化更弱的保证。
一旦事务提交,它对于数据的修改会持久化到DB
DB系统本质是提供一个安全可靠的地方存储数据,而不用担心丢失。持久性就是这样的承诺,保证一旦事务提交成功,即使发生硬件故障或DB崩溃,事务写入的任何数据也不会丢失。
单节点DB,持久性意味着数据已被写入非易失性存储设备,如硬盘、SSD。写入过程中,通常涉及预写日志,以便在磁盘数据损坏时可进行恢复。支持复制的DB中,持久性意味着数据已成功复制到多个节点。为实现持久性保证,DB必须等到这些写入或复制完成后,才能报告事务成功提交。
完美的持久性是不存在的:若所有硬盘和所有备份同时被(人为)销毁,那DB也无能为力。
历史上,持久性最早意味着写入磁带存档,后来演变为写入磁盘、SSD。最近,又代表多节点间“复制(replication)”。哪种实现更好呢?
没有一个是完美的:
没有技术能提供绝对的持久性保证。只有各种降低风险的技术,包括写盘,复制到远程机器和备份。
ACID的原子性和隔离性主要针对客户端在同一事务中包含多个写时,DB提供的保证:
若一系列写操作中间出错,则事务必须中止,并丢弃当前事务的所有写入。即DB免去了用户对部分失败的担忧,要么全部成功,要么全部失败的保证。
同时运行的事务互不干扰。如若一个事务进行多次写入,则另一个事务要么看到其全部写入结果或什么都看不到,而不该是中间的部分结果。
这些定义假设一个事务中修改多个对象(如行,文档,记录)。这种多对象事务目的通常是为了在多个数据对象之间保持同步。图-2展示一个电邮案例。
显示用户未读件数:
SELECT COUNT (*)
FROM emails
WHERE recipient_id = 2
AND unread_flag = true
但若邮件太多,查询太慢,决定用单独字段存储未读数量。每当收到一个新邮件,增加未读计数器,当邮件标记为已读,也得减少该计数器。
用户2遇到异常情况:邮件列表显示了未读消息,但计数器显示为零未读消息,因为还没更新 2。隔离性将保证用户2要么同时看到新邮件和增长后的计数器,要么都看不到,而不是前后矛盾的中间结果。
图-3说明了对原子性需求:若事务过程中出错,导致邮箱和未读计数器的内容不同步,则事务将被中止,事务将被中止,且之前插入的电子邮件将被回滚。
多对象事务要求确定知道某种方式包含哪些读写操作。关系型DB,客户端一般和DB服务器建立TCP网络连接,因而对特定的某个连接,BEGIN TRANSACTION
和 COMMIT
之间的所有内容都属于同一事务3
许多非关系DB不会将这些操作组合一起。即使支持多对象API(如KV存储的multi-put API 可以在一个操作中更新多个K),但这并不一定意味着它具有事务语义:该命令可能在一些键上成功,在其他的键上失败,使数据库处于部分更新的状态。
原子性和隔离性也适用单个对象更新。如若向DB写入20KB的JSON文档:
这些问题很让人头大,故存储引擎必备设计:对单节点、单个对象层面上提供原子性和隔离性(如 KV 对)。原子性可以通过使用日志来实现崩溃恢复(B+树),并对每个对象加锁实现隔离 。
某DB也提供高级原子操作 4,如自增,这就不再需要像图-1那样执行读取 - 修改 - 写回。类似的CAS操作,即只有当前值未被其他并发修改过,才允许执行写。
这些单对象操作可有效防止多个客户端并发修改同一对象时的丢失更新。但它们不是通常意义上的事务。虽然CAS及其他单一对象操作有时被称为 “轻量级事务”,甚至出于营销目的被称为 “ACID”,但存在误导。事务通常针对的是多个对象,将多个操作聚合为一个执行单元的机制。
许多分布式数据存储不支持多对象事务,因为多对象事务很难跨分区实现,且在高可用性或高性能情况下也碍事。
但分布式数据库中实现事务,并没有什么原理障碍。但是否需要多对象事务?是否可能只用KV数据模型和单对象操作就能满足应用需求呢?
确有一些场景,单对象插入、更新和删除就够了。但很多其他场景要求协调写入几个不同的对象:
这些应用即使没有事务支持,或许仍可工作。但无原子性保证,错误处理就复杂多了,缺乏隔离性,就会导致并发问题。
事务的一大关键特性,若出错,中止所有操作,之后可安全重试。ACID DB基于此理念:若DB存在违反原子性、隔离性或持久性的风险,则完全放弃事务,而非部分放弃。
但并非所有系统都遵循这理念。如无主节点复制的数据存储会在 “尽力而为” 基础上尝试多做点。可概括理解为为:DB已尽其所能,但万一遇到错误,系统不会撤销已完成的操作,此时需应用程序责任从错误中恢复。
错误无法避免,但我们倾向于只考虑正常case,而忽略错误处理。如Rails ActiveRecord和 Django这类ORM框架,事务异常时不会重试而只是简单抛堆栈信息,用户虽然得到错误提示,但所有之前的输入都被丢弃了。这肯定不该发生,中止的重点就是允许安全重试。
重试中止的事务虽是个简单有效的错误处理机制,但不完美:
我正在尝试打印 timeval 类型的值。实际上我可以打印它,但我收到以下警告: 该行有多个标记 格式“%ld”需要“long int”类型,但参数 2 的类型为“struct timeval” 程序
我正在编写自己的 unix 终端,但在执行命令时遇到问题: 首先,我获取用户输入并将其存储到缓冲区中,然后我将单词分开并将它们存储到我的 argv[] 数组中。IE命令是“firefox”以启动存储在
我是 CUDA 的新手。我有一个关于一个简单程序的问题,希望有人能注意到我的错误。 __global__ void ADD(float* A, float* B, float* C) { con
我有一个关于 C 语言 CGI 编程的一般性问题。 我使用嵌入式 Web 服务器来处理 Web 界面。为此,我在服务器中存储了一个 HTML 文件。在此 HTML 文件中包含 JavaScript 和
**摘要:**在代码的世界中,是存在很多艺术般的写法,这可能也是部分程序员追求编程这项事业的内在动力。 本文分享自华为云社区《【云驻共创】用4种代码中的艺术试图唤回你对编程的兴趣》,作者: break
我有一个函数,它的任务是在父对象中创建一个变量。我想要的是让函数在调用它的级别创建变量。 createVariable testFunc() [1] "test" > testFunc2() [1]
以下代码用于将多个连续的空格替换为1个空格。虽然我设法做到了,但我对花括号的使用感到困惑。 这个实际上运行良好: #include #include int main() { int ch, la
我正在尝试将文件写入磁盘,然后自动重新编译。不幸的是,某事似乎不起作用,我收到一条我还不明白的错误消息(我是 C 初学者 :-)。如果我手动编译生成的 hello.c,一切正常吗?! #include
如何将指针值传递给结构数组; 例如,在 txt 上我有这个: John Doe;xxxx@hotmail.com;214425532; 我的代码: typedef struct Person{
我尝试编写一些代码来检索 objectID,结果是 2B-06-01-04-01-82-31-01-03-01-01 . 这个值不正确吗? // Send a SysObjectId SNMP req
您好,提前感谢您的帮助, (请注意评论部分以获得更多见解:即,以下示例中的成本列已添加到此问题中;西蒙提供了一个很好的答案,但成本列本身并未出现在他的数据响应中,尽管他提供的功能与成本列一起使用) 我
我想知道是否有人能够提出一些解决非线性优化问题的软件包的方法,而非线性优化问题可以为优化解决方案提供整数变量?问题是使具有相等约束的函数最小化,该函数受某些上下边界约束的约束。 我已经在R中使用了'n
我是 R 编程的初学者,正在尝试向具有 50 列的矩阵添加一个额外的列。这个新列将是该行中前 10 个值的平均值。 randomMatrix <- generateMatrix(1,5000,100,
我在《K&R II C 编程 ANSI C》一书中读到,“>>”和“0; nwords--) sum += *buf++; sum = (sum >>
当下拉列表的选择发生变化时,我想: 1) 通过 div 在整个网站上显示一些 GUI 阻止覆盖 2)然后处理一些代码 3) 然后隐藏叠加层。 问题是,当我在事件监听器函数中编写此逻辑时,将执行 onC
我正在使用 Clojure 和 RESTEasy 设计 JAX-RS REST 服务器. 据我了解,用 Lisp 系列语言编写的应用程序比用“传统”命令式语言编写的应用程序更多地构建为“特定于领域的语
我目前正在研究一种替代出勤监控系统作为一项举措。目前,我设计的用户表单如下所示: Time Stamp Userform 它的工作原理如下: 员工将选择他/她将使用的时间戳类型:开始时间、超时、第一次
我是一名学生,试图自学编程,从在线资源和像您这样的人那里获得帮助。我在网上找到了一个练习来创建一个小程序来执行此操作: 编写一个程序,读取数字 a 和 b(长整型)并列出 a 和 b 之间有多少个数字
我正在尝试编写一个 shell 程序,给定一个参数,打印程序的名称和参数中的每个奇数词(即,不是偶数词)。但是,我没有得到预期的结果。在跟踪我的程序时,我注意到,尽管奇数词(例如,第 5 个词,5 %
只是想知道是否有任何 Java API 可以让您控制台式机/笔记本电脑外壳上的 LED? 或者,如果不可能,是否有可能? 最佳答案 如果你说的是前面的 LED 指示电源状态和 HDD 繁忙状态,恐怕没
我是一名优秀的程序员,十分优秀!