【问题标题】:How to handle gapless sequence with JPA and Postgres?如何使用 JPA 和 Postgres 处理无间隙序列?
【发布时间】:2015-02-04 17:14:11
【问题描述】:

我正在使用 Spring、Spring JPA、Hibernate 和 Postgresql 开发 REST API。 我有一个要求,我需要在实体中有一系列代码。

考虑以下实体:

public class Document{
    private Long id;
    private String code;

    //getters and setters
}

在我的 spring 方法中,我正在做这样的事情来保存一个新的entity

    String prefix = "D";

    //get records
    List<Document> documents = this.documentRepository.findAll();

    //find max value of code
    int max = 0;
    for(Document d:documents){
        String code = d.getCode();
        int number = Integer.parseInt(code.substring(prefix.length()));
        if(number>max) max = number;
    }

    //increment
    long currentNumber = max+1;

    entity.setCode(prefix+currentNumber);

    this.documentRepository.save(entity);

这导致我遇到这样一种情况,如果我尝试调用此方法两次,我会得到两个具有相同 code 的文档。

为了解决这个问题,我尝试在我的方法中添加 @Transactional@Transactional(isolation = Isolation.SERIALIZABLE) 注释。事务正在正确创建,但是其中一个 api 调用失败了

ERROR:  could not serialize access due to read/write dependencies among transactions

根据我阅读的有关隔离级别的信息,SERIALIZABLE 是我所需要的。此外,看起来 Postgres 对这个隔离级别采取了“积极”的方法,期望两个事务都成功提交。但是,情况可能并非总是如此,客户端应用程序需要重试失败的事务。

观察:

  • 如果现有文档D4删除,则接受下一个创建的文档变为D5。因此,这并不像名称本身所说的那样“无缝”。
  • 我的实际用例要复杂一些。我不只是在做findAll,我只是为了这个问题而简化了。在我的例子中,假设Documents 包含在Folder 中。无间隙序列只是该文件夹中,而不是全局。每个Folder 有很多Documents。因此,对于每个文件夹,我们都有 D1,D2,D3,...

可接受的解决方案:

  • 如果失败,让 spring 重试事务
  • 这种无间隙序列问题的替代方法
  • 表锁定会起作用吗?

【问题讨论】:

  • 为什么不直接使用 JPA 的@GeneratedValuestackoverflow.com/questions/11788483/…所以,这并不像名称本身所说的那样“无缝”。——这就是序列的作用)
  • 为什么需要无缝?要真正确保在并发或分布式系统中将是一个相当大的挑战。通常,随着间隔顺序增加就足够了,因为您仍然保持秩序(但同样,在并发系统中,秩序也有待商榷)
  • @pozs,这种方法能确保事务隔离吗?我认为这种方法行不通,因为这会强制执行“全局”序列?我在Document 中寻找多个序列。每个Folder 有很多Documents。因此,对于每个文件夹,我们都有 D1,D2,D3,...
  • @Alex 它必须是无缝的,因为它只是系统的要求。我们可以放宽该要求的唯一情况是删除的实体。如果我们删除“中间”的实体,我们可以忍受这个差距。
  • @miguelcobain,那么从设计的角度来看,这不是一个无缝系统......希望您的团队意识到您提出的 100% 工作是多么困难。这听起来像是一个任意选择的设计决策(不是技术限制),会困扰你很长时间

标签: java spring hibernate postgresql jpa


【解决方案1】:

根据我阅读的有关隔离级别的信息,SERIALIZABLE 是我所需要的

如果这样冲突很多,你会得到非常高的回滚率。

我建议使用锁定方法。

有一个序列生成器表。在此表中,使用原生查询获取新值:

UPDATE mysequencetable SET nextvalue = nextvalue + 1 RETURNING nextvalue

但是,您仍然需要处理重试,因为如果您有多个这样的表,您的事务可能会以不同的顺序获取行,然后死锁。

所以您的应用程序必须能够重试事务。一个编写良好的应用程序无论如何都必须这样做,否则如果数据库重新启动、暂时无法访问等,它将无法正常运行。

现在,如果您还必须在 DELETEs 上重新编号,这是一个不同的问题,您需要一个触发器。

【讨论】: