在CockroachDB中何时以及为何使用SELECT FOR UPDATE

221 阅读6分钟

我们没有实现SELECT FOR UPDATE来确保一致性。与Amazon Aurora不同,CockroachDB已经保证了可序列化的隔离,这是ANSI SQL标准所提供的最高隔离级别。争夺发生了。当事务发生争执时,应用开发者不应该让他们的数据的完整性面临风险。为了解决这个问题,CockroachDB必须偶尔返回错误,提示应用程序重试有可能出现异常的事务,比如写错。这意味着,就像PostgreSQL的可序列化隔离一样,开发者需要为争夺的事务实现重试循环。对于习惯了低隔离级别的关系型数据库的开发者来说,这可能是一个陌生的模式。

在CockroachDB 20.1中,我们通过启用SELECT FOR UPDATE简化了对客户端事务重试错误的处理。这个功能允许应用程序在每个语句的基础上明确控制行级锁定。通过在事务的早期读取时更积极地锁定,应用程序可以避免导致客户端事务重试错误的情况。

在这篇文章中,我们将探讨事务重试错误以及SELECT FOR UPDATE是如何发挥作用的。最后,我们将分享一系列的测试,使用高度竞争的工作负载,比较有无SELECT FOR UPDATE的性能(在v19.2和v20.1之间),以证明性能的提升和更少的客户端事务重试错误。

SELECT FOR UPDATE演示

在下面的GIF中,我们演示了SELECT FOR UPDATE避免了客户端事务重试错误:

这个例子揭示了最常见的重试错误形式--一个事务在没有获得锁的情况下读取了一条记录,它在客户端对该记录进行了修改,然后它试图将修改内容写回数据库中的同一记录。如果第二个并发事务也修改了该行,并且在第一个事务的读和写之间提交,第一个事务必须重试。如果它不这样做,它将有可能把错误的更新值写回该行。

例如,想象一下同一整数计数器的两个增量。如果两个增量事务都从计数器中读出了12的值,并且后来都被允许将其更新为13,那么这两次写入就会出现 "倾斜",其中一个事务的效果就会消失。一个提供可序列化隔离的数据库不能允许这种情况发生。

所以就其本身而言,重试错误比数据损坏要好,但是如果我们不想实现客户端的重试循环呢。这就是SELECT FOR UPDATE的作用。通过在每个事务中使用SELECT FOR UPDATE选择行的初始状态,我们提前获得了对行的独占锁。如果两个并发的事务试图对同一行进行SELECT FOR UPDATE,其中一个将不得不等待。这可以防止我们陷入这样的情况:我们向客户返回了关于行的值的过期信息,所以我们永远不需要使用重试错误来撤销这些信息。

使用雅虎云服务基准对SELECT FOR UPDATE进行基准测试

YCSB(Yahoo!云服务基准)是一个行业标准。它模拟了 "现实的 "互联网风格的工作负载,这些工作负载会引起热点并产生争论。为了验证SELECT FOR UPDATE的性能,我们在一个3节点的c5d.9xlarge机器集群(AWS)上使用v19.2和v20.1,并应用YCSB基准套件的工作负载A和F。

对于工作负载A和F,我们测试了有无YCSB列族优化。这种优化对每一列使用一个列族,以避免同一行内不同列的写入之间的竞争。

工作负载A测试隐式SELECT FOR UPDATE语句

sql.defaults.implicit_select_for_update.enabled 集群设置被启用时(默认为true),隐式SELECT FOR UPDATE在UPDATE语句中内部使用读锁定子系统。一个UPDATE语句在CockroachDB中被转化为一个读和一个写。隐式SELECT FOR UPDATE意味着我们在UPDATE语句的读取部分使用FOR UPDATE锁。这意味着你的应用程序可以获得隐式SELECT FOR UPDATE的好处,而不需要任何改变。

YCSB工作负载A沿着这个倾斜分布发出50%的SELECT 语句和50%的UPDATE 语句。这使我们能够测试隐式SELECT FOR UPDATE功能,因为UPDATE语句使用读锁定子系统来提高性能。

工作负载F测试显式SELECT ... FOR UPDATE语句

YCSB工作负载F发出50%的SELECT 语句和50%的读-修改-写事务,即BEGIN;SELECT;UPDATE;COMMIT; 。这使得我们可以测试更新的显式选择功能,因为我们可以尝试在读-修改-写事务中的SELECT 语句中加入FOR UPDATE

CockroachDB 20.1之前和之后的结果

我们在CockroachDB 19.2和20.1版本上运行了YCSB,看看两者的区别。

吞吐量
在v19.2和v20.1之间,YCSB的吞吐量(操作/秒)在所有情况下都有所提高:工作负载A,工作负载F,有列族和无列族。更大的队列响应能力和减少了因交易重试而导致的激动,并提高了系统吞吐量。

尾部延迟
在v19.2和v20.1之间,所有工作负载的最大交易延迟都下降了,无论是否有列族。这表明,SELECT FOR UPDATE 队列的改进的公平性特征转化为减少尾部延迟和更可预测的性能。

交易错误的数量
我们通过测量运行YCSB时客户端交易重试的数量回到了实现SELECT FOR UPDATE的灵感。YCSB的工作负载F执行多语句交易(BEGIN;SELECT;UPDATE;COMMIT;),因此容易受到客户端重试的影响。

管理界面在sql.restart_savepoint.rollback.count 报告中报告了客户端交易重试的数量。

测试开始时,针对Cockroach v19.2运行工作负载F。就在01:24之后,Cockroach被升级到了v20.1,以使我们实现SELECT FOR UPDATE--关键时刻。负载生成器开始在多语句事务中的SELECT语句中添加FOR UPDATE后缀。客户端的重试消失了。所有的重试条件都在事务中的第一条语句上,允许它们在服务器上透明地重试。

结论

在CockroachDB 20.1中,测试表明启用SELECT FOR UPDATE在三个方面改善了CockroachDB:

  1. 吞吐量
  2. 尾部延时
  3. 交易重试错误的数量

对于隐式UPDATE语句,这个新功能在CockroachDB 20.1中是默认启用的。但是对于显式的SELECT FOR UPDATE,你需要明确地修改你的查询以获得好处。要开始使用SELECT FOR UPDATE,请深入了解我们的SELECT FOR UDPATE文档