您的位置:首页 > 编程语言 > ASP

C# ASP.NET B/S模式下,采用lock语法 实现多用户并发产生不重复递增单号的一种解决方法技术参考

2010-10-24 23:29 1366 查看
有时候也好奇,若是老外发个技术文章,会不会到处是有人骂街的?进行人身攻击的?中国人喜欢打击别人,不知道老外是不是也是这个性格?好奇的问一下大家。



往往我们在开发程序、调试程序时,无法模拟多用户同时操作的实际环境下的运行情况。

为了模拟多用户并发操作,我们先写个多线程的例子来充分模拟多用户并发的情况




代码

class SequenceTest
{
/// <summary>
/// 定义委托
/// </summary>
/// <param name="user">用户</param>
delegate void MakeSequenceDelegate(string user);

/// <summary>
/// 这里是测试序列
/// </summary>
/// <param name="user">用户</param>
private void MakeSequence(string user)
{
for (int i = 0; i < 10; i++)
{
BaseSequenceManager sequenceManager = new BaseSequenceManager();
// 模拟2010年7月份的订单编号产生
System.Console.WriteLine(user + ":" + sequenceManager.GetSequence("Order201007"));
}
}

/// <summary>
/// 这里是模拟多用户同时点击
/// </summary>
public void DoTest()
{
// 模拟3个用户的并发操作
MakeSequenceDelegate sequenceDelegate1 = new MakeSequenceDelegate(MakeSequence);
sequenceDelegate1.BeginInvoke("user1", null, null);
MakeSequenceDelegate sequenceDelegate2 = new MakeSequenceDelegate(MakeSequence);
sequenceDelegate2.BeginInvoke("user2", null, null);
MakeSequenceDelegate sequenceDelegate3 = new MakeSequenceDelegate(MakeSequence);
sequenceDelegate3.BeginInvoke("user3", null, null);
}
}



序列表的设计效果如下图,表中存储了当前是什么序列的序号为多少等信息。





由于没进行并发控制,程序的输出情况如下,当然在单用户操作测试时,是不太可能测试出并发情况下的运行状态的。



这里会有重复序列急丢失序列的情况会发生,并不能保证多用户并发时,能完全产生唯一的订单编号。

为什么会发生并发问题? 因为你在读的时候,我也在读,你在更新序列时,我也在更新序列,因为相同的程序在运行多份,用户1,2同时都读到了0007这个序号。



进行并发控制后的运行效果如下:



这里序号是连续的,而且是没有丢失情况,也没重复情况发生。



这里是如何避免并发?BaseSequenceManager中进行了如下排斥并发的加工。





private static readonly object SequenceLock = new object();



    string returnValue = string.Empty;

// 这里用锁的机制,提高并发控制能力
lock (SequenceLock)
{

returnValue = 读取数据库中的当前序列值(一)

更新数据库中的序列(二)

}

return returnValue;



因为数据库的读取,更新,需要2步操作,是导致了并发问题的所在。



以上文章主要涉及到如下技术问题:

1:需要能写出多线程的模拟程序。

2:多线程函数如何传递参数需要学会。

3:这也算是所谓的核心基础组件的自动化测试吧。

