[原创]作为一个程序员,有些知识你应该知道----关于并发和数据库封锁

         近来碰上件很郁闷的事,国内某CMM3级的开发公司答应给我们的业务系统进行改造,但从4月拖到现在,效率真是低,新的系统又需要旧系统的支持,花了百来万就做出这么个垃圾的JAVA程序,实在忍无可忍,自己操刀上阵,一个人花了一周把他们用j2ee做了半年的程序改造成了 asp .net 程序。
 
 第一步,解决一个他们郁闷了很久,但是连开发人员自己都弄不明白的问题。
 
         现存问题:系统老是会生成2个以上同样的案号。过去一年中发生了5次左右,但是开发人员认为自己写的代码是不可能发生这种事的。
 
 业务需求如下:
         取案号:格式为4位年份+2位月份+2位日期+4位顺序号(超过4位不补0)
 其中顺序号要求每天从1开始,一般一天不会发生1万件以上的案件,所以要求<999的案号前面补0,如果超过1万件,就直接返回顺序号。
 
 表结构
 表名 TradeSerial
 字段1 字符型日期
 字段2 当前顺序号
 
例如:
     TradeSerial
20050701    23
 

以下是他们写的JAVA代码

public int getTradeSerial() {
    DateTime dt 
= new DateTime();
    String nowDate 
= dt.getSystemDate();
    SQL 
= "select * from TradeSerial";
    ResultSet rs 
= null;
    
try {
      rs 
= query(SQL);
      
while (rs.next()) {
        TradeSerial 
= rs.getString("SerialNo");
        TradeDate 
= rs.getString("TradeDate");
      }

    }

    
catch (Exception e) {
      
return -1;
    }

    
finally {
      closeConnection();
    }


    
//校验表中日期是否和系统日期相同,相同编号加1否则编号从1计算
    if (TradeDate.equals(nowDate)) {
      TradeSerial 
= Integer.toString(Integer.parseInt(TradeSerial) + 1);
      SQL 
= "update TradeSerial set SerialNo=" + TradeSerial;
    }

    
else{
      TradeSerial
="1";
      SQL
="update TradeSerial set SerialNo=" + TradeSerial+" , TradeDate="+nowDate;
    }


    
try {
      execute(SQL);
    }

    
catch (Exception e) {
      
return -1;
    }

    
finally {
      closeConnection();
    }

    
//日期加编号生成真正业务编号
    TradeSerial="00"+nowDate+fillZero(TradeSerial,3);
    
return 0;
  }


  
public String fillZero(String input,int len){
    
int l=input.length();
    
if (l>=len)
      
return input;
    
while (l<len){
      l
++;
      input
="0"+input;
    }

    
return input;
  }



复查完这段JAVA代码,我查点吐血。居然连事务都没有用。项目组成员JAVA水平差到这个地步无语了。

问题分析:
 出错原因是没有认识到b/s程序某些情况下是一个多线程程序,当2个用户几乎在同时取案号时,发生了并发问题。

