在Postgres中使用Serializable事务级别发生意外故障

时间:2019-07-15 14:15:20

标签: postgresql postgresql-9.6

我们正在开发轻量级的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的页锁,因此失败,因为同一页中的其他插入同时发生。

这个故事变得越来越重要,因为我们注意到随着表的增长,测试重现的失败率降低(记录分成更多页面,因此发生的冲突更少)。

那么我的问题是:

  • 这是Postgres中的预期行为吗?一个简单的pkey查找是否最多不需要一个元组锁,而不是一个页面锁?
  • 在隔离级别Serializable上我们应该能够做到这一点吗?降低到repeatable read可以解决问题,但是我们不愿意在不了解更多的情况下这样做。
  • 在这种情况下,我们应该采取什么明显的措施来帮助Postgres?例如。该查询的注释或我们可以启用的设置?

在相关情况下,我们正在用Kotlin编写的Exposed后端中使用Ktor。 Postgres的版本是9.6。

1 个答案:

答案 0 :(得分:2)

这是working as expected

  

为了在依赖可序列化事务进行并发控制时获得最佳性能,应考虑以下问题:

     

[...]

     

在您的测试用例中,一个表页面上有三个或更多谓词锁,因此该锁将升级为页面锁。这就是为什么交易彼此冲突(它们影响同一页面)的原因。

尝试增加max_pred_locks_per_page