4: lock 语句(C# 参考) http://msdn.microsoft.com/zh-cn/library/c5kehkcz(VS.80).aspx



测试起来好用的管理软件,真正投放到实际生产环境中往往会发生很多意想不到的错误,这往往是没能重复测试多用户并发情况下的运行情况导致的占一部分。

以上程序虽然没什么大难度,下午耗费了接近2-3个小时,才调整好,希望对读者评估工作量能有个参考。



估计在国内的管理类软件,90%以上都没进行严格的多用户并发测试,90%以上的都没考虑应用程序并发问题及数据库的并发问题,若不是迫不得已越简单越省事就好,何必跟自己过不去呢,搞那么多繁琐的并发处理。



将权限管理、工作流管理做到我能力的极致,一个人只能做好那么很少的几件事情。

posted on 2010-07-04 18:23 吉日嘎拉 不仅权通用权限 阅读(3845) 评论(63) 编辑 收藏



评论

1865411

#1楼  回复 引用 查看  说的那么复杂,看看李会军的单件模式就明白了

2010-07-04 18:32 | 绝代恭敬

#2楼  回复 引用 查看 

汗 这是很很基本的 。。
2010-07-04 18:48 | xiaotie

#3楼  回复 引用 查看 

恭喜老吉,终于可以在首页发文了.
2010-07-04 18:49 | Cocoo

#4楼  回复 引用 查看 

确实
比较基础
2010-07-04 18:50 | liulun

#5楼  回复 引用 查看 

单件模式.我唯一在工作中真正使用且有好的结果的一种设计模式..................................
2010-07-04 18:58 | 小猪凯

#6楼  回复 引用 查看 

@小猪凯
俺用注册表模式取代了单件模式
2010-07-04 19:18 | xiaotie

#7楼  回复 引用 

还Lock技术,这不就是一个C#的语法糖, Lock关键字吗
2010-07-04 19:19 | YunanwOffLine[未注册用户]

#8楼  回复 引用 

这种方式你的程序只能有一个实例在跑!
2010-07-04 19:44 | 不解[未注册用户]

#9楼  回复 引用 查看 

吉日终于发“技术”文章了
2010-07-04 19:59 | 绯雨

#10楼  回复 引用 查看 

用数据做自增主键?

我们现在选择用时间序列了,性能高。
2010-07-04 20:04 |

#11楼  回复 引用 查看 

单服务器的情况下,这种方式可以。如果是多服务器负载均衡的配置,还是会生成重复序列号。
2010-07-04 21:00 | Nominee

#12楼  回复 引用 

最后一段:“估计在国内的管理类软件,90%以上都没进行严格的多用户并发测试,90%以上的都没考虑应用程序并发问题及数据库的并发问题...“

这段话好伤人。
2010-07-04 21:06 | 不明真相的围观者[未注册用户]

#13楼  回复 引用 

知道sqlserver里有一个@@identity是干嘛用的吗楼主?
2010-07-04 22:14 | coolbeer[未注册用户]

#14楼[楼主]  回复 引用 查看 

@coolbeer

@@identity? 有这个东西吗?哈哈。

2010-07-04 22:29 | 吉日嘎拉 不仅权限管理

#15楼[楼主]  回复 引用 查看 

引用辰:
用数据做自增主键?

我们现在选择用时间序列了,性能高。

时间序列是什么东东?

2010-07-04 22:29 | 吉日嘎拉 不仅权限管理

#16楼  回复 引用 查看 

@吉日嘎拉 不仅权限管理

如果用表+事务控制主键,性能非常差劲。

如果用数据库自增,编程麻烦,移植麻烦。

我就用时间序列,因为时间序列一定是递增的。伪代码大概是:

long formerId = -1;

public long GenerateId()
{
while(true)
{
long currentId = DateTIme -> 精确到毫秒+不过java能够精确到纳秒
if(curentId == formerId) continue;
break;
}
formerId = currendId;
return currentId;
}

2010-07-04 22:33 |

#17楼  回复 引用 查看 

加上lock之后,就保证了唯一了
2010-07-04 22:34 |

#18楼[楼主]  回复 引用 查看 

还真没明白李会军的单件模式与 多用户并发产生不重复递增单号的能扯上关系,我没那么聪明了。

引用绝代恭敬:
说的那么复杂,看看李会军的单件模式就明白了

2010-07-04 22:36 | 吉日嘎拉 不仅权限管理

#19楼  回复 引用 查看 

如果用毫秒,一秒钟最多生成1000条记录。

oracle单线程极限是1400/s
mysql = 500/s
sqlserver = 1300/s
access = 1.4w/s

貌似对整体的性能影响不大。

如果不同的表结构+主键前缀,那么性能能提高n倍了。
2010-07-04 22:41 |

#20楼  回复 引用 

不明白楼主为啥不用sqlserver的自增ID,如果采用了自动增长的ID,就不用象现在这样脱裤子放屁多此一举,该功能sqlserver已经提供了,在你插入数据的时候
这个样子 insert table([aa],[bb]) value('aa','bb')
select @@identity
即可取出当前用户的自增ID,并且不同用户肯定会取得自己增长的那个ID,不会出现并发情况,当然如果楼主非要自己来保证也可以
2010-07-04 22:56 | coolbeer[未注册用户]

#21楼  回复 引用 查看 

@coolbeer
我觉得楼主的讨论是有现实意义的,比如要生成一个订单的唯一编号,要求是:前缀为公司名称,比如:IBeam_,后是当天日期,紧接着是当天的订单序列号,如果是这样的需求,要如何实现?

生成的编号可能是:IBeam_20100704_000001

数据库自增ID 是给机器阅读的,这个编号是给人阅读的,目标不一样,实现手法自然也有差异。
2010-07-04 23:14 | 杨义金

#22楼[楼主]  回复 引用 查看 

@杨义金

// 先获取日期
string date = DateTime.ToString("yyyymmdd");
string sequence = sequenceManager.GetSequence("Order" + date));
string returnValue = "IBeam_" + date + "_" + sequence;

