如何解决ado.net中的并发冲突



这样一个场景:在表中插入一定数量的数据,当达到阈值时不再插入,我模拟了这种场景,在多线程(如asp.net)出现并发问题的情况下。

我的问题是如何解决并发问题,不要使用lock情况下的

void Main()
{
   Enumerable.Range(0,20).ToList().ForEach(i=>{
       MockMulit();
   });
}
//Start a certain number of threads for concurrent simulation
void MockMulit()
{
  int threadCount=100;
  ClearData();//delete all data for test

  var tasks=new List<Task>(threadCount);
  Enumerable.Range(1,threadCount).ToList().ForEach(i=>{
     var j=i;
     tasks.Add(Task.Factory.StartNew(()=>T3(string.Format("Thread{0}-{1}",Thread.CurrentThread.ManagedThreadId,j))));
  });
  Task.WaitAll(tasks.ToArray());
  CountData().Dump();//show that the result
}

方法一-并发性非常严重的

void T1(string name)
{
        using(var conn=GetOpendConn())
        {
            var count=conn.Query<int>(@"select count(*)  from dbo.Down").Single();
            if(count<20)
            {
                conn.Execute(@"insert into dbo.Down (UserName) values (@UserName)",new{UserName=name});
            }
        }
}

方法二-将sql放在一起可以减少并发,但仍存在

void T2(string name)
{
   using(var conn=GetOpendConn())
   {
       conn.Execute(@"
                      if((select count(*)  from dbo.Down)<20)
                        begin
                          --WAITFOR DELAY '00:00:00.100';
                          insert into dbo.Down (UserName) values (@UserName)
                        end",new{UserName=name});
   }
}

方法三-用锁破坏并发,但我不认为这是的最佳解决方案

private static readonly object countLock=new object();
void T3(string name)
{
    lock(countLock)
    {
        using(var conn=GetOpendConn())
        {
            var count=conn.Query<int>(@"select count(*)  from dbo.Down").Single();
            if(count<20)
            conn.Execute(@"insert into dbo.Down (UserName) values (@UserName)",new{UserName=name});
        }
    }
}

其他帮助方法

//delete all data
void ClearData()
{
   using(var conn=GetOpendConn())
   {
       conn.Execute(@"delete from dbo.Down");
   }
}
//get count
int CountData()
{
   using(var conn=GetOpendConn())
   {
      return conn.Query<int>(@"select count(*)  from dbo.Down").Single();
   }
}
//get the opened connection
DbConnection GetOpendConn()
{
    var conn=new SqlConnection(@"Data Source=.;Integrated Security=SSPI;Initial Catalog=TestDemo;");
    if(conn.State!=ConnectionState.Open)
       conn.Open();
    return conn;
}

听起来你只想在少于20行的情况下插入Down。如果是:将其作为单一操作:

insert dbo.Down (UserName)
select @UserName
where (select count(1) from dbo.Down) < 20
select @@rowount -- 1 if we inserted, 0 otherwise

或者,如果*需要*,则需要使用一个事务,最好是"Serializable",以便获得密钥范围锁-甚至可能在初始计数中添加(UPDLOCK),以确保它获得一个热切的写锁(或多个块,而不是死锁)。但是:单个TSQL操作(如图所示)更可取。你甚至可以让它变得更偏执(尽管我不确定它是否需要):

declare @count int
begin tran
insert dbo.Down (UserName)
select @UserName
where (select count(1) from dbo.Down (UPDLOCK)) < 20
set @count = @@rowount
commit tran
select @count -- 1 if we inserted, 0 otherwise

考虑反过来做,可能会简单得多。例如:

  • 创建一个具有自动递增索引的表。可以称之为"Up"或"RequestOrdering",或其他什么
  • 以任何顺序获取客户端请求。对于每个请求:
    • 在Up中插入新行
    • 获取最后插入的行ID
    • 如果最后一个插入ID<=20,做一个真正的插入
  • 用完后把你的上桌扔掉

如果您的数据库支持多列主键,其中一个主键自动递增(IIRC、MyISAM表支持),则可以在多个产品特价中重复使用"Up"表。


一个更简单的实现是在"Down"中插入20行,并在每个请求中删除一行。检查受影响的行数(应为1)以查看用户是否成功。这与多种产品的特色菜配合得很好。例如:

delete * from Down where down_special_id = Xxx limit 1

也许最简单,也为了跟踪谁"赢了",为每个产品创建一行,并让每个用户更新一行(而且只有一行)。再次检查受影响的行数,看看它们是否成功:

update Up
set
  user_name = @user_name
where
  user_name is null
  and product_id = @product_id
limit 1

最新更新