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

  • 主题发起人 主题发起人 BaKuBaKu
  • 开始时间 开始时间
B

BaKuBaKu

Unregistered / Unconfirmed
GUEST, unregistred user!
我用 Delphi 5 + SQL Server 7 遇到这样的问题。
情况是这样的:
从一个表中读出某商品的采购明细记录,返回到客户端,
比如:
采购日期 批次 数量 已售数量 是否售完
九月一号 1 100 个 0 No
九月二号 2 200 个 0 No
...
现在有一个客户一次买了 150 个,系统的规则是先购进的要先卖出,如果某一批次卖完了
就把“是否售完”置为 Yes。
我必须做一个循环,逐批比较和修改,这样结果应该是:
第一批已售数量为 100,是否售完为 Yes。
第二批已售数量为 50,是否售完为 No。
<Font Color=#FF0000><strong>
这个流程好像很简单,但是当很多客户机同时做这个操作时,问题就出来了。
</Font></strong>
在 A 时刻,A 工作站把采购记录读到了 A 客户端,然后在 B 时刻,B 工作站也把
采购记录读到了 B 客户端,然后 A 先回写,B 再回写,结果就可想而知了... ...
这个问题的关键在于,数据事先被读到了客户端,而读到客户端的数据在回写之前,
可能又被别的进程改写,但是由于某种原因,使得应用程序不得不这样做(比如修改的
操作逻辑很复杂,必须在客户端完成复杂的判断与计算)。
我曾经尝试用存储过程和光标完成该功能,但马上意识到,这样做是换汤不换药,
光标实际上充当了上一种方式中的客户端数据集的角色。
请问怎样防止这样的错误?
From: BaKuBaKu
 
加锁,不允许同时访问。
 