2010-07-04 23:35 | 吉日嘎拉 不仅权限管理

#23楼  回复 引用 查看 

为什么别人分享点技术大家都这么愤慨啊?
不能老提溜着过去的那点事不放啊。

这里欢迎的是技术谈谈,又不是口水。与其口水,不如不回复。
2010-07-05 00:34 | handt

#24楼  回复 引用 查看 

我只想笑!
2010-07-05 00:37 | 思益工作室

#25楼  回复 引用 查看 

我真不厚道。。。我是进来看吉日被喷的
2010-07-05 01:24 | birdshome

#26楼  回复 引用 查看 

估计在国内的管理类软件,90%以上都没进行严格的多用户并发测试,90%以上的都没考虑应用程序并发问题及数据库的并发问题,若不是迫不得已越简单越省事就好,何必跟自己过不去呢,搞那么多繁琐的并发处理

我笑,哈哈~~~
估计你们项目组没有测试人员,要不你也不会说"往往我们在开发程序、调试程序时,无法模拟多用户同时操作的实际环境下的运行情况。"

传说中的吉日啊....
2010-07-05 02:05 | 土豆烤肉

#27楼  回复 引用 查看 

1、可以用数据库事务达到相同效果,由于ASP.NET的进程不是唯一的(你不能创建跨进程锁),所以只有事务才能确保不出问题。

2、数据库访问是一个低性能(或者说耗时比较长)的操作,在这种操作上设置互斥锁会导致程序退化成单线程(当然在你的例子中似乎没有利用多核体高性能的需求,毕竟这只是整个功能的一部分)
2010-07-05 03:20 | Ivony...

#28楼  回复 引用 查看 

经验的积累,支持吉日!

1,数据库事务取单号会造成断号,客户肯定抓狂。这一点,没做过的同学是没机会体验的。(就跟自增ID一样)
2,单号的格式一般是比较复杂的,否则也没必要生成单号,直接用自增好了。所以需要取出来,结合业务数据生成
3,回到原理上,大家都知道,生成不断号的递增单号,关键点在于锁。吉日的解决方案是做线程锁,比起没有锁的做法,是一个巨大的进步。但评论里面也有很多人提到,这个方案对分布式无效。实际上,吉日的方案能满足一定程度的要求,如果有更高的要求,最好的就是在数据库内部锁了。写个存储过程吧,在数据库上取单号,生成新单号。

突然想到一种新方法,不知道是否可行。
不要锁定,读取最大单号,生成新单号,然后写入数据库:
if 新单号不存在 then insert into……
然后检查返回的影响行数,如果影响行数不为1,重新处理一次。

这个方法,其实我是想利用数据表的锁,不知道对不对。
2010-07-05 03:33 | 大石头

#29楼  回复 引用 查看 

如果“流水号”不好控制的话,完全可以考虑采用“预置号”的方法,就象火车售票系统那样。

2010-07-05 07:53 | 卡通一下

#30楼  回复 引用 查看 

@卡通一下
说说解决办法?

===================================
学习了哈,
我的做法通常也是 加锁,取最大值+1 进行返回
2010-07-05 08:47 | COOL-CHEN

#31楼  回复 引用 查看 

引用COOL-CHEN:
@卡通一下
说说解决办法?
===================================
学习了哈,
我的做法通常也是 加锁,取最大值+1 进行返回
预置号就是采用一张预置号表,预先设置一组号码,预置多少看系统使用频度。

另外,使用预置号仅仅是锁定行,而且对索引也没有什么影响,效率要比你取最大值+1高得多,特别适合多用户、高并发的方案。

2010-07-05 08:53 | 卡通一下

#32楼  回复 引用 

博客园真是用心良苦啊,“评论头条”都出来了。。。
2010-07-05 09:13 | Castle.Net[未注册用户]

#33楼  回复 引用 查看 

是什么样的成长经历让楼主有如此另类的性格和忽视一切的厚脸皮!!
2010-07-05 09:30 | discover

#34楼  回复 引用 查看 

BS楼上。

欢迎一切技术问题讨论,不管这个问题是高深还是不高深的...
因为人本身的深度是不一样的...
2010-07-05 10:37 | 农村的芬芳

#35楼  回复 引用 查看 

