防止并发错误的问题。 From: BaKuBaKu(200分)

  • 主题发起人 主题发起人 BaKuBaKu
  • 开始时间 开始时间
所以我说必须要改一下数据结构和做法


 
我的建议:
设计一个服务器程序专门用来在服务器管理一个信号灯变量,这个我想
很容易做吧,每次要提交数据的时候,去问问这个服务器程序,是否能够提
交,如果能的话,就提交,并占有信号灯变量(当然是让服务器程序做),
如果不能就等待,提交数据以后,释放这个信号灯。
 
所以我用SP嘛. 我记得哪本书上提到过: SQL Server的SP是不可中断的. 因此, 不用
担心在取到@n后数据表会被其他用户改变. (除非我记错了)
 
说明一下:
@n 是返回值
@UsedNum是你传进来的你一个人的消耗量

>> if @n >= 0 then
-----> 表明要保存的数量+表中原始数量>=库存数量

>> update 表 set 消耗数量=库存, 标记=1 where id = @id // !!!
---->偷偷懒嘛, n>=0时表明你的消耗量+原来表中的消耗量已经超过(或者正好=)库
存量了, n的值就是超过库存的具体数目. (又不可能超过库存数量, 为什么不直接把库
存量赋值到消耗量呢?), 有什么可大惊小怪的?

>> else
>> update 表 set 消耗数量=消耗数量+@UsedNum where .... // !!!
----> 你传进来的是你一个人的消耗量, 当然应当在总消耗量上加(别忘了前面的判断,
消耗数量+@UsedNum 肯定 < 库存量的.

代码逻辑上绝对没有问题, 现在的关键只是有关SQL Server的SP是否可中断的问题了.

如果SP可中断, 那么需要另加一个信号量字段, 取数与修改前加锁, 改完了解锁, 其实
也很简单. 只是在取n值的代码前加一句:
Update 表 Set 信号量=1 where (id = @id) and (信号量=0)

然后判断@@Count这个系统变量(是叫这名吗?指示上一次操作的记录数), 如果> 0
则表示加锁成功, 可以进行下面的操作, 并在最后返回前
Update 表 Set 信号量=0 where id = @id
如果为0表示已经被别人锁住了, 可以另外进行处理(或者返回一个特殊值--比如-1提示
让客户端重试).
 
可以再建一张库存商品明细表。结构如下:
ID 商品 数量 平均采购单价 已售数量 是否售完
1 杯子 100 个 2.00 元 0 No
2 盘子 200 个 3.00 元 0 No
...

下次若采购库存中已经有的商品,如杯子100 单价 4:00元, 则更新以上的记录为:
ID 商品 库存数量 平均采购单价 已售数量 是否售完
1 杯子 200 个 3.00元 0 No //该记录被更新
2 盘子 200 个 3.00 元 0 No
...
//其中,
数量=原有数量+本次采购数量
平均采购单价=[(库存数量+已售数量)*平均采购单价)+
(本次采购数量*本次采购单价)]/
(库存数量+已售数量+本次采购数量)


以后每台客户机就读取该表的数据。这样问题就解决了。




 
我的做法是这样的,由于我的进/出仓单是放在同一个表的,主表用1表示进仓,-1表示出
仓,在从表增加一个字是已销售数量(一定要对应进仓单),在销售时,可以根据从表的
货品、批次、数量、单价和主表的1表示进仓、-1表示出仓得出库存(1*数量+(-1)*数量),
这样销售时,就可以看到库存,每销售一批货物就把对应的货物和数量update到进/出仓单
的已销售数量就可以了。
 
To lczhuohuo: 请问怎样在数据库的存储过程脚本中创建信号量?CreateMutex 是 API
函数啊。还有,即使创建成功了,却变成了串行的模式。我的目的是在
尽可能并发的情况下,保证数据的正确性。