问题的解决办法:
 临界区封锁,注意,仅用事务并不一定能解决这个问题。
 
 针对这个问题给出以下几个解决的方案:
 
 解决方案1.在程序中用锁技术,使用lock 对临界区代码加锁,保证同一时间只有一个线程执行取案号的代码。
 
 //生成案件编号
  public string  GetAJBH()
  {
  ...  
   lock(this)
   {
    try
    {
     //取案号
    }
    .......
   
  但是这里有一个问题。今后的系统可能不仅仅是一个B/S结构的系统,有些功能可能要在PDA上完成。这时这种方法就不能保证案号不冲突了。
 
  方案2:在数据库里实现。
   显然,这个是最好的解决办法。数据库一但选定,不太会改变。
 
  但是这里又出现2个问题。
  1.MS Sqlserver数据库:对于Sqlserver,只要用事务就可以完成以上功能,如果没有记错的话,SQLSERVER一但开始事务就是使用表封锁了。
 
  2.Oracle数据库。很遗憾,Oracle中仅用事务仍然可能会出现脏读现象。需要人工加锁。
  其DML锁有如下三种封锁方式:
  (1)、共享封锁方式(SHARE)
  (2)、独占封锁方式(EXCLUSIVE)   (注:lock table 表名 in exclusive mode;)
  (3)、共享更新封锁(SHARE UPDATE)(注:只要select ... from 表  for update即可进行封锁)
  这里又有一个问题。那就是你选用那个锁,答案应该是独占封锁方式。
 
  因为共享更新封锁是一种行封锁。针对取案号假设写了如下存储过程:


create or replace procedure getbh(ajbh  out varcharis
int;
bh 
number default -99;--假设没有值
CURSOR C1 IS select SERIALNO into bh from TRADESERIAL  where TRADEDATE=to_char(sysdate,'yyyyMMdd'for update;
begin
     
FOR I IN C1 LOOP--当天有值,那么值加1
        update TradeSerial set SerialNo=SerialNo+1;
        bh:
=I.SERIALNO+1;
     
END LOOP;
     
if(bh <> -99then--当天有值,返回        
           fillzero(bh,4,ajbh);     
           ajbh:
=to_char(sysdate,'yyyyMMdd')||ajbh;
           
return;
     
else--新的一天了
         bh:=1;
         
update TradeSerial set SerialNo=1,TRADEDATE=to_char(sysdate,'yyyyMMdd');
     
end if;
     fillzero(bh,
4,ajbh); 
     ajbh:
=to_char(sysdate,'yyyyMMdd')||ajbh;
end getbh;

经单步调试试验证明依然无法避免重号,因为如果新的一天开始了,那么表中的日期与当前日期不同,所做的select语句值为空,行封锁就不存在。如果有2个用户同时取案号。那么会取到2个当天的1号案件。所以要用 独占封锁方式。

 

create or replace procedure getbh(ajbh  out varcharis
int;
bh 
number default -99;--假设没有值
CURSOR C1 IS select SERIALNO into bh from TRADESERIAL  where TRADEDATE=to_char(sysdate,'yyyyMMdd');
begin
lock 
table TRADESERIAL in exclusive mode;
     
FOR I IN C1 LOOP--当天有值,那么值加1
        update TradeSerial set SerialNo=SerialNo+1;
        bh:
=I.SERIALNO+1;
     
END LOOP;
     
if(bh <> -99then--当天有值,返回        
           fillzero(bh,4,ajbh);     
           ajbh:
=to_char(sysdate,'yyyyMMdd')||ajbh;
           
return;
     
else--新的一天了
         bh:=1;
         
update TradeSerial set SerialNo=1,TRADEDATE=to_char(sysdate,'yyyyMMdd');
     
end if;
     fillzero(bh,
4,ajbh); 
     ajbh:
=to_char(sysdate,'yyyyMMdd')||ajbh;
end getbh;
 


感想:上面这个问题只是举的一个小例子,要求看似简单,以为会点数据库操作就OK了,但是深究下去,问题是很复杂的。
        可是目前IT人员大多有一种浮燥的心理。技术层出不穷,什么都想学,但常常浮于表面。而且大学生多了,心态大多都有点问题。学了一周的JSP,asp就号称精通,连学纺织,化工,机械类的人都招来当程序员。
         以前说重学历,但是现在大学教育质量普遍下降,刚出来的毕业生啥也不懂。学历考不住了。
         以前说重经验,但是很多情况是,很多非科班出身的程序员基础差,而且一天到晚忙着挣钱做项目,技术上的东西一点没心思学。结果经验是丰富了。可是所谓的经验却是错误的经验,而且还在不断的积累错误的经验,项目搞糟了,跳槽走人。

posted on 2005-07-24 15:41  使名扬  阅读(1414)  评论(5编辑  收藏  举报