(1)你没有能够模拟大规模的并发请求访问;(或者你并没有遇到过这种场景);所以你的方法仅适用于用户规模比较小的情况(不会暴露出问题)。

(2)对并发要求很高的地方是不能使用 lock 的,否则在高并发情况下会导致拒绝服务;因为拿不到序列号,导致死锁,超时等;

(3)可在数据库端控制;

2010-07-05 10:49 | hoodlum1980

#36楼  回复 引用 查看 

“private static readonly object SequenceLock = new object();
string returnValue = string.Empty;
// 这里用锁的机制,提高并发控制能力
lock (SequenceLock)”
必须纠正你这里的错误注释,你这里是全局性的lock,会强制并发请求进行排队,会称为并发的最大瓶颈(也就是完全禁止并发)。

lock 应该用于线程同步,访问独占性资源等场合。尤其在并发负荷很高的地方要避免使用。

2010-07-05 10:53 | hoodlum1980

#37楼[楼主]  回复 引用 查看 

@hoodlum1980

那听听大师,对数据库的并发控制理论如何?
你上面写得是不错,很棒。

2010-07-05 10:56 | 吉日嘎拉 不仅权限管理

#38楼  回复 引用 查看 

@吉日嘎拉 不仅权限管理
你不用这样叫我,因为我在项目中的确就有这种情况。因为获取流水号的存储过程偶发死锁,所以有人(最可怕的是不牛但自以为牛)自作聪明的加了lock(我是强烈反对的),结果版本马上被迫回滚了。因为大多数业务都需要获取流水号,所以这里的并发负荷很高。

原来获取流水号的存储过程会偶发死锁,因为即使是rowlock在并发时也可能会有争夺访问,后来请微软帮查看,把存储过程里的两条SQL语句,调整成只有一条SQL语句(相当于原子性的了)。这样就基本解决了死锁问题。
2010-07-05 11:07 | hoodlum1980

#39楼  回复 引用 查看 

引用Ivony...:
1、可以用数据库事务达到相同效果,由于ASP.NET的进程不是唯一的(你不能创建跨进程锁),所以只有事务才能确保不出问题。

2、数据库访问是一个低性能(或者说耗时比较长)的操作,在这种操作上设置互斥锁会导致程序退化成单线程(当然在你的例子中似乎没有利用多核体高性能的需求,毕竟这只是整个功能的一部分)
用事务怎么操作呢?说来听听
2010-07-05 11:07 | Will Meng

#40楼[楼主]  回复 引用 查看 

@hoodlum1980

一个 update 语句,一个select 语句,不知道如何能写成一个语句?稍微好奇的问一下。

2010-07-05 11:22 | 吉日嘎拉 不仅权限管理

#41楼  回复 引用 查看 

@吉日嘎拉 不仅权限管理
update ...; select ...;
2010-07-05 11:54 | jianyi

#42楼[楼主]  回复 引用 查看 

@jianyi

不会吧?
2010-07-05 11:55 | 吉日嘎拉 不仅权限管理

#43楼  回复 引用 查看 

同意hoodlum1980的说法
2010-07-05 11:58 | Assion Yang

#44楼  回复 引用 查看 

调整成一条也就是一个隐式事务吧,虽然显式开启事务会糟糕一些,也不至于差那么大的说。。。。。

改成INSERT INTO ... SELECT不就成一条了么?
2010-07-05 12:37 | Ivony...

#45楼  回复 引用 查看 

INSERT INTO ... SELECT MAX( Serial ) + 1 AS Serial, 'aaa' AS AAA, 'bbb AS BBB ...
2010-07-05 12:38 | Ivony...

#46楼  回复 引用 查看 

估计在国内的管理类软件,90%以上都没进行严格的多用户并发测试,90%以上的都没考虑应用程序并发问题及数据库的并发问题,若不是迫不得已越简单越省事就好,何必跟自己过不去呢,搞那么多繁琐的并发处理。

估计楼主一直没做,然后发现了就得此结论...
2010-07-05 12:45 | henry

#47楼  回复 引用 查看 

哦。。。这么深奥的。。。
2010-07-05 13:01 | koscarkos

#48楼  回复 引用 查看 

@hoodlum1980
并发不高这样做并不错误,如果并发高的情况,尽量把lock的时间减少,如果多个业务都用这个lock,但之间没有冲突的情况就拆分成多个.
如有些帐务处理所有帐号用同一个lock问题就来了,但如果每个帐号只lock自己那在多的并发也不成问题.具体问题具体分析,有些时候代码在某些情况是不好,但在某些情况确很方便适合.

