Spring JPA与Hibernate财务序列生成

7
在我的应用中,我正在建立一个发票模型。在我的国家(意大利),每张发票必须有一个唯一的连续编号,不能有空缺,并且每年必须从1开始重新计数。
我反复思考了如何最好地实现它,但是我没有找到关于这个问题的好指南。目前,我有一个JpaRepository,其中包含我自定义的同步save()方法,在该方法中我获取了上一个使用的ID。
SELECT MAX(numero) FROM Invoice WHERE YEAR(date) = :year

这种方法的问题在于不太安全,因为开发人员应该知道只有使用特定服务才能进行保存。
相反,我更喜欢一种对开发人员隐藏的方法。 我考虑在@EntityListeners中使用@Prepersist方法。这听起来不错,但在该类中获取实体管理器并不那么简单...所以也许不是最佳位置...
最后,我想到了Hibernate拦截器...
请给我一些提示。该问题似乎是一个相当通用的问题;因此可能已经有一个好的实践方法可供遵循。
谢谢

使用synchronized方法还存在另一个问题。同步在单个JVM内有效,而不跨越JVM。因此,如果您扩展到集群中,每个JVM将拥有自己的synchronized方法,但它们之间不会进行任何同步。 - manish
1个回答

11

这个问题可以分解为以下需求:

  1. 顺序唯一:按照给定的值(例如 1000001)开始生成数字序列,并始终以固定值(例如 1)递增。
  2. 无间隙:数字之间不能有任何间隔。因此,如果第一个生成的数字是 1000001,增量为 1,到目前为止已经生成了 200 个数字,那么最新的数字应该是 1000201
  3. 并发性:多个进程必须能够同时生成数字。
  4. 创建时生成:必须在记录创建时生成数字。
  5. 无互斥锁:生成数字不应需要互斥锁。

任何一种解决方案都只能满足其中的 4 个需求。例如,如果要保证 1-4,每个进程都需要获取锁,以便其他进程不能生成和使用它所生成的相同数字。因此,将 1-4 视为要求意味着必须放弃 5。类似地,如果您想要保证 1、2、4 和 5,您需要确保只有一个进程(线程)在任一时间生成数字,因为在并发环境中无法保证唯一性而不使用锁。按照这种逻辑继续下去,您将看到为什么不可能同时保证所有这些需求。

现在,解决方案取决于您愿意牺牲 1-5 中的哪一个。如果您愿意牺牲第四个需求但不是第五个需求,您可以在空闲时间运行批处理过程来生成数字。然而,如果您将此清单放在业务用户(或财务人员)面前,他们会要求您遵守 1-4,因为对他们来说,第五个需求只是一个纯技术问题,因此他们不想受到干扰。如果是这种情况,一个可能的策略是:

  • 尽可能执行生成发票所需的所有计算,将发票号生成步骤作为最后一步。这将确保任何可能发生的异常在生成编号之前发生,并确保在非常短的时间内获取锁,从而不会对应用程序的并发性或性能产生太大影响。
  • 保持单独的表(例如DOCUMENT_SEQUENCE)以跟踪上一个生成的编号。
  • 在保存发票之前,在序列表上进行排他的行级锁定(例如隔离级别SERIALIZABLE),查找要使用的所需序列值并立即保存发票。这不应该花费太多时间,因为读取一行、增加其值和保存记录应该是足够短的操作。如果可能,将此短事务设置为主要事务的嵌套事务。
  • 保持足够合理的数据库超时,以便等待SERIALIZABLE锁的并发线程不会太快地超时。
  • 将整个操作放在重试循环中,在完全放弃之前至少重试10次。这将确保如果锁定队列建立得太快,操作仍然会在完全放弃之前尝试几次。许多商业软件包的重试计数高达40、60或100次。

除此之外,如果可能且符合您的数据库设计指南,请在发票号列上放置唯一性约束条件,以确保不会以任何代价存储重复值。

Spring为您提供了实现这一切所需的所有工具。

我有一个示例应用程序,演示了如何将所有这些组件一起使用。


感谢您非常清晰的解释。您非常理解要求,是的,我想保证1-4。我看了一下您的代码,但是我没有看到序列表上的独占锁...最后我的问题是,在侦听器中的@PrePersist方法中生成编号是否是一个好主意,这样我就可以确信无论调用哪个服务或存储库,最终数字都会自动生成。谢谢! - drenda
我的示例避免了独占锁,而是使用发票号码上的唯一约束。该约束可以被移除,并且事务隔离级别可以更改为SERIALIZABLE,但这会导致在任何超过10个并发用户的情况下出现更多的异常(事务超时)。你可以使用@PrePersist路线。 - manish
那么,如果EntityManager不可注入,使用@PrePersist也不是一个坏主意吧? - drenda
你最好使用一个EntityListener代替PrePersist回调。一个实体可以有多个EntityListener但只能有一个PrePersist回调,且许多框架会添加用于跟踪实体状态的PrePersist回调。如果您以后混合其他库,可能会遇到意外问题。个人而言,我会将编号生成代码保留在服务中,并通过架构强制执行发票生成,但在实体层中这样做也不是明显错误的。 - manish
非常感谢。我正在按照您的示例进行操作,但不幸的是,在@Retryable起作用时,出现了org.hibernate.AssertionFailure:mypackage.Invoice中的空ID。我已将此问题标记为已回答,并将为此奇怪的错误打开另一个问题。再次感谢。 - drenda

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接