程序给你写复杂了,增加了逻辑的复杂度,增加了潜在bug的可能性,容易产生并发造成的脏数据;而且会因为多了很多锁影响其他会话的执行;有百害而无一利;
当然我只是抛砖引玉,其实很多技巧都可以避免游标的时候,或减少游标嵌套的层数;
c. 游标有时“高不成低不就”,我在pl/sql用的稍微复杂些的算法,比如回
溯,递归等,鉴于游标只能前后滚动,而且没有想数组那样的按照行
号来回跳转,所以,必须把这些记录值存在数组后才能做灵活的处理; d. 游标效率低,这个表面上到无所谓,慢就慢点,但是如果高并发,就
增加了死锁的几率;
2.3写update语句的注意点; 2.3.1 “进攻式”编程:
我们在写update 语句的时候,有时候未必需要“真写”,
常见的例子是:单据审核,比如把单据状态从0变成1;(0-初始 1-审核 2-完成)
很多人的写法是:
Begin
Select 单据状态 into v_单据状态 from 单据表 where 单号=’123’; If v_单据状态=’1’ Then
dbms_output.put_line (‘单号123已经审核’); return; End if;
Update单据表 set 单据状态=’1’ where单号=’123’; dbms_output.put_line (‘单号123审核成功’);
…………… End;
这样的写法看上去很符合人的逻辑:先判断是否已经审核了,如果已经审核了,就不要做了,如果还没有什么,那么就打上标志;
可是漏洞很大:还是我上面说的“刻舟求剑”;
因为这段代码主要有两句话,先查询状态,再更改状态;
但是这两句话有时间差,第二句话执行的时候,第一句话的查询结果可能发生;也就是说在高并发的情况下,该程序可能得到错误的结果;
怎么去保护呢,很自然,有人想到了:
Select 单据状态 into v_单据状态 from 单据表 where 单号=’123’; 这句话后面加上 for update ;的确,加上这个确实让代码“安全”了;
但是这样,如果高并发,虽然运行不会出逻辑问题,但是如果100人同时执行,执行一次需要0.1秒,那么最终全部完全的时间是10秒;因为这里是串行的,每个人执行到for update 这里就会排队依次执行;
所以最好这样写: begin
Update单据表 set 单据状态=’1’ where单号=’123’ and 单据状态=’0’; If sql%rowcount=0
then
dbms_output.put_line (‘单号123审核成功’); else
dbms_output.put_line (‘单号123已经审核’); return; end if; …………… End;
这种写法“一石二鸟”,一条update语句既可以作为更新,又含有了检查的功能; 通俗的说:要更新什么,就在where里面写上它更新前应该的状态;
这样如果有100个人同时执行的时候,只有第一个人顺利执行,其他人等第一个花0.1秒提交后,全部是sql%rowcount=0不会引起锁,所以就不会有串行执行的互相等待;
这种以Update 兼容select 的做法叫做“进攻式”编程,这个术语,好像是有一本叫做“SQL编程艺术”里提出的;原理就是避免或减少“防御性”的判断,直接做数据的处理;好处是提供并发性,简化程序,防止脏数据;
顺便说一句:
有的人防止并发异常,自作聪明的“发明”了一种写法;
你不是怕并发异常吗?所以他在过程的第一句就来上:
Update 单据 set memo=memo where 单号=’123’; –-随便找个字段“自己”更新“自己” 这样的目的就是为了“锁住”,多个人同时执行的时候不会有数据异常;
我以前遇到一个合作伙伴的项目,每个过程前的第一句话,都来上这种“什么也不做,就是要锁你”这样的怪语句,如果真的要锁,你还不如写成;
Select … into .. from 单据 where 单号=’123’ for update
至少这样的语句还容易理解,而你来个 update… set 自己=自己 where..;这让人看了很“费解”(也许他不知道for update );
于是我就问他为什么这样做,他还理直气壮的跟我说,这是他们团队的一个“高手”想出的“妙招”,用来防止多个人同时做一件事情发生的数据异常;我不怀疑这个高手的智商,可是这些高手难道就不能多看点书,遇到问题就会出“怪招”,还自鸣得意;
而且每执行一个update语句,都会产生日志的,你就是 update 自己=自己,看上去啥也没干,那还是要生成修改前和修该后的日志,这些无谓的日志去消耗干嘛呢;
如果有兴趣查看日志的消耗,我写了一段脚本,有兴趣的试试:
DECLARE
V_VALUE1 PLS_INTEGER; V_VALUE2 PLS_INTEGER; begin
-- 记录执行前的redo size ; SELECT A.VALUE INTO V_VALUE1
FROM V$MYSTAT A, V$STATNAME B WHERE A.STATISTIC# = B.STATISTIC# AND NAME = 'redo size';
--------------------------------
--写上你的update 语句,哪怕update 自己=自己 ,也会生成日志; COMMIT;--及时把redo buffer 的数据写会redolog文件;
-------------------
-- 记录执行后的redo size ; SELECT A.VALUE
INTO V_VALUE2
FROM V$MYSTAT A, V$STATNAME B WHERE A.STATISTIC# = B.STATISTIC# AND NAME = 'redo size';
dbms_output.put_line('执行前redo size:' || to_char(V_VALUE1));
dbms_output.put_line('执行后redo size:' || to_char(V_VALUE2));
dbms_output.put_line('增加的redo size:' || to_char(V_VALUE2 - V_VALUE1)); end;
2.3.2 update带返回值:
这个“功能”非常实用,如果不知道这个功能,pl/sql算白学了, 不能说t-sql没有这样的功能,你就不知道用;
就像老外到中国不去长城一样;
该功能的表达方式如:
Update table1 set field2=xxx where field1=xxx Return fiend1,field2,field3 into v1,v2,v3;
或者批量(多条记录):
Update table1 set field2=xxx where field1=xxx
Return fiend1,field2,field3 BULK COLLECT into v1,v2,v3;--变量是数组类型;
这样可以省略很多select 代码;这个倒无所谓,有的时候,update之后,再想selelct 不是那么方面的;
比如:Update table1 set field2=(selelct … from …) where field1=xxx Return field2 into v2; 这个就非常方便;
这个功能如果仅仅是为了减少部分select,那到不算什么大特色;相对t-sql,也只不过方面些而已;
但有的时候,对于高并发的处理,它有很多妙用;
比如:仓库入库,
在我们在入库前,就要做货位分配处理,假如有100托货,同时入库,入库的原则很简答,就是找最小货位;你怎么保证着100托的货同时瞬间分配而不会互相干扰;
这个很简答的问题,一般人都这样写:
Begin
Select min(货位) invo v_货位 from 货位表 where 货位状态=’空闲’; Update 货位表 set 货位状态=’已分配’ where 货位=v_货位; dbms_output.put_line('分配货位成功,是:’|| v_货位); End ;
但是这样写,只能一个一个的按顺序分配,如果100个人同时执行这样的分配工作,那么这些货位都“打架”了;
所以,这个问题,我的最初的写法是: Begin
For I in 1..10
Loop
Select min(货位) invo v_货位 from 货位表 where 货位状态=’空闲’; Update 货位表 set 货位状态=’已分配’ where 货位=v_货位; If sql%rowcunt=1 then
Begin
dbms_output.put_line('分配货位成功,是:’|| v_货位); exit; end if; end loop; End ;
就是给它10次机会,如果并发太高,每次找到最小货位后,就被别人“捷足先登”,那只能多试几次了;
总有一次能成功把;当然,如果胆子大一点,可能给“无限”的尝试次数,不管这样,这种写法很怪;
有什么好的解决方法吗,这里就用到了update返回值的方法; Begin
Update 货位表 set 货位状态=’已分配’ where 货位=(Select min(货位) from 货位表 where 货位状态=’空闲’) where 货位状态=’空闲’) returning 货位 into v_货位; dbms_output.put_line('分配货位成功,是:’|| v_货位); End ;
这样即使100个人同时分配,也不会分到一起的,说老实话,如果不是用这个update …return into .. 的技巧,我还真没想到有什么特别好的方法来处理这个问题;
有的人会怀疑,该写法是不是“安全”的,我经过很多测试,确保是安全的,
大家仔细看这句话:
Update 货位表 set 货位状态=’已分配’ where 货位=(Select min(货位) from 货位表 where 货位状态=’空闲’)
AND 货位状态='空闲'; returning 货位 into v_货位;
这句话里,虽然只是操作一个表,但是其实分两部分,
第1部分:Select min(货位) from 货位表 where 货位状态=’空闲’
第2部分: Update 货位表 set 货位状态=’已分配’where 货位=(第1部分) AND 货位状态='空闲' 也许有人有会疑惑,会不会在高并发的的时候出现这种情况:
第1部分先得到了一个最小货位 001,执行到第2部分的时候,001 的状态已经不是“空闲”,因为第1部分没有锁;
当第2部分执行的时候,可能得到的sql%rowcount=0 ;
我个人认为这种情况不可能,oracle的运行机制可能是这样的: 假如两个人(不同的session)同时执行这句嵌套的update ;
那么同时得到的最小货位肯定一样,其中一个先更新,这时候在提交前会锁住该货位;
另一个发现该货位的记录锁住了,就整个放弃,等你完成,我在重新执行这个SQL,于是它会不停地重新执行这条语句,直到前一个人提交了,这时候它检索的min(货位)也不可能是001货位了,而可能是002货位;
我个人的理解是:对于一条SQL,无论该SQL多么复杂,在执行的过程中,它内部可能要分好几步去执行,但是只要有一步被锁住,那么它前面做好的几步会全部作废,全部重头再执行,如果执行到某一步,发现又有锁了,那再重新全部回头再做,以此循环,直到成功。
这就类似于做一件高级的陶瓷艺术品,有很多工序,中间某个烧火的工序时间控制错了,那么前面的工序全部作废,重头再来;
如果这种原理,就非常安全;