2010-07-05 13:27 | henry

#49楼  回复 引用 查看 

引用吉日嘎拉 不仅权限管理:
@hoodlum1980

一个 update 语句,一个select 语句,不知道如何能写成一个语句?稍微好奇的问一下。
写成一条语句不是自欺欺人吗?数据库还是分两部分处理呀!

我觉得还是写在一个事务中,UPDATE成功取号之后,SELECT返回就是了。

2010-07-05 14:13 | 卡通一下

#50楼  回复 引用 查看 

@Ivony...

仅用数据库事物恐怕不能解决问题,应该是数据库事务+加排它锁,可以防止,脏读、不可重复读、幻读。

SQL Server 中可以:
BEGIN TRAN
SELECT @maxid = max(id)+1 FROM test(XLOCK,PAGLOCK)

COMMIT TRAN

XLOCK 使用排它锁并一直保持到由语句处理的所有数据上的事务结束时。使用PAGLOCK或TABLOCK指定该锁,保证其它查询被堵塞。
2010-07-05 14:51 | design-life

#51楼  回复 引用 查看 

连续单号在很多企业中是必须的,好多同学不一定见过这样的要求。

但是你那个Lock的方案对于安装了10出个库存管理的客户端的系统,不如Ivony的数据库解决方案, 对于web 而言,也存在上面同学所说NLB的问题。 lock不支持进程间的同步,你需要Mutex或Semaphore, 即便是用了Mutex也解决不了NLB的问题。
2010-07-05 15:56 | 梦幻天涯

#52楼  回复 引用 查看 

一直都喜欢使用乐观锁去处理。
2010-07-05 16:30 | 汝熹

#53楼[楼主]  回复 引用 查看 

不小心被评论头条了,也很荣幸啊,哈哈。

2010-07-05 17:26 | 吉日嘎拉 不仅权限管理

#54楼  回复 引用 查看 

sql server 2005开始支持OUT

update ... out ...

这篇文章是个讨论用的教材
2010-07-05 18:04 | wiseshrek

#55楼  回复 引用 

程序并发与数据库并发都要考虑为最好吧
2010-07-05 18:33 | kevinlzf[未注册用户]

#56楼  回复 引用 

@吉日嘎拉 不仅权限管理

用数据库中用存储过程解决,借助数据库本身的并发控制机制

@newID int out

update sequence_table
set @newID = nowid + 1,
nowid = nowid + 1
where key = 'orderPrimarykey'

return @newID
2010-07-05 19:35 | 路人F[未注册用户]

#57楼[楼主]  回复 引用 查看 

@路人F

强啊,佩服了。

2010-07-05 20:11 | 吉日嘎拉 不仅权限管理

#58楼  回复 引用 

引用吉日嘎拉 不仅权限管理:
@路人F

强啊,佩服了。

其实事情就这么简单:)
2010-07-05 20:52 | 路人F[未注册用户]

#59楼  回复 引用 查看 

在sql语句里加锁
update tb_BH with(rowlock) ...
http://www.cnblogs.com/chenxumi/archive/2010/05/01/1725352.html
2010-07-05 21:40 | chenxumi

#60楼  回复 引用 

引用吉日嘎拉 不仅权限管理:
@coolbeer

@@identity? 有这个东西吗?哈哈。

select SCOPE_IDENTITY()

返回上面操作的数据表最后row的IDENTITY列的值

SELECT @@IDENTITY

返回上面操作最后一个表的最后row的IDENTITY列的值

2010-07-05 22:40 | 有这个东西[未注册用户]

#61楼  回复 引用 查看 

@梦幻天涯
@梦幻天涯
我测试过了如果仅用事务

declare @maxId int
begin tran

select @maxId = max(id)+1 from genCode

select @maxId

同时再开一个连接,执行
select max(id)+1 from genCode
返回的结果是同一个,可见当事务没有执行完毕时,会出现脏读。

反之,如果采用事务加锁的机制:
declare @maxId int
begin tran

select @maxId = max(id)+1 from genCode with(xlock,paglock)

select @maxId

不提交事务,执行
select max(id)+1 from genCode
会处于等待状态,直到事务提交完毕,才返回结果,可见如果是严格并发要采用锁机制。另外,我没有搞清楚,如果单纯的事务是怎么解决并发问题的?请教了
2010-07-06 08:57 | design-life

#62楼  回复 引用 查看 

@design-life

你的做法我完全认同,可以达到预期的效果, 不过在SQL Server中,你也可以设置事务的隔离级别
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