在任何数据库管理系统(DBMS)中,并发控制在确保多个事务能够同时进行而不会导致数据不一致或损坏方面起着至关重要的作用。它指的是管理事务如何交互的机制,特别是在它们访问共享资源时。分布式数据库管理系统(D-DBMS)由于数据分布在多个节点上而引入了复杂性,导致了数据复制、网络延迟和保持一致性等挑战,这使得并发控制的重要性更加突出。
在设计高效扩展的分布式系统时,避免持续协调变得至关重要。通过这样做,组件必须对其他组件的状态做出假设,因为它们不能持续检查。这些假设可以分为乐观和悲观。乐观假设是乐观并发控制(OCC)的核心,假设冲突很少发生,允许事务在没有锁的情况下进行,并在发生冲突时解决。这种方法在分布式环境中特别有用,可以增强可扩展性,但需要强大的冲突检测和解决机制来保持ACID属性——在系统中保持原子性、一致性、隔离性和持久性。悲观假设是悲观并发控制(PCC)的基础,假设冲突是常见的,并在事务期间对资源实施锁定以防止干扰。尽管这提供了一致性,但它可能会限制可扩展性和性能。选择OCC还是PCC取决于您的系统是更受益于避免早期协调并在后期解决冲突,还是通过提前控制来防止任何事务冲突。
在这篇文章中,我们深入探讨了并发控制,提供了有关设计高效事务模式的宝贵见解,并展示了有效解决常见并发挑战的示例。我们还包括一个示例代码,展示如何在Amazon Aurora DSQL(DSQL)中实现重试模式,以无缝管理并发控制异常。
何时使用PCC或OCC
在选择OCC和PCC之间时,关键是要使选择与您的工作负载和数据库环境保持一致。
PCC非常适合能够优化锁管理的单实例数据库,在频繁更新小键范围的高争用场景中表现出色,例如持续更新相同的股票代码。尽管这种模式在单实例RDBMS上效果很好,但在分布式系统中效果较差。
另一方面,OCC在低争用环境中表现最佳,允许事务在不锁定资源的情况下进行,仅在提交阶段检查冲突。这减少了执行开销和阻塞延迟,使OCC适用于冲突较少或非阻塞执行至关重要的工作负载。尽管在高争用场景下管理OCC可能更复杂,但它有效支持具有可扩展键范围和像追加操作这样受益于水平可扩展性的模式的分布式系统。
Aurora DSQL的OCC优势
Aurora DSQL采用乐观并发控制(OCC),这是一种在非关系型数据库中比在关系型数据库中更常用的技术。使用乐观并发控制时,事务逻辑在运行时几乎不考虑其他可能尝试更新相同行的事务。在事务完成后,尝试将更改提交到数据库。Aurora DSQL随后检查其他并发事务的写入是否干扰了该事务。如果没有,事务成功提交;否则,数据库会向事务报告错误。在这种情况下,用户必须决定如何继续,就像在依赖悲观并发控制的数据库中一样。对于大多数用例,最佳方法是重试事务。
在Aurora DSQL中,单个慢速客户端或长时间运行的查询不会影响或减慢其他事务的速度,因为争用是在服务器端的提交时处理的,而不是在SQL运行时处理的。相比之下,基于锁的设计允许客户端在启动事务时获取行甚至整个表的独占锁。如果客户端意外停止,这些锁可能会被无限期地持有,可能会阻止其他操作。这会阻止其他客户端取得进展,使它们处于不确定长度的队列中。相比之下,OCC立即通知客户端任何争用情况,提供确定的结果,允许立即重试或中止。基于锁的系统通常只有在达到超时时间后才会执行重试或取消逻辑。这使得Aurora DSQL在构建大型分布式应用程序时,尤其是在跨AWS区域时,对现实中的故障和错误更加健壮。
在多区域集群中,当用户提交事务时,SQL操作在提交事务的区域(区域1)的本地存储中执行。一旦整个事务完成,事务的后图像以及涉及的键会被发送到另一个区域(区域2)。在区域2,有一个事务处理器领导者,它知道该区域内所有正在进行的更改。当它收到来自区域1的后图像以及涉及该事务的键列表时,它会将其与本地区域内正在积极更改的所有键进行检查。如果没有冲突,它会发送确认以进行提交。
如果有冲突,最早提交的事务将成功,任何剩余的事务将需要重试,导致并发控制冲突。
Aurora DSQL为具有多区域主动-主动可用性的组织提供业务连续性。OCC通过同步复制提高了多区域事务的效率。Aurora DSQL在本地处理读写事务,而无需跨区域通信,仅在请求事务提交时检查跨区域的并发控制。OCC消除了锁协商的需要,使Aurora DSQL能够预处理完整的后图像,并将其与多可用区和多区域仲裁进行检查。这种方法降低了跨可用区和区域的同步事务的延迟,因为Aurora DSQL可以在没有锁开销的情况下处理它们。
乐观并发控制和隔离级别
与其他支持可序列化隔离的分布式SQL数据库相比,Aurora DSQL支持强快照隔离,这相当于PostgreSQL中的可重复读隔离级别,其中事务从事务开始时的数据库快照中读取数据。这在只读事务的情况下特别有用,因为它们不需要排队,并且不太容易受到乐观并发控制(OCC)的影响。
事务模式指导
并发数据库事务更新相同记录时存在冲突风险。尽管良好的数据建模可以减少这些风险,但冲突仍然是不可避免的,必须加以处理。数据库提供并发管理功能,开发人员应理解并在其应用程序中有效实施这些功能。
Aurora DSQL使用乐观并发控制,这可能需要不同的编程方法,特别是在同一键的并发更新率非常高的用例中。完成工作后,您尝试提交事务。Aurora DSQL会检查是否有其他更新事务干扰了您的事务。如果没有,事务成功。否则,数据库会报告错误。然后您必须决定如何处理:要么立即重新尝试事务,要么引入指数退避和抖动以减少后续冲突的可能性。
尽管乐观并发控制总是有助于事务在数据库中进行,但在高争用下它的性能仍然可能非常差。因此,有一些良好的事务模式可以作为指导:
让我们探讨一些Aurora DSQL中可能发生乐观并发控制异常的用例,并附上代码示例。
示例1:跨区域事务中的数据冲突
在像Aurora DSQL这样的分布式SQL系统中,乐观并发控制(OCC)异常常见的场景是当多个区域尝试同时更新相同的数据时。对于这个示例,假设我们有一个包含两个链接区域的集群:us-east-1和us-east-2,它们都在操作相同的账户数据:
us-east-1中的事务A读取账户的余额和版本,准备更新它。
同时,us-east-2中的事务B也读取相同账户的余额和版本,准备执行自己的更新。
事务B成功更新账户余额并增加版本号。
当事务A稍后尝试使用旧版本提交其更新时,由于版本已被事务B更新,发生OCC异常。
这个场景展示了在不同区域对相同数据的并发事务如何导致OCC异常。
现在,让我们通过一个代码示例来分解这个过程。
1. Create the table and insert a record:
CREATE TABLE orders.accounts ( id int PRIMARY KEY, balance DECIMAL(10, 2), version INT NOT NULL ); INSERT INTO accounts (balance, version) VALUES (100.00, 1);
2. Transaction A reads the account balance and version:
BEGIN; SELECT id, balance, version FROM accounts WHERE id = 1;
3. Transaction B reads the same data, updates it, and increments the version:
BEGIN; UPDATE accounts SET balance = balance + 50, version = version + 1 WHERE id = 1 AND version = 1; COMMIT;
4. Transaction A tries to update using the old version of the account, triggering an OCC conflict:
UPDATE accounts SET balance = balance - 30, version = version + 1 WHERE id = 1 AND version = 1; commit;
In this scenario, Transaction A would fail because the version number has been changed by Transaction B, simulating an OCC exception:
ERROR: change conflicts with another transaction, please retry: (OC000)
让我们深入了解实际发生的情况。
事务A是一个读/写事务,因此在读取阶段,查询处理器会解析SELECT语句并查阅分片映射,以找到存储这些数据的存储节点,并从这些节点检索数据。事务B也是一个读/写事务,查询处理器在UPDATE语句中创建读取集和写入集。当事务B在更新后发出提交时,查询处理器会将读取集和写入集打包并发送给事务处理器。现在,每个事务处理器将检查是否有其他事务对读取集进行了修改。如果没有更改,那么它将读取并写入提交层的前图像和后图像,使提交持久化且版本不增加。事务A发出UPDATE语句。由于查询处理器已经拥有更新所需的所有数据,它会创建写入集并将读取集和写入集传递给事务处理器。由于读取集的数据发生了变化,事务处理器将拒绝更改并给出OCC异常。此时,客户端可以重试事务B所做更新后的相同事务。
示例2:使用SELECT FOR UPDATE管理写偏差
我们之前提到Aurora DSQL通常不会对读取记录执行并发检查。SELECT FOR UPDATE改变了这种行为,并标记读取行以进行并发检查。
这就是你在Aurora DSQL中管理写偏差的方法。
在写偏差中,两个并发事务可以读取一个公共数据集,并各自进行更新以更改公共数据集,但它们彼此不重叠。由于它们不重叠(不修改相同的数据),因此不会触发并发保护。
在Aurora DSQL中,FOR UPDATE子句通过对标记的行引入额外的检查来修改乐观并发控制(OCC)的典型行为。这种调整防止了事务同时与相同数据集交互时可能发生的异常。与依赖锁机制来管理冲突的传统悲观并发控制(PCC)不同,OCC以不同的方式处理潜在的写入冲突。以下示例演示了FOR UPDATE子句如何在这种情况下强制执行并发检查。
让我们看看这种情况如何在现实世界中发生。
1. First, create an Orders table and insert a couple of row:
CREATE TABLE IF NOT EXISTS orders.orders ( order_id int PRIMARY KEY, customer_id INTEGER NOT NULL, order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, total_amount NUMERIC(10, 2) ); INSERT INTO schema_name.orders (order_id, customer_id, order_date, total_amount) VALUES (1, 12, '2024-09-26 14:30:00', 99.99); INSERT INTO schema_name.orders (order_id, customer_id, order_date, total_amount) VALUES (2, 123, '2024-09-26 14:32:00', 99.99);
2. Transaction A begins a read/write transaction using a FOR UPDATE clause but hasn’t yet executed the update or committed:
begin; select order_id, customer_id, total_amount from orders.orders where order_id = 1 FOR UPDATE;
3. Transaction B starts its own read/write transaction and commits:
begin; select order_id, customer_id, total_amount from orders.orders where order_id = 1 FOR UPDATE; update orders.orders SET total_amount = 103 WHERE order_id = 1; commit;
4. Transaction A then attempts to update the accounts table within the same transaction:
UPDATE orders.accounts SET balance = balance + 60, version = version + 1 WHERE id = 1 AND version = 1; commit;
When Transaction A issues the commit, the following OCC exception occurs:
ERROR: change conflicts with another transaction, please retry: (OC000)
即使更新不在包含已更改读取集的订单表上,仍然发生了OCC异常。
让我们分解一下实际发生的情况。
在这种情况下,事务A创建了一个包含order_id=1详细信息的读取集。同时,事务B也是一个读/写事务,读取并更新相同的order_id。稍后,当事务A尝试更新账户表中的余额时,它遇到了OCC错误(OC000)。具体情况如下:当事务A启动时,查询处理器创建读取集并等待生成写入集以便提交。然而,当事务A仍在进行中时,事务B更新并提交了事务A读取集中相同order_id的更改。当事务A继续更新账户表时,查询处理器创建写入集并将读取集和写入集一起交给事务处理器进行验证。在此阶段,事务处理器注意到由于事务B的更改,读取集中的数据有了新版本,导致它拒绝事务,迫使事务A重试。
这个例子展示了在您希望在Aurora DSQL中管理写偏差的情况下,使用FOR UPDATE是一种有效的处理方法。
请注意,FOR UPDATE仅在读/写事务中起作用。在只读事务中尝试使用FOR UPDATE将导致警告,并且不会报告读取行的更新:
postgres=# commit; WARNING: SELECT FOR UPDATE in a read-only transaction is a no-op COMMIT
此外,SELECT FOR UPDATE过滤器需要包含您选择的表的主键。在我们的例子中,表orders的主键是order_id,因此以下的SELECT FOR UPDATE将会失败:
postgres=> select customer_id from orders where orders.customer_id=123 for update; ERROR: locking clause such as FOR UPDATE can be applied only on tables with equality predicates on the key
但是这个将会成功,因为过滤器包含了主键:
postgres=> begin; BEGIN postgres=> select customer_id from orders where orders.customer_id=123 and order_id=2 for update; customer_id 123 (1 row)
示例3:目录不同步异常
尽管数据冲突是OCC异常的主要原因,但目录不同步问题也可能触发这些错误。当并发事务在活动会话期间修改或访问数据库架构(例如创建或更改表)时,就会发生这种情况。例如,如果一个事务在另一个事务尝试读取或写入该表时创建了一个表,由于会话对目录的视图已过时,可能会发生OCC错误(如OC001)。
重试操作通常可以解决这个问题,因为在初次失败后,数据库目录会被刷新。为了在生产环境中尽量减少此类错误的风险,建议避免以多线程方式执行DDL操作。同步的架构修改可能会导致竞争条件、事务失败和回滚。在受控的单线程环境中管理DDL更改是一种更可靠的方法,可以减少并发冲突并保持更顺畅的数据库操作。
让我们看看这种情况在现实世界中是如何发生的。
1. Transaction A creates the table:
CREATE TABLE orders.accounts ( id INT PRIMARY KEY, balance int, version int );
2. Transaction B, which already has an open session, tries to write a record into the table:
insert into orders.accounts VALUES (1, 10, 1); postgres=*> insert into orders.accounts VALUES (1, 10, 1); ERROR: schema has been updated by another transaction, please retry: (OC001) LINE 1: insert into orders.accounts VALUES (1, 10, 1);
在这个例子中,事务A创建了一个表,而事务B尝试向同一个表中写入记录并收到OCC异常,但随后的重试成功了。
以下是一些其他可能遇到OCC异常(OC001)的场景,这些通常可以通过重试解决:
事务A正在向现有表中添加列,而事务B正在尝试读取或写入该表。事务B将遇到OCC异常。
事务A删除了一个表,而事务B随后尝试访问同一个表。
事务A向表中添加了一列,而事务B同时尝试添加一个不同名称的列。
任何与正在进行的事务冲突的目录更改。
总之,OCC异常(OC001)通常由于并发修改数据库架构或目录更改引起,但通常可以通过实施适当的重试机制来解决。
使用重试机制处理OCC异常
在OCC中,实施退避和抖动是管理冲突事务重试的最佳实践,避免同步重试可能导致的进一步冲突或系统过载。退避确保在冲突后,重试不是立即进行的,而是间隔越来越长的延迟,有助于减少系统负载。抖动引入了这些延迟的随机性。退避和抖动共同减少了争用,并提高了在使用OCC的分布式系统中重试逻辑的效率。有关更深入的了解,请参阅指数退避和抖动。以下代码示例可通过此仓库获得。
让我们通过一个模拟高事务环境中OCC异常并使用退避和抖动策略管理重试的场景来进行演示。
1. First, use the create.py script to create an order schema and two tables: accounts and orders:
python create.py --host <endpoint> --database postgres --user <user_name> --region <region> --schema orders
2. Generate LoadRun the load_generator.py script to generate load for the database, inserting data into the orders table:
python load_generator.py --host <endpoint> --database postgres --user <user_name> --region <region> --schema orders --tablename orders --threads 10
3. To introduce an OCC condition, alter the accounts table by adding a new column in another PostgreSQL session:
ALTER TABLE order.accounts ADD COLUMN balance INT;
After the schema is updated, the load_generator.py script fails with the following error:
Error during insert: schema has been updated by another transaction, please retry: (OC001)
4. Now, let’s integrate backoff and jitter into the retry logic by running the retry_backoff_jitter.py script, an enhanced version of the load_generator.py script with built-in retry mechanisms:
python retry_backoff_jitter.py --host <endpoint> --database postgres --user <user_name> --region <region> --schema orders --tablename orders --threads 10
5. Now, introduce another schema change in the accounts table:
ALTER TABLE order.accounts ADD COLUMN totalsale INT;
As the retry logic kicks in, you’ll see the script handling the OCC exception with retries:
Error during batch insert: schema has been updated by another transaction, please retry: (OC001), retrying in 2.11 seconds (attempt 1/5) Error during batch insert: schema has been updated by another transaction, please retry: (OC001), retrying in 2.03 seconds (attempt 2/5)
这可以根据重试策略进行微调。在这种情况下,我们使用秒作为延迟单位。
在分布式系统中有效管理OCC异常需要一个全面的重试策略,该策略可以在无法避免应用程序键空间(热键)非常高争用区域的情况下,结合退避和抖动。一个经过深思熟虑的方法可以帮助在大规模下提供可预测性,最大化吞吐量,并增加自然表现出高争用率(热键)工作负载区域的稳定性。除了重试逻辑之外,从一开始就构建应用程序以最小化争用——偏向于追加模式而不是就地更新——是关键。大多数现代应用程序设计和分布式数据库(如Aurora DSQL)受益于水平扩展,其中引入新键而不是更新现有键支持最佳系统可扩展性。此外,实现幂等性确保重试不会创建重复操作或数据不一致,并且持久性失败的死信队列允许升级和人工干预,进一步增强系统可靠性。
结论
并发控制是任何数据库管理系统的关键方面,在处理分布式数据库时变得更加重要。由于其分布式架构,使用Aurora DSQL的OCC非常适合,因为它通过避免在事务执行期间需要资源锁定来实现更高的吞吐量和系统效率。
Aurora DSQL中OCC的主要优点是:
尽管OCC提供了显著的好处,但它也需要仔细考虑事务模式以实现最佳性能。假设事务可能失败、实施超时和带抖动的指数退避、以及使用SELECT FOR UPDATE管理写偏差等原则是开发人员在使用Aurora DSQL时的重要指南。
通过了解Aurora DSQL采用的并发控制机制和最佳实践,您可以在保持数据一致性和可用性的同时,利用系统的分布式能力,即使在复杂的高事务工作负载面前也是如此。要了解更多关于Aurora DSQL的信息,请参考文档。