顺序计数器的正确隔离级别
Correct isolation level for sequential counter
我正在开发一个计费系统(C# 代码,MySQL Galera Cluster 后端,InnoDB 存储引擎)并且对如何为发票生成真正唯一的序列号有疑问。
通常在其他系统中,我创建了一个独特的服务来获取发票编号,每当生成发票时,我都会向该服务询问一个号码,并且该服务保证了对 table 的独占访问权柜台不会有任何问题。
但是这个新系统是为了高可用性而集群的,所以这种方法是不可接受的table,因为需要有多个这样的服务运行。
所以我在这里应用的逻辑是这样的:
- 开始交易
- 创建没有序列号的发票
- 检索串行计数器
- 将新计数器写入 table
- 更新发票
- 提交
如果我没记错的话,如果其他事务在当前事务完成之前更新了计数器,那么提交将抛出异常,然后我可以重试该操作,这将确保发票编号的顺序。
所以我的问题是,哪一个是实现此目的的正确隔离级别? 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';
假设您的 table 看起来像这样(并且您不查询例如 select max(no) from invoices
来获取最后一个数字),两种隔离级别都将起作用。它们的主要区别在于锁定的方式(多行)。如果您在(在我的示例中)type
上有一个索引,它们的行为将完全相同。
然后,该决定将取决于您的其余查询。 repeatable read
通常是一个好的和安全的选择(并且默认是有原因的);如果你降低它,你可能不得不更努力地考虑潜在的问题,但可能会获得一些 performance/less 阻塞。
您没有指定如何设置集群,您显然必须确保它们都使用相同的 table 或在您的 master 上使用不同的偏移量。
关于你的问题,当另一个事务试图更改值时会发生什么:第二个事务将在需要锁定资源的点等待,直到第一个事务释放它(通常是在它准备好时),你只会达到超时(或检测到死锁)时获取异常。
我正在开发一个计费系统(C# 代码,MySQL Galera Cluster 后端,InnoDB 存储引擎)并且对如何为发票生成真正唯一的序列号有疑问。
通常在其他系统中,我创建了一个独特的服务来获取发票编号,每当生成发票时,我都会向该服务询问一个号码,并且该服务保证了对 table 的独占访问权柜台不会有任何问题。
但是这个新系统是为了高可用性而集群的,所以这种方法是不可接受的table,因为需要有多个这样的服务运行。
所以我在这里应用的逻辑是这样的:
- 开始交易
- 创建没有序列号的发票
- 检索串行计数器
- 将新计数器写入 table
- 更新发票
- 提交
如果我没记错的话,如果其他事务在当前事务完成之前更新了计数器,那么提交将抛出异常,然后我可以重试该操作,这将确保发票编号的顺序。
所以我的问题是,哪一个是实现此目的的正确隔离级别? 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';
假设您的 table 看起来像这样(并且您不查询例如 select max(no) from invoices
来获取最后一个数字),两种隔离级别都将起作用。它们的主要区别在于锁定的方式(多行)。如果您在(在我的示例中)type
上有一个索引,它们的行为将完全相同。
然后,该决定将取决于您的其余查询。 repeatable read
通常是一个好的和安全的选择(并且默认是有原因的);如果你降低它,你可能不得不更努力地考虑潜在的问题,但可能会获得一些 performance/less 阻塞。
您没有指定如何设置集群,您显然必须确保它们都使用相同的 table 或在您的 master 上使用不同的偏移量。
关于你的问题,当另一个事务试图更改值时会发生什么:第二个事务将在需要锁定资源的点等待,直到第一个事务释放它(通常是在它准备好时),你只会达到超时(或检测到死锁)时获取异常。