有时候也好奇,若是老外发个技术文章,会不会到处是有人骂街的?进行人身攻击的?中国人喜欢打击别人,不知道老外是不是也是这个性格?好奇的问一下大家。
往往我们在开发程序、调试程序时,无法模拟多用户同时操作的实际环境下的运行情况。
为了模拟多用户并发操作,我们先写个多线程的例子来充分模拟多用户并发的情况
{
/// <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#2楼 回复 引用 查看
汗 这是很很基本的 。。#3楼 回复 引用 查看
恭喜老吉,终于可以在首页发文了.#4楼 回复 引用 查看
确实比较基础
#5楼 回复 引用 查看
单件模式.我唯一在工作中真正使用且有好的结果的一种设计模式..................................#6楼 回复 引用 查看
@小猪凯俺用注册表模式取代了单件模式
#7楼 回复 引用
还Lock技术,这不就是一个C#的语法糖, Lock关键字吗#8楼 回复 引用
这种方式你的程序只能有一个实例在跑!#9楼 回复 引用 查看
吉日终于发“技术”文章了#10楼 回复 引用 查看
用数据做自增主键?我们现在选择用时间序列了,性能高。
#11楼 回复 引用 查看
单服务器的情况下,这种方式可以。如果是多服务器负载均衡的配置,还是会生成重复序列号。#12楼 回复 引用
最后一段:“估计在国内的管理类软件,90%以上都没进行严格的多用户并发测试,90%以上的都没考虑应用程序并发问题及数据库的并发问题...“这段话好伤人。
#13楼 回复 引用
知道sqlserver里有一个@@identity是干嘛用的吗楼主?#14楼[楼主] 回复 引用 查看
@coolbeer@@identity? 有这个东西吗?哈哈。
#15楼[楼主] 回复 引用 查看
时间序列是什么东东?
#16楼 回复 引用 查看
@吉日嘎拉 不仅权限管理如果用表+事务控制主键,性能非常差劲。
如果用数据库自增,编程麻烦,移植麻烦。
我就用时间序列,因为时间序列一定是递增的。伪代码大概是:
long formerId = -1;
public long GenerateId()
{
while(true)
{
long currentId = DateTIme -> 精确到毫秒+不过java能够精确到纳秒
if(curentId == formerId) continue;
break;
}
formerId = currendId;
return currentId;
}
#17楼 回复 引用 查看
加上lock之后,就保证了唯一了#18楼[楼主] 回复 引用 查看
还真没明白李会军的单件模式与 多用户并发产生不重复递增单号的能扯上关系,我没那么聪明了。#19楼 回复 引用 查看
如果用毫秒,一秒钟最多生成1000条记录。oracle单线程极限是1400/s
mysql = 500/s
sqlserver = 1300/s
access = 1.4w/s
貌似对整体的性能影响不大。
如果不同的表结构+主键前缀,那么性能能提高n倍了。
#20楼 回复 引用
不明白楼主为啥不用sqlserver的自增ID,如果采用了自动增长的ID,就不用象现在这样脱裤子放屁多此一举,该功能sqlserver已经提供了,在你插入数据的时候这个样子 insert table([aa],[bb]) value('aa','bb')
select @@identity
即可取出当前用户的自增ID,并且不同用户肯定会取得自己增长的那个ID,不会出现并发情况,当然如果楼主非要自己来保证也可以
#21楼 回复 引用 查看
@coolbeer我觉得楼主的讨论是有现实意义的,比如要生成一个订单的唯一编号,要求是:前缀为公司名称,比如:IBeam_,后是当天日期,紧接着是当天的订单序列号,如果是这样的需求,要如何实现?
生成的编号可能是:IBeam_20100704_000001
数据库自增ID 是给机器阅读的,这个编号是给人阅读的,目标不一样,实现手法自然也有差异。
#22楼[楼主] 回复 引用 查看
@杨义金// 先获取日期
string date = DateTime.ToString("yyyymmdd");
string sequence = sequenceManager.GetSequence("Order" + date));
string returnValue = "IBeam_" + date + "_" + sequence;
#23楼 回复 引用 查看
为什么别人分享点技术大家都这么愤慨啊?不能老提溜着过去的那点事不放啊。
这里欢迎的是技术谈谈,又不是口水。与其口水,不如不回复。
#24楼 回复 引用 查看
我只想笑!#25楼 回复 引用 查看
我真不厚道。。。我是进来看吉日被喷的#26楼 回复 引用 查看
估计在国内的管理类软件,90%以上都没进行严格的多用户并发测试,90%以上的都没考虑应用程序并发问题及数据库的并发问题,若不是迫不得已越简单越省事就好,何必跟自己过不去呢,搞那么多繁琐的并发处理我笑,哈哈~~~
估计你们项目组没有测试人员,要不你也不会说"往往我们在开发程序、调试程序时,无法模拟多用户同时操作的实际环境下的运行情况。"
传说中的吉日啊....
#27楼 回复 引用 查看
1、可以用数据库事务达到相同效果,由于ASP.NET的进程不是唯一的(你不能创建跨进程锁),所以只有事务才能确保不出问题。2、数据库访问是一个低性能(或者说耗时比较长)的操作,在这种操作上设置互斥锁会导致程序退化成单线程(当然在你的例子中似乎没有利用多核体高性能的需求,毕竟这只是整个功能的一部分)
#28楼 回复 引用 查看
经验的积累,支持吉日!1,数据库事务取单号会造成断号,客户肯定抓狂。这一点,没做过的同学是没机会体验的。(就跟自增ID一样)
2,单号的格式一般是比较复杂的,否则也没必要生成单号,直接用自增好了。所以需要取出来,结合业务数据生成
3,回到原理上,大家都知道,生成不断号的递增单号,关键点在于锁。吉日的解决方案是做线程锁,比起没有锁的做法,是一个巨大的进步。但评论里面也有很多人提到,这个方案对分布式无效。实际上,吉日的方案能满足一定程度的要求,如果有更高的要求,最好的就是在数据库内部锁了。写个存储过程吧,在数据库上取单号,生成新单号。
突然想到一种新方法,不知道是否可行。
不要锁定,读取最大单号,生成新单号,然后写入数据库:
if 新单号不存在 then insert into……
然后检查返回的影响行数,如果影响行数不为1,重新处理一次。
这个方法,其实我是想利用数据表的锁,不知道对不对。
#29楼 回复 引用 查看
如果“流水号”不好控制的话,完全可以考虑采用“预置号”的方法,就象火车售票系统那样。#30楼 回复 引用 查看
@卡通一下说说解决办法?
===================================
学习了哈,
我的做法通常也是 加锁,取最大值+1 进行返回
#31楼 回复 引用 查看
预置号就是采用一张预置号表,预先设置一组号码,预置多少看系统使用频度。
另外,使用预置号仅仅是锁定行,而且对索引也没有什么影响,效率要比你取最大值+1高得多,特别适合多用户、高并发的方案。
#32楼 回复 引用
博客园真是用心良苦啊,“评论头条”都出来了。。。#33楼 回复 引用 查看
是什么样的成长经历让楼主有如此另类的性格和忽视一切的厚脸皮!!#34楼 回复 引用 查看
BS楼上。欢迎一切技术问题讨论,不管这个问题是高深还是不高深的...
因为人本身的深度是不一样的...
#35楼 回复 引用 查看
(1)你没有能够模拟大规模的并发请求访问;(或者你并没有遇到过这种场景);所以你的方法仅适用于用户规模比较小的情况(不会暴露出问题)。(2)对并发要求很高的地方是不能使用 lock 的,否则在高并发情况下会导致拒绝服务;因为拿不到序列号,导致死锁,超时等;
(3)可在数据库端控制;
#36楼 回复 引用 查看
“private static readonly object SequenceLock = new object();string returnValue = string.Empty;
// 这里用锁的机制,提高并发控制能力
lock (SequenceLock)”
必须纠正你这里的错误注释,你这里是全局性的lock,会强制并发请求进行排队,会称为并发的最大瓶颈(也就是完全禁止并发)。
lock 应该用于线程同步,访问独占性资源等场合。尤其在并发负荷很高的地方要避免使用。
#37楼[楼主] 回复 引用 查看
@hoodlum1980那听听大师,对数据库的并发控制理论如何?
你上面写得是不错,很棒。
#38楼 回复 引用 查看
@吉日嘎拉 不仅权限管理你不用这样叫我,因为我在项目中的确就有这种情况。因为获取流水号的存储过程偶发死锁,所以有人(最可怕的是不牛但自以为牛)自作聪明的加了lock(我是强烈反对的),结果版本马上被迫回滚了。因为大多数业务都需要获取流水号,所以这里的并发负荷很高。
原来获取流水号的存储过程会偶发死锁,因为即使是rowlock在并发时也可能会有争夺访问,后来请微软帮查看,把存储过程里的两条SQL语句,调整成只有一条SQL语句(相当于原子性的了)。这样就基本解决了死锁问题。
#39楼 回复 引用 查看
用事务怎么操作呢?说来听听
#40楼[楼主] 回复 引用 查看
@hoodlum1980一个 update 语句,一个select 语句,不知道如何能写成一个语句?稍微好奇的问一下。
#41楼 回复 引用 查看
@吉日嘎拉 不仅权限管理update ...; select ...;
#42楼[楼主] 回复 引用 查看
@jianyi不会吧?
#43楼 回复 引用 查看
同意hoodlum1980的说法#44楼 回复 引用 查看
调整成一条也就是一个隐式事务吧,虽然显式开启事务会糟糕一些,也不至于差那么大的说。。。。。改成INSERT INTO ... SELECT不就成一条了么?
#45楼 回复 引用 查看
INSERT INTO ... SELECT MAX( Serial ) + 1 AS Serial, 'aaa' AS AAA, 'bbb AS BBB ...#46楼 回复 引用 查看
估计在国内的管理类软件,90%以上都没进行严格的多用户并发测试,90%以上的都没考虑应用程序并发问题及数据库的并发问题,若不是迫不得已越简单越省事就好,何必跟自己过不去呢,搞那么多繁琐的并发处理。估计楼主一直没做,然后发现了就得此结论...
#47楼 回复 引用 查看
哦。。。这么深奥的。。。#48楼 回复 引用 查看
@hoodlum1980并发不高这样做并不错误,如果并发高的情况,尽量把lock的时间减少,如果多个业务都用这个lock,但之间没有冲突的情况就拆分成多个.
如有些帐务处理所有帐号用同一个lock问题就来了,但如果每个帐号只lock自己那在多的并发也不成问题.具体问题具体分析,有些时候代码在某些情况是不好,但在某些情况确很方便适合.
#49楼 回复 引用 查看
写成一条语句不是自欺欺人吗?数据库还是分两部分处理呀!
我觉得还是写在一个事务中,UPDATE成功取号之后,SELECT返回就是了。
#50楼 回复 引用 查看
@Ivony...仅用数据库事物恐怕不能解决问题,应该是数据库事务+加排它锁,可以防止,脏读、不可重复读、幻读。
SQL Server 中可以:
BEGIN TRAN
SELECT @maxid = max(id)+1 FROM test(XLOCK,PAGLOCK)
COMMIT TRAN
XLOCK 使用排它锁并一直保持到由语句处理的所有数据上的事务结束时。使用PAGLOCK或TABLOCK指定该锁,保证其它查询被堵塞。
#51楼 回复 引用 查看
连续单号在很多企业中是必须的,好多同学不一定见过这样的要求。但是你那个Lock的方案对于安装了10出个库存管理的客户端的系统,不如Ivony的数据库解决方案, 对于web 而言,也存在上面同学所说NLB的问题。 lock不支持进程间的同步,你需要Mutex或Semaphore, 即便是用了Mutex也解决不了NLB的问题。
#52楼 回复 引用 查看
一直都喜欢使用乐观锁去处理。#53楼[楼主] 回复 引用 查看
不小心被评论头条了,也很荣幸啊,哈哈。#54楼 回复 引用 查看
sql server 2005开始支持OUTupdate ... out ...
这篇文章是个讨论用的教材
#55楼 回复 引用
程序并发与数据库并发都要考虑为最好吧#56楼 回复 引用
@吉日嘎拉 不仅权限管理用数据库中用存储过程解决,借助数据库本身的并发控制机制
@newID int out
update sequence_table
set @newID = nowid + 1,
nowid = nowid + 1
where key = 'orderPrimarykey'
return @newID
#57楼[楼主] 回复 引用 查看
@路人F强啊,佩服了。
#58楼 回复 引用
其实事情就这么简单:)
#59楼 回复 引用 查看
在sql语句里加锁update tb_BH with(rowlock) ...
http://www.cnblogs.com/chenxumi/archive/2010/05/01/1725352.html
#60楼 回复 引用
select SCOPE_IDENTITY()
返回上面操作的数据表最后row的IDENTITY列的值
SELECT @@IDENTITY
返回上面操作最后一个表的最后row的IDENTITY列的值
#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
会处于等待状态,直到事务提交完毕,才返回结果,可见如果是严格并发要采用锁机制。另外,我没有搞清楚,如果单纯的事务是怎么解决并发问题的?请教了
#62楼 回复 引用 查看
@design-life你的做法我完全认同,可以达到预期的效果, 不过在SQL Server中,你也可以设置事务的隔离级别