我们没有实现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:
- 吞吐量
- 尾部延时
- 交易重试错误的数量
对于隐式UPDATE语句,这个新功能在CockroachDB 20.1中是默认启用的。但是对于显式的SELECT FOR UPDATE,你需要明确地修改你的查询以获得好处。要开始使用SELECT FOR UPDATE,请深入了解我们的SELECT FOR UDPATE文档。