Re Another_eYes: (来自:Another_eYes 时间:00-9-22 1:09:42 ID:344810)
你说的:“关键只是有关SQL Server的SP是否可中断的问题”,我的看法是这样的,
SP 不就是一段代码吗?就像多任务环境下一样,每时每刻 SQL Server 中都有大量的 SP
在并发执行,(如果 SQL Server 只允许所有的 SP 串行操作,那么效率未免太低了吧),
比如 SP1 正在执行第一条时,SP2 可能正在执行第二条。而且一个 SP 不可能一进入就
把它所用到的表全部占有(锁定),而是用到的时候才加锁(比如 Update 时加一个
Exclusive Lock,Select 时加一个 Shared Lock)。
因此说, eYes 的方法在执行第一条 Select 之后,第二条 Update 之前,别的工作
站还是有可能在这一瞬间修改数据的,并不是说把这些语句写到 SP 中就可以保证无误了。
至于 eYes 的“信号灯字段”方法,在系统中会出现“影子”数据,而且事务没有提交
之前,谁也不能保证该字段写入后别的 SP 能否看到,所以就不考虑了。

To 地平线:
要是商业成本核算真的像这么简单就好了,关键是“先入先出”啊,不是像你说的一样
用“平均采购单价”来计算成本的。“先入先出”是指先卖出最先买进的商品,并按照实际
采购价格计算成本。“先入先出”只是一种方法,还有“后入先出”、“全月平均”、
“加权平均”、“个别计价”等很多种呢。

To fstao: 你的方法也不解决问题的。

To 温柔一刀: 我可以考虑改变数据结构和做法,愿洗耳恭听。

From: BaKuBaKu
 
不会啊,我都是这样做的,30个用户并发输入,用了一年没有出现任何问题,你怎么知道我
的方法不行啊?你试过没有?
 
to BaKuBaKu:

如果可以改变数据结构和做法,我认为很简单,
在销售明细表上增加一个字段,TimeStamp类型,
用于记录发生业务的时间,而不去实时计算利润。

在一天结束后,运行一个程序,按照业务发生先后,
逐笔计算利润/成本,计算方法实际上就是你原来
实时计算的方法,只是放到最后一次性计算,从而
彻底避免并发问题,由于不需要实时计算,没有任何冲突,
并发性也获得了最大保证。

如果你不是要每卖出一块肥皂就要知道“利润”的话,
这样做应该可以了。就我所知商场每天结束总会有一些
类似结账的程序,只不过多加一个罢了。
 
>>因此说, eYes 的方法在执行第一条 Select 之后,第二条 Update 之前,别的工作
>>站还是有可能在这一瞬间修改数据的,并不是说把这些语句写到 SP 中就可以保证无误了

绝对不可能:
首先你是否承认在一个时刻SQL只能执行一条UPDATE(哪怕有一万个client同时传递
相同的SQL语句, Server也是顺序执行而不是并发的)?
因此:
如果你进入SP的第一句是
Update 表 set 信号量=1 where (id = @id) and <font color='red'><strong>(信号量=0)</strong></font>
如果有其他用户正在访问(信号量字段=1), 这句语句将修改0条记录.
其次, 请您注意: 我并未用Transection, 所以, 修改将马上提交到表中, 不存在"影子"问题.
此时再@n = select ..... 将保证读出的记录无人修改过.
只要你未执行Update 表 set 信号量=0, 其他sp将绝对不可能执行修改.
由于用的是SP, 所以不担心客户端中途断线的事发生, 即使用户断线, Server也会将
SP执行完成(除非执行到一半时服务器断电了, 不过真的出现这种情况, 无论你用什
么方法都会得到错误的结果), 而此时信号量字段已经复位可以让别人使用了.
实在想象不出您说的冲突(或者说脏读的数据)从何而来.
这个解决方案考不考虑是您的事, 但是你要说这种方法有冲突可能, 我绝对不同意.
 
我说这么一段话:
编程序如果出现这些情况的时候,我一般是这样的,想一想现实生活中
如果是人而不是程序来完成这些工作,应该怎么做!这个问题,如果是若干
个售货员不通过程序进行操作,那么他的做法我想应该是在每次可能打扰到
其他人的工作的时候会事先通知一下,问一下大家,我要把这些东西卖出去
了,这样做有问题吗?如果没人反对,他就卖,如果有人反对的话,他就研
究一下,为什么反对?他应该怎么办?而同样的当我听到一个人喊的时候,
我就会听一下,他喊的事情给我有没有关系,我是不是要采取一些措施。总
之我觉得售货员之间的交流是不可避免的了。至于交流的方式,有很多方法
,可以在server上面维护一个action表,也可以客户之间直接交流——不过
说实在的,确实很复杂啊。
不知道是不是屁话,嘿嘿!
 