在query中:
myquery.close;
myquery.sql.clear;
myquery.sql.add('select * from cgmxb where hh='''+货号'''');
myquery.open;
myquery.edit;
这样就可以对表进行加锁了。
注意: QUERY的属性CACHEDUPDATE要设为FALSE。
 
也许我疏忽了介绍系统的模拟环境。
想象一下,如果在一个大的超市里,几十台 Pos 机同时收银,又假设每个 Pos 机正好
都在卖肥皂,如果像 Hwk2000 说的那样加锁,那么所有买肥皂的顾客都要串行等待,显然
是不现实的。
Ok ?
 
To Expert:
请问是怎样的加锁算法?
 
用另一个表(比如叫操作表)记录用户的操作, 里面只有总数和总消耗数. 在每天的特定
时刻再将这个表中的记录去冲库存表
(当然实时查询库存麻烦点, 需要包含进操作表中的数据).
 
eYes 的方法可行,我原来这样做成功过,但是好像不太方便,而且影响其他模块的设计
流程,这样做的目的其实就是即时算出商品的成本价格,然后填写到销售明细表中。
(因为成本是保存在采购明细表中的,而且每次采购的成本价格都可能不一样)。
像 eYes 这样做的话,有一个缺点,就是不能随时查询到最新的利润(销售金额-成本
金额),必须在做完一个特定的计算成本的操作之后才能查到,就像 eYes 说的在特定的
时刻做的一个操作。
还有就是,我想了解怎样利用关系数据库的本身的并发特性实现这样的功能,比如加锁
或者是信号量变量等。
谢谢。

From: BaKuBaKu
 
>>不能随时查询到最新的利润
怎么不能? 只是公式变一 下而已(库存销售金额+临时表销售金额-成本金额)
 
mmm, 如果一定要用那个库, 用sp也许能做到: 这个sp传递进去的参数是您想修改的
总数(比如100), sp所做的就是修改数据库并返回超过库存部分. 您的Client用一个循环
调用这个SP直到返回的超过部分为0.
 
使用三层结构,将应用逻辑放到Application Server上去。
 
以前问过的:
更新数据库不是把新数据直接填回数据库,而是发回一个要更新的数据,
如A用户买了三个商品,原来有四个,你不要填回1,而是发回个3,让数据库去减,
4-3=1。即 a=a-X.就不会多扣。
例如: 在用户发回3时,B用户先发了2,4-2=2,那么数据库再处理A用户时,就是
2-3=-1,这时就可以报错了。
这种做法就不会搞错。缺点是:可能会产生白做的交易。但应付一般业务足以。
 
虽然大家都很热情,但是我发现各位还是没有解答到点子上:
请允许我再次说明一下问题吧:

一个商场,有这样一张采购明细表:
ID 商品 采购日期 批次 数量 采购价格 已售数量 是否售完
1 杯子 九月一号 1 100 个 2.00 元 0 No
2 杯子 九月二号 2 200 个 3.00 元 0 No
...
如果商场一次卖出了150个杯子,则会有如下的销售明细表:(假设杯子统一卖 4 元一个)
商品 销售日期 数量 销售金额 成本金额 (销售金额-成本金额=利润)
杯子 九月三号 150 个 600 元 350 元
...
根据先进先出的原则,实际上售出的是第一批的100个和第二批的50个。
350 元的成本金额是根据批次和采购价格算出来的,即:
第一批 100 个 * 2.00 + 第二批 50 个 * 3.00 = 350 元

如果我要记录这笔销售业务,现在要做的是:
1、计算成本金额。目的在于方便地计算利润。(销售金额-成本金额=利润)
2、更新采购明细表的已售数量,以便下次能正确计算成本,如果已售数量等于采购数量,
则把这一批杯子的“是否售完”填为 Yes。
3、填写销售明细。如果上面两步正确执行的话,这一步非常简单,不用多说。

我现在的做法是:
1、把该商品的采购记录查询到客户端(不包括已售完的记录),以上面的采购明细表
为例,将有两条记录。
2、然后比较销售数量和剩余数量,第一批只有100个,而客户一次买了150个,那么首先
把这一批全部卖出去,“已售数量”=100,“是否售完”=Yes,得到200元成本金额。接着
把第二批的“已售数量”=50,“是否售完”=No,得到 50*3=150 元成本金额。
以上是用循环实现,且结果数据(“是否售完”和“已售数量”)暂时在客户端用
数组缓存起来,没有回写数据库。
3、这时已得到填写销售明细表的所有数据,于是启动一个事务:
DB.StartTransaction;
try
Insert 销售明细; // So easy
// 下面根据结果缓存数组,循环回写采购明细记录
Update 采购明细 Set 已售数量=DataArray.YSSL,
是否售完=DataArray.SFSW
Where ID = DataArray.ID // 用 ID 型字段绝对定位采购记录
DB.Commit;
except
DB.RollBack;
end;
<Font Color=#FF0000>请注意,Update 采购明细时是用绝对数值回填,假如上面在做完第二步的瞬间,另外的
客户机又修改了采购明细表的数据,那么第二步结束时得到的缓存数据就是无效的,执行
第三步时就会写入错误的数据。
</strong>必须保证上面的三步操作一气呵成,而不仅仅是在第三步启动一个事务那么简单。
</font></strong>

OK ?

From: BaKuBaKu
 
>>Update 采购明细时是用绝对数值回填
为什么要这么做? 自己给自己设置障碍?
 
我总觉得这种并发操作是不可避免的,处理他是否有异议?
 
我认为完全可以避免冲突, 最简单就是用sp.
比如你的程序先记录下肥皂有哪写记录(库存不必关心). 用户买了150块肥皂, 你向
sp发送参数 ID='肥皂1', 数量=150.
sp里面在修改前先判断是否数量超过库存, 如果是, 则置标记, 然后把修改消耗量, 最
后返回未处理的数字, 比如库存只有100, 用户输入了150, 则返回50.
客户端只要判断当前返回值是否大于0, 如果是, 则取下一条记录的ID. 重复上述步骤
直到返回值<=0为止.
我sp不熟, 语法错误请包涵, 不过大致操作如下:
@n = select @UsedNum + 消耗数量 - 库存 from 表 where id = @id
if @n >= 0 then
update 表 set 消耗数量=库存, 标记=1 where id = @id
else
update 表 set 消耗数量=消耗数量+@UsedNum where ....

这个n就是返回值.
 
改一下数据结构和做法,很简单,
不改的话,个人认为没什么希望。
 
>>Update 采购明细时是用绝对数值回填
为什么要这么做? 自己给自己设置障碍?
 
BaKuBaKu,怎么样啊?

如果你的数据结构/程序做法一点也不能动,我也就不说我的建议了,
否则请回应。
 
1、用存储过程,将这些动作成为一个操作,如果失败在存储过程里面一起ROLLBACK
2、如果一定要在程序里执行,可以将这两个表锁住, 即在最这些动作前用
UPDATE 表 set ID = ID where ID = 1
最后一起COMMIT或ROLLBACK
 
各位对不起,因为出差几天,没有光顾论坛,下面继续提出我的看法:
eYes 和吴剑明不约而同地说:
>>Update 采购明细时是用绝对数值回填
为什么要这么做? 自己给自己设置障碍?
请 eYes 看看自己提出的方案吧:(时间:00-9-16 22:18:54 ID:338942 )
@n = select @UsedNum + 消耗数量 - 库存 from 表 where id = @id
if @n >= 0 then
update 表 <font color = #ff0000>set 消耗数量=库存</font>, 标记=1 where id = @id // !!!
else
update 表 <font color = #ff0000>set 消耗数量=消耗数量+@UsedNum </font> where .... // !!!
上面的 Update 语法难道不是用绝对数值回填吗?而且这个流程存在严重的并发问题:
在第一次 Select 时得到了 @n,然后后面通过判断 @n 的值,执行相应的操作,好像
是正确的,但是不要忘了,这是并发的环境。如果在第一时刻执行了 Select 得到 @n 的
值,这时另外的工作站在第二时刻的一瞬间又修改了表里的数据,这时的 @n 岂不是一个
无效的数值吗?
其实我上面说的也是这个意思,只是不好表达清楚而已。
Do you think so ?

From: BaKuBaKu
 
后退
顶部