我在并发方案中遇到了重复增量字段的问题。
我正在使用EF作为ORM工具,尝试插入一个具有充当增量INT字段的字段的实体。基本上这个字段称为“SequenceNumber”,其中插入前的每个新记录将使用MAX读取数据库以获取最后一个SequenceNumber,向其追加+1,并保存更改。
在获取最后SequenceNumber
和保存之间,这就是并发发生的地方。
我没有将ID
用于SequenceNumber
,因为它不是唯一约束,并且可能会在某些条件下重置,例如每月,每年等。
InvoiceNumber | SequenceNumber | DateCreated
INV00001_08_14 | 1 | 25/08/2014
INV00001_08_14 | 1 | 25/08/2014 <= (concurrency is creating two SeqNo 1)
INV00002_08_14 | 2 | 25/08/2014
INV00003_08_14 | 3 | 26/08/2014
INV00004_08_14 | 4 | 27/08/2014
INV00005_08_14 | 5 | 29/08/2014
INV00001_09_14 | 1 | 01/09/2014 <= (sequence number reset)
发票编号的格式基于SequenceNumber
。
经过一些研究后,我最终得到了这些可能的解决方案,但想知道最佳实践
悲观并发,将表从任何读取锁定,直到当前事务完成(这个想法并不花哨,因为我认为性能会产生很大的影响?)
仅为此目的创建一个存储过程,选择并插入单个语句,因为并发性最小(如果可能,更喜欢基于EF的方法)
==============================
修改
我想到的另一个解决方案是将 InvoiceNumber 设为唯一约束,因此对于重复条目的情况,它会抛出Unique Constraint Violation错误,catch它,并重新尝试获取新的SequenceNumber和InvoiceNumber以重新插入数据库。
答案 0 :(得分:4)
使用带有身份密钥的假表/实体。
在假表中插入发票插入以获取ID之前。 使用ID,不是作为FK而是作为SequenceNumber
重置:截断假表。
可预测的问题:序列中可能有漏洞,具体取决于插入错误。
从sql server 2012开始就要注意CREATE SEQUENCE。顺便说一下,如果你认为这个想法很好,那么upvote on datavoice
=====
sql序列可以通过约束用作默认值:
ALTER TABLE Invoice
ADD CONSTRAINT DefSequence DEFAULT (NEXT VALUE FOR InvoiceSeq)
FOR SequenceNumber;
GO
通过将SequenceNumber设置为DatabaseGeneratedOption.Computed
,它可能是完美的。
答案 1 :(得分:1)
乐观并发意味着没有锁定。在操作结束时检查对DB的更改。如果不是其他人修改了影响操作的东西,那就意味着它没问题。如果其他人修改了影响操作的内容,则会抛出异常。 (这个名字来自这样一个事实:你很乐观,希望在你使用它的时候没有别的东西可以修改数据)。
存储过程是一个很好的解决方案。为什么不使用它? - 请记住,您必须将过程代码包含在事务中。如果没有,你就不会受到并发问题的保护。
You can modify the data inside a transaction directly from EF。
触发器也可以。但是,我不建议使用它:如果您不知道它存在,则很难检测到它存在。
在任何情况下,如果您只执行读取行的非常简单的操作并写入其他操作,则事务锁定不应表示性能问题。在存在之前不要尝试解决性能问题。
修改强> 性能问题是由长时间运行的事务创建的,即需要很长时间才能执行的事务,例如因为它们读取或写入大量数据(例如,更新包含具有许多相关行的子表的聚合的字段,或者在没有可用索引的数百万行的巨大表中查找数据。如果将序列保存在单行的单行中,读取它的速度非常快,并且希望在另一个表中写一行,也非常非常快,特别是如果聚簇索引(默认情况下是主键)聚集索引)允许在表的末尾写入行,就像在拥挤的表中一样。为确保数据在表的末尾处,每个新行应该是聚集索引中的最后一行。
答案 2 :(得分:1)
对于任何可能面临同样挑战的人,我最终确定的是使InvoiceNumber成为约束,并处理UniqueConstraintException并重新尝试获取InvoiceNumber。
为了避免无限循环(如果它曾经发生过),添加了一个“重试”计数器。
int retry = 5;
try
{
// Get new purchase order number from service
Invoice.InvoiceNumber = GetNewPurchaseOrderNumber();
// Explicit set state as Added
Invoice.ObjectState = ObjectState.Added;
_uow.Repository<Invoice>().InsertGraph(Invoice);
_uow.Save();
}
catch (DbUpdateException ex)
{
retry--;
if (null == ex.InnerException) throw;
var innerException = ex.InnerException.InnerException
as System.Data.SqlClient.SqlException;
if (innerException != null &&
(
innerException.Number == 2627 ||
innerException.Number == 2601)
&& retry > 0
)
{
// Get new Invoice Number from service
Invoice.InvoiceNumber = GetNewPurchaseOrderNumber();
_uow.Save();
}
else
{
throw;
}
}