顺序计数器的正确隔离级别

Correct isolation level for sequential counter

我正在开发一个计费系统(C# 代码,MySQL Galera Cluster 后端,InnoDB 存储引擎)并且对如何为发票生成真正唯一的序列号有疑问。

通常在其他系统中,我创建了一个独特的服务来获取发票编号,每当生成发票时,我都会向该服务询问一个号码,并且该服务保证了对 table 的独占访问权柜台不会有任何问题。

但是这个新系统是为了高可用性而集群的,所以这种方法是不可接受的table,因为需要有多个这样的服务运行。

所以我在这里应用的逻辑是这样的:

如果我没记错的话,如果其他事务在当前事务完成之前更新了计数器,那么提交将抛出异常,然后我可以重试该操作,这将确保发票编号的顺序。

所以我的问题是,哪一个是实现此目的的正确隔离级别? READ_COMMITED 是否足够或可能会重复?或者有更好的方法吗?

实际上,如果您不小心,这两种隔离级别都会给您带来麻烦。

除了技术差异(例如他们锁定了多少行),READ COMMITTEDREPEATABLE READ 在处理以下情况的方式上有所不同:

start transaction;
select no from counters where type = 'INVOICE';
-- some other session changes the value and commits it
select no from counters where type = 'INVOICE';

READ COMMITTED 会给你两个不同的结果,REPEATABLE READ 会给你两个 select 的旧值。但是两种隔离级别都不会阻止任何人更改该值,因此您不希望出现任何一种情况。

重要的是锁定您要更改的行,这样其他人就无法更改它:

start transaction;
select no from counters where type = 'INVOICE' for update;
update counters set no = @newvalue where type = 'INVOICE';

或者如果计算简单先进行实际更新:

start transaction;
update counters set no = no + 1 where type = 'INVOICE';
select no from counters where type = 'INVOICE';

假设您的 table 看起来像这样(并且您不查询例如 select max(no) from invoices 来获取最后一个数字),两种隔离级别都将起作用。它们的主要区别在于锁定的方式(多行)。如果您在(在我的示例中)type 上有一个索引,它们的行为将完全相同。

然后,该决定将取决于您的其余查询。 repeatable read 通常是一个好的和安全的选择(并且默认是有原因的);如果你降低它,你可能不得不更努力地考虑潜在的问题,但可能会获得一些 performance/less 阻塞。

您没有指定如何设置集群,您显然必须确保它们都使用相同的 table 或在您的 master 上使用不同的偏移量。

关于你的问题,当另一个事务试图更改值时会发生什么:第二个事务将在需要锁定资源的点等待,直到第一个事务释放它(通常是在它准备好时),你只会达到超时(或检测到死锁)时获取异常。