Another_eYes,你的确实有问题,因为这样
先select,然后update,这里面重要的是update是按照select的结果来
update的,也就是说,如果在update的时候状态不是select那时候的状态
了,那么就是错误的。
 
>>然后判断@@Count这个系统变量(是叫这名吗?指示上一次操作的记录数), 如果> 0
>>则表示加锁成功, 可以进行下面的操作,
lczhuohuo, 您没看到上面一句吗?
根本不可能出现Select和后面update时状态两样(我认为你指的是信号量字段的值)的
情况
 
如果是POS,我觉得温柔一刀说得对,
为什么要每卖出去一块肥皂就要算一次利润呢?
客户机只记录销售情况就可以了,
连库存都不用去管。

想一想,几十台机用条码扫描输入,
每秒每台机有可能达到两条记录,
在这种情况下做这么多的后台工作,
岂不是给自己找烦?

建议用每班或每天结转的方式,
在结转时才冲库存和算利润。

我觉得POS的应是:
1.顾客都是拿着货交钱的,所以,只要顾客拿货来交钱,你就要录入进去,
总不会对顾客说:噢,电脑说这货已卖完了.........
2.客户程序速度要高,永远不要让顾客等,所以,少做点后台的事就是了。
 
很高兴能告诉各位,我想我应该已经找到了正确的答案。
SQL Server 7 的 T/SQL 语言中的 Select 语句,允许在 From 子句中加入附加的锁定
选项,像这样写:
Select * From ... <font color = #ff0000>With (HOLDLOCK)</font>
括号里还可以是 NOLOCK 等等的好几个其他的选项,奇怪我以前怎么从来就没有注意过。
文档中说,这个 LOCK 在 Select 后会一直保持到事务被 Commit 时,其间,任何 Update
操作都将被阻止(等待),或者超时后发生异常。
为了证明,我特意做了一个例子测试:(下面是伪码)
Button1Click: (保持锁定,并不结束事务)
Database1.StartTransaction;
Select * From 表 With (HOLDLOCK)
Button2Click: (提交事务,表的锁定将自动解除)
Database1.Commit;
Button3Click: (试图修改表)
Set Timeout 5000 (设置最大等待时间为 5 秒)
Update 表 Set ...=...;
我在两台机器上同时运行这个程序:
1、A 机器首先执行 Button1Click;(Select 并保持锁)
2、B 机器其次执行 Button3Click;(这时将等待,因为该表已被锁定)
3、A 机器执行 Button2Click;(锁定解除,B 的 Update 同时成功,等待结束)
期间如果 A 超过 5 秒没有提交事务,B 的事务将回滚,同时触发异常。
我想这应该是标准的解决方法,但是又一个现象很奇怪,上面的演示过程在进行第一次时
很正常,但是再执行就不对了,具体是 B 机器的 Update 不再等待了,什么时候都能成功,
把程序退出后重新进入就可以了,但总是只能正确地做一次,怪!怪!怪!
SQL Server 7 的锁机制内容很多,在 Books Online 里查找 Locking 主题会出来很多
小分支主题,比如 Cursor Locking、Lock Conflict,还有行级锁,页面锁、表锁、
数据库锁等等,我看着看着就晕了,不过总算是摸着了一点道道。
像 eYes 提出的“信号量”方案,在实际中采用似乎不太现实,但是对于这个方案的正确
性,我一时也没有办法反驳,呵呵,就暂时回避吧。
特别感谢:温柔一刀、Another_eYes、吴剑明、lczhuohuo。
温柔一刀的提议其实 eYes 已经提过:(来自:Another_eYes 时间:00-9-14 18:53:07 ID:336394 )
我在下面也解释了这样做的原因,其中固然有设计思路的问题,更重要的是希望通过讨论
加强这方面的知识。
如果有对这方面感兴趣的大虾,欢迎前来讨论,这个问题暂时留在这里,三天后如果没
有人继续,我再把它结束。
From: BaKuBaKu
 
大概是我看得不仔细吧,因此分数就算了吧,不要搞平均主义。
反正参与讨论就可以了。

不过可以告诉你,不要相信sql server的什么功能,最好还是
修改设计思路。
 
多人接受答案了。
 
后退
顶部