我们正在开发轻量级的CRUD应用程序,并选择对交易使用Serializable
隔离级别。
但是,当增加环境的负载时,我们会看到大量交易失败,我们认为这不会造成任何问题。特别是,我们有一项事务已设法简化为以下事务,并且仍然可以复制问题:
transaction(Connection.TRANSACTION_SERIALIZABLE, 3) {
val record = MyRecord(UUID.randomUUID(), UUID.randomUUID(), DEFAULT_JSON)
myDao().getRecord(record.id)
myDao().addRecord(record)
}
转换为SQL:
SELECT mytable.id, mytable.userId, mytable.json, mytable.deleted_at FROM mytable WHERE mytable.id = '93ea4a65-cd52-4d73-a
e74-38055c1b066b'
INSERT INTO mytable (deleted_at, json, id, user_id) VALUES
(NULL, '{"version":7}', '93ea4a65-cd52-4d73-ae74-38055c1b066b', '026d3c48-cdc5-4748-927b-408712e00f89')
即,通过PRIMARY KEY
UUID列进行简单的检索然后插入。在逐步提高速度时(例如40个线程,每个线程连续运行50个事务),我们看到其中绝大多数失败,但出现以下异常:
o.p.u.PSQLException: ERROR: could not serialize access due to read/write dependencies among transactions
Detail: Reason code: Canceled on identification as a pivot, during write.
Hint: The transaction might succeed if retried.
at o.p.c.v.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2433)
at o.p.c.v.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2178)
at o.p.c.v.QueryExecutorImpl.execute(QueryExecutorImpl.java:306)
at o.p.jdbc.PgStatement.executeInternal(PgStatement.java:441)
at o.p.jdbc.PgStatement.execute(PgStatement.java:365)
at o.p.j.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:155)
at o.p.j.PgPreparedStatement.executeUpdate(PgPreparedStatement.java:132)
at o.j.e.s.s.InsertStatement.execInsertFunction(InsertStatement.kt:86)
at o.j.e.s.s.InsertStatement.executeInternal(InsertStatement.kt:95)
at o.j.e.s.s.InsertStatement.executeInternal(InsertStatement.kt:12)
at o.j.e.s.s.Statement.executeIn$exposed(Statement.kt:59)
... 90 common frames omitted
Wrapped by: o.j.e.e.ExposedSQLException: org.postgresql.util.PSQLException: ERROR: could not serialize access due to read/write dependencies among transactions
Detail: Reason code: Canceled on identification as a pivot, during write.
Hint: The transaction might succeed if retried.
at o.j.e.s.s.Statement.executeIn$exposed(Statement.kt:61)
at o.j.e.s.Transaction.exec(Transaction.kt:129)
at o.j.e.s.Transaction.exec(Transaction.kt:123)
at o.j.e.s.s.Statement.execute(Statement.kt:29)
at o.j.e.sql.QueriesKt.insert(Queries.kt:44)
at g.c.e.d.MyDao.insertEvent(DefaultEventsDao.kt:40)
... 81 common frames omitted
在线程运行时进入pg_locks
,我们可以看到:
| locktype | database | relation | page | tuple | virtualxid | transactionid | classid | objid | objsubid | virtualtransaction | pid | mode | granted | fastpath
| page | 18496 | 17542 | 2 | <null> | <null> | <null> | <null> | <null> | <null> | 30/75 | 1467 | SIReadLock | True | False
| page | 18496 | 17542 | 5 | <null> | <null> | <null> | <null> | <null> | <null> | 34/45 | 1471 | SIReadLock | True | False
| page | 18496 | 17542 | 2 | <null> | <null> | <null> | <null> | <null> | <null> | 8/335 | 1446 | SIReadLock | True | False
| page | 18496 | 17542 | 1 | <null> | <null> | <null> | <null> | <null> | <null> | 31/65 | 1468 | SIReadLock | True | False
| page | 18496 | 17542 | 6 | <null> | <null> | <null> | <null> | <null> | <null> | 43/15 | 1480 | SIReadLock | True | False
| page | 18496 | 17542 | 4 | <null> | <null> | <null> | <null> | <null> | <null> | 5/357 | 1482 | SIReadLock | True | False
| page | 18496 | 17542 | 6 | <null> | <null> | <null> | <null> | <null> | <null> | 41/15 | 1478 | SIReadLock | True | False
| page | 18496 | 17542 | 6 | <null> | <null> | <null> | <null> | <null> | <null> | 40/30 | 1477 | SIReadLock | True | False
关系17542
与表的pkey相对应(通过查询pg_class
进行验证)。因此,似乎事务需要SELECT
的页锁,因此失败,因为同一页中的其他插入同时发生。
这个故事变得越来越重要,因为我们注意到随着表的增长,测试重现的失败率降低(记录分成更多页面,因此发生的冲突更少)。
那么我的问题是:
Serializable
上我们应该能够做到这一点吗?降低到repeatable read
可以解决问题,但是我们不愿意在不了解更多的情况下这样做。在相关情况下,我们正在用Kotlin编写的Exposed
后端中使用Ktor
。 Postgres的版本是9.6。
答案 0 :(得分:2)
为了在依赖可序列化事务进行并发控制时获得最佳性能,应考虑以下问题:
[...]
- 当由于谓词锁定表内存不足而被迫将多个页面级谓词锁定组合到单个关系级谓词锁定中时,序列化失败率可能会增加。您可以通过增加
max_pred_locks_per_transaction
,max_pred_locks_per_relation
和/或max_pred_locks_per_page
来避免这种情况。
在您的测试用例中,一个表页面上有三个或更多谓词锁,因此该锁将升级为页面锁。这就是为什么交易彼此冲突(它们影响同一页面)的原因。
尝试增加max_pred_locks_per_page
。