我们中有些人构建了一个游戏,而另一些人则熟悉电子商务平台,D-APP,甚至所有这些类型的应用程序等。我们正在处理的每种类型的软件都需要不同的技术,其中一些技术相似。因此,每个软件产品都可能发生同样的问题。在这篇文章中,我们将一起讨论高工作量数据库中的一个问题。它是并发控制。
问题
让我们考虑一下我们有一个包括帐户表的电子银行应用程序。每个帐户都存储余额,当有撤销交易时,我们需要减去,并在有存款交易时添加。
假设系统开发了以下步骤:
- 选择发件人的余额
- 检查发件人的余额
- 选择收件人的余额
- 减去发送者和收件人的余额
- 更新余额
有了上面的所有松散步骤,我们可以想象这里有一些空白。因此随时可能出现问题。例如,想象一下我们有两个用户:a余额为300美元,b,在这里平衡不相关。我们还有两项单独的交易:第一个请求从a到b发送$ 200,第二个请求是300美元。
当这两项交易同时出现时,我们可以在这里看到一个非常透明的问题,同时选择发件人的余额(300美元),并同时获得成功,首先将余额更新至$ 100 ,而第二个将其更新为$0。
这只是一个简单的示例。我们也有许多这样的相关场景,但是列出所有这些情况并不是本文的目的。我们将使用它作为一个问题,可以帮助我们打开数据库中用于解决问题的技术之一:显式锁定。。
请注意,我将使用PostgreSQL解决问题,因此本文中的每个概念都应偏向此数据库。可以以不同的概念和名称以不同的方式实现不同的数据库,但是在引擎盖下,它们应该相似。
首先,数据库中的明确锁定是什么?
数据库锁定是最常见的机制之一,可以通过防止多个交易同时访问相同数据来帮助我们在数据库中实现并发控制。我们需要探索的第一件事是在SQL数据库中锁定的类型。
我知道,我们有两种流行的数据库锁定类型
- 共享锁允许多次交易同时读取资源,但请防止其他交易修改锁定资源,直到锁定锁定为止。当我们需要经常阅读数据但很少会修改数据时,它们会有所帮助。
- 当交易需要修改数据时,使用独家锁。这种类型的锁可以防止其他任何交易访问相同的数据,直到锁定锁定为止。这意味着,当交易在资源上包含一个独家锁定时,它可以修改数据而不会干扰其他交易。
除了这些类型的锁外,有些数据库还支持其他数据库,例如以下
- 更新锁可用于保护资源在阅读时被修改。
- 意图锁定信号是在资源上获取共享或独家锁定的意图。这可以将其视为锁的锁。
- 模式锁用于防止并发模式修改。
除了类型外,我们还根据以下锁的范围将数据库锁定为几个级别。
+----------------------------------------------------+
| |
| DATABASE LEVEL LOCKING |
| |
| +--------------------------------------------+ |
| | | |
| | TABLE LEVEL LOCKING | |
| | | |
| | +--------------------------------------+ | |
| | | | | |
| | | PAGE LEVEL LOCKING | | |
| | | | | |
| | | +-----------------------------+ | | |
| | | | | | | |
| | | | ROW LEVEL LOCKING | | | |
| | | | | | | |
| | | +-----------------------------+ | | |
| | | | | |
| | +--------------------------------------+ | |
| | | |
| +--------------------------------------------+ |
| |
+----------------------------------------------------+
图像1:锁定范围
锁定级别 | 描述 |
---|---|
数据库级锁 | 可以应用于数据库的最高锁定级别。此锁可以防止对整个数据库的任何并发访问。 |
表级锁 | 将锁应用于整个桌子,以防止对表的任何并发访问。 |
页面级锁 | 将锁应用于表中的一个数据页面,以防止对该页面的任何并发访问。 |
行级锁 | 最颗粒状的锁定水平应用于表中的一行数据。这允许同时访问同一表中的其他行。 |
表1:锁定级别
在这篇文章中,我们只关注表和行级锁定。
表级锁用于防止任何交易访问完整表或关系。锁的行为取决于其类型,并且并不总是相同的。通常,当正确的行为触发时,数据库会自动使用这些锁。但是,您也可以使用LOCK
命令获得特定的锁。
有几种可用于数据库的锁定模式,级别和锁定类型各不相同。锁类型之间的关键区别是它们可能与之冲突的其他锁定类型集。这意味着,当在特定表上设置锁定时,它会防止其他交易获得相互冲突的锁。重要的是要注意,交易可能与自身发生冲突。
例如,在PostgreSQL中,我们的下表代表冲突的锁模式。
当然,这是带有ACCESS SHARE
的更新表,在ROW SHARE
之前添加到第二列中
请求的锁定模式 | 访问共享 | 行共享 | 排独家 | 分享更新独家 | 分享 | 分享行独家 | 独家 | 访问独家 |
---|---|---|---|---|---|---|---|---|
访问共享 | x | |||||||
行共享 | x | x | ||||||
行独家 | x | x | x | x | ||||
共享更新独家 | x | x | x | x | x | |||
共享 | x | x | x | x | x | |||
共享行独家 | x | x | x | x | x | x | ||
独家 | x | x | x | x | x | x | x | |
访问独家 | x | x | x | x | x | x | x | x |
表2:表级别的冲突锁定模式[1]
另一个常见的概念是行级锁。在此级别上,锁不会影响数据查询;他们只将作家和储物柜阻止到同一行。行级锁可以在交易端或保存点回滚期间释放,就像桌级锁一样。
类似于表级锁,行级锁也具有不同的锁定模式,并且每个锁可能与他人冲突。下表描述了这些模式:
请求的锁定模式 | 键共享 | 共享 | 没有密钥更新 | 更新 |
---|---|---|---|---|
键共享 | x | |||
共享 | x | x | ||
没有密钥更新 | x | x | x | |
更新 | x | x | x | x |
表3:冲突的行级锁[2]
除了上面列出的数据库定义的锁外,一些数据库还提供了一种用于创建具有应用程序定义含义(称为咨询锁的锁的方法)。这些锁不会自动使用;有时,我们需要自定义锁定机制的能力,因此我们在应用程序级别上实现咨询锁并手动控制它们。
例如,我们可以通过两种方式在PostgreSQL中获取咨询锁:
- 会话级别的咨询锁。在这种情况下,完成交易后不会自动释放锁,因此我们需要手动发布。
- 交易级别的咨询锁,看起来与常规锁更相似。我们不需要明确的解锁操作员来发布它。
在实施中,咨询锁尝试在特定关系或表上获取EXCLUSIVE
锁定,并防止其他交易访问它。
我们很高兴继续下一部分,我们将讨论实际问题。
为什么我们需要这些锁,以及如何选择正确的锁类型?
首先,我们继续解决本文开头提出的问题。
在这种情况下,两项交易都同时更新了同一帐户的余额,从而导致数据冲突。帐户x和帐户y的最终余额不同,具体取决于首先进行交易。
为了避免此问题,我们可以使用SELECT FOR UPDATE
锁定要更新的行,直到交易进行。这样可以确保只有一项交易可以一次修改所选行,从而防止数据冲突。这是我们如何使用SELECT FOR UPDATE
转移资金的一个示例:
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE account_number = 'A' FOR UPDATE;
-- Locks the row for account A
SELECT balance FROM accounts WHERE account_number = 'B' FOR UPDATE;
-- Locks the row for account B
UPDATE accounts SET balance = balance - 500 WHERE account_number = 'A';
-- Deduct $500 from account A
UPDATE accounts SET balance = balance + 500 WHERE account_number = 'B';
-- Add $500 to account B
COMMIT;
-- Releases the locks and commits the transaction
这样,另一项交易也希望在A和B的余额上进行SELECT FOR UPDATE
,需要等到当前交易进行。这样可以防止数据发生冲突。
咨询锁怎么样?
假设您有一个带有多个服务器的分布式系统,需要从消息队列处理消息。每个服务器负责从队列的特定子集读取消息,并且您要确保没有两个服务器同时处理相同的消息。
一种方法是使用SELECT ... FOR UPDATE
在处理时锁定消息行。但是,这将要求所有服务器都使用相同的数据库连接,这可能成为瓶颈并限制可扩展性。此外,如果服务器崩溃或失去了与数据库的连接,则将发布其锁,并且可能会通过另一台服务器处理相同的消息。
更好的方法是使用咨询锁。每个服务器都可以使用自己的数据库连接在处理消息ID上获取咨询锁定。这将阻止其他服务器同时处理相同的消息,即使他们使用了不同的数据库连接甚至不同的数据库。
这是一个示例脚本,该脚本演示了PostgreSQL中的咨询锁的使用:
-- Assume we have a message queue table with an ID column and a status column
CREATE TABLE message_queue (
id SERIAL PRIMARY KEY,
status TEXT
);
-- Function to process a message with a given ID
CREATE OR REPLACE FUNCTION process_message(id BIGINT)
RETURNS VOID AS $$
DECLARE
lock_acquired BOOLEAN;
BEGIN
-- Attempt to acquire an advisory lock on the message ID
lock_acquired := pg_try_advisory_lock(id);
-- If the lock was acquired, update the message status and commit the transaction
IF lock_acquired THEN
UPDATE message_queue SET status = 'processing' WHERE id = $1;
COMMIT;
-- Do some processing here...
UPDATE message_queue SET status = 'processed' WHERE id = $1;
COMMIT;
ELSE
-- The lock was not acquired, so another server must be processing this message
RAISE NOTICE 'Could not acquire lock for message ID %', id;
END IF;
END;
$$ LANGUAGE plpgsql;
-- Call the process_message function with a specific message ID
SELECT process_message(123);
通常,只有在必要时才使用咨询锁。它们可以为应用程序代码增加复杂性,如果不正确使用,也可以成为争论和绩效问题的来源。
结论
显式锁定是在高工作负载数据库中解决并发控制的最访问方法。根据您的应用程序或功能的上下文,您可以选择适当的数据库锁定类型/级别锁定级别,以避免数据冲突,考虑到利弊。但是,这不是唯一的选择。您还可以选择其他方法,例如实现队列或单独的服务,该服务将每个请求划分并规则为数据库。希望这篇文章可以帮助您选择将来实施应用程序的正确方法。
参考
[1]文档:15:13.3。显式锁定。表13.2。冲突的锁定模式,Postgresql,https://www.postgresql.org/docs/current/explicit-locking.html。 2023年4月23日访问。
[2]文档:15:13.3。显式锁定。表13.3。冲突的行级锁,Postgresql,https://www.postgresql.org/docs/current/explicit-locking.html。 2023年4月23日访问。
[3]咨询锁以及如何使用它们。 2023年4月23日访问。
[4] - 理查德·克莱顿(Richard Clayton) - 与Postgres Advisory Lock分发锁定。 2023年4月23日访问。
[5]锁定数据库和隔离机制|由丹尼·萨姆(Denny Sam)|鼓舞人心。 2023年4月23日访问。