此问题可以分解为以下要求:
- 顺序唯一 :从给定值(例如
1000001
)开始按顺序生成数字,然后始终以固定值(例如1
)递增。 - 无间隙 :数字之间不得有间隙。因此,如果生成的第一个数字为
1000001
,增量为,1
并且到目前为止已生成200个数字,则最新的数字应为1000201
。 - 并发性 :多个进程必须能够同时生成数字。
- 创建时生成 :数字必须在创建记录时生成。
- 无排他锁 :生成数字不需要排他锁。
任何解决方案都只能满足这5个要求中的4个。例如,如果要保证1-4,则每个进程都将需要加锁,以便其他进程无法生成并使用与其生成的相同编号。因此,将1-4强加为要求将意味着必须放弃5。同样,如果要保证1、2、4和5,则需要确保一次仅一个进程(线程)一次生成一个数字,因为在没有锁定的并发环境中无法保证唯一性。继续这种逻辑,您将了解为什么不可能同时保证所有这些要求。
现在,解决方案取决于您愿意牺牲1-5个中的哪一个。如果您愿意牺牲#4而不是#5,则可以在空闲时间运行批处理以生成数字。但是,如果您将此列表放在业务用户(或财务人员)面前,他们将要求您遵守1-4,因为#5对他们来说纯粹是技术问题,因此他们不希望成为困扰着它。如果是这样,可能的策略是:
- 执行所有生成发票所需的可能的计算,并保持发票号生成步骤为最后一步。这将确保在生成数字之前可能发生的任何异常,并确保在很短的时间内进行锁定,从而不会过多地影响应用程序的并发性或性能。
- 保留一个单独的表(例如,
document_SEQUENCE
)来跟踪最后生成的数字。 - 在保存发票之前,对序列表进行排他的行级锁定(例如,隔离级别
SERIALIZABLE
),找到要使用的所需序列值并立即保存发票。这应该不会花费太多时间,因为读取一行,增加其值并保存一条记录应该足够短。如果可能的话,使此短事务成为主事务的嵌套事务。 - 保持足够的数据库超时,以便等待
SERIALIZABLE
锁定的并发线程不会太快超时。 - 将整个操作置于重试循环中,至少要重试10次,然后才能完全放弃。这将确保如果锁定队列建立得太快,则在完全放弃之前仍会尝试几次操作。许多商业软件包的重试计数高达40、60或100。
除此之外,如果可能并在数据库设计准则允许的范围内,请在发票编号列上设置唯一的约束,以使重复的值不会被花费任何费用。
Spring为您提供了实现此目标的所有工具。
- Transactions :通过
@Transactional
注释。 - 序列化 :通过注释的
isolation
属性@Transactional
。 - 数据库访问 :通过Spring JDBC,Spring ORM和Spring Data JPA。
- 重试 :通过spring重试。
我有一个示例应用程序,演示了如何将所有这些部分一起使用。



