我有一个名为Operations
的表格,其中包括sourceId
, destinationId
, amount
, status
等列。每当用户进行转账时,API会先计算信用操作金额之和减去借记操作金额之和,然后将一行新数据插入该表格,前提是余额大于或等于转账金额。
问题在于并发性,因为同时执行多个转账操作的用户可能会导致负余额。
有多种处理PostgreSQL并发性问题的方法:
- 可串行事务隔离级别
- 表锁定
- 行版本控制
- 行锁定
等等。
我们期望的行为是,在设置事务隔离级别为SERIALIZABLE
之外,数据库应该等待前一个事务结束,并获取最新插入的版本,而不是在(sourceId, version)
上失败重复提交。
但是,我并不确定最佳的方法。这是我的尝试:
1. 可串行事务隔离级别
这是最简单的方法,但问题在于锁升级,因为如果数据库引擎负载过重,1个事务可能会锁定整个表格,这是记录下来的行为。
伪代码:
newId = INSERT INTO "Operations" ("SourceId", "DestinationId", "Amount", "Status", "OccuredAt") values (null, 2, 3, 100, 'PENDING', null);
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT * FROM "Operations" WHERE ("SourceId" = 2 or "DestinationId"=2) and "Status" = 'SUCCESSFUL';
'''API: check if balance > transfer amount'''
UPDATE "Operations" SET "Status" = 'SUCCESSFUL' where id = newId
COMMIT;
2. 表锁定
不使用序列化事务级别就可以避免这种情况。
3. 行版本控制
从性能角度考虑,这是目前最好的方法。我们添加了一个version int
列和一个(sourceId, version)
唯一索引,当事务被插入时,它会带有下一个版本号。如果两个事务并发,数据库将抛出错误:
重复键值违反了“IX_Transactions_SourceWalletId_Version”唯一约束条件
伪代码:
newId = INSERT INTO "Operations" ("SourceId", "DestinationId", "Amount", "Status", "OccuredAt") values (null, 2, 3, 100, 'PENDING', null);
BEGIN;
lastVersion = SELECT o."Version"
FROM "Operations"
WHERE ("SourceId" = 2) AND ("Version" IS NOT NULL)
ORDER BY o."Version" DESC
LIMIT 1
SELECT * FROM "Operations" WHERE ("SourceId" = 2 or "DestinationId"=2)
and "Status" = 'SUCCESSFUL';
'''API: check if balance > transfer amount'''
UPDATE "Operations" SET "Status" = 'SUCCESSFUL', "Version" = lastVersion + 1 where id = newId;
COMMIT;
4. 行锁定
在计算用户余额之前,使用 sourceWalletId = x
(其中x
是进行转账的用户)锁定所有交易行。但是我无法在PostgreSQL中找到此操作的方法,使用for update
可以完成此操作,但在并发事务等待第一个事务后,结果不会返回新插入的行,这是PostgreSQL记录的行为。