我正在开发一个计费系统(C# 代码、MySQL Galera Cluster 后端、InnoDB 存储引擎)并且对如何为发票生成真正唯一的序列号有疑问。
通常在其他系统中,我创建了一个独特的服务来获取发票编号,每当生成发票时,我向该服务询问一个号码,并且由于该服务保证了对持有柜台的表格的独占访问权,因此不会根本没有问题。
但是这个新系统是为了高可用性而集群的,所以这种方法是 Not Acceptable ,因为需要运行多个这样的服务。
所以我在这里应用的逻辑是这样的:
- 开始交易
- 创建没有序列号的发票
- 检索串行计数器
- 将新计数器写入表
- 更新发票
- promise
如果我没记错的话,如果其他事务在当前事务完成之前更新了计数器,那么提交将抛出异常,然后我可以重试该操作,这将确保发票编号的顺序。
所以我的问题是,哪一个是实现此目的的正确隔离级别? READ_COMMITED 是否足够或可能产生重复?或者有更好的方法吗?
最佳答案
实际上,如果您不小心,这两种隔离级别都会给您带来麻烦。
除了技术差异(例如,它们锁定了多少行),READ COMMITTED
和 REPEATABLE 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';
假设您的表看起来像这样(并且您不查询例如 select max(no) from invoices
以获取最后一个数字),两种隔离级别都将起作用。它们的主要区别在于锁定的方式(多行)。如果您在(在我的示例中)type
上有一个索引,它们的行为将完全相同。
然后,决定将取决于您的其余查询。 repeatable read
通常是一个好的和安全的选择(并且默认是有原因的);如果你降低它,你可能不得不更加努力地考虑潜在的问题,但可能会获得一些性能/更少的阻塞。
您没有指定如何设置集群,您显然必须确保它们都使用相同的表或在您的主服务器上使用不同的偏移量。
关于您的问题,当另一个事务试图更改值时会发生什么:第二个事务将在需要锁定资源的点等待,直到第一个事务释放它(通常是在准备就绪时),您只会得到一个达到超时(或检测到死锁)时出现异常。
关于c# - 顺序计数器的正确隔离级别,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40677912/