防止JAVA程序重复启动的一个另类解决办法

         我们项目中有一个后台任务处理程序,是java开发application,用以处理网站提交的一些批量数据文件,因为这些数据文件数据量一般都比较大,所以写了这个批量处理程序,用以异步处理这些批量数据文件。这个程序设计成插件式的,处理各种不同数据文件的功能单独作为一个插件,然后使用Spring来粘合各个组件,这样就可以很方便地对该程序进行扩展。
        今天客户提出一个要求:需要控制这个程序在同一主机上只能启动一个实例。
        为了实现客户要求,我首先想到就是在数据库中建一张表,程序启动时往该表中写入一个标志,等程序结束时再删除标志。但这种方式存在一个问题就是,如果程序是非正常停止或被杀进程,那么这个标志就不可能被清除,那下一次启动就会误判为重复启动;另外,如果用数据库来记录启动标志的话,还把该程序跟数据库紧密耦合起来,感觉很别扭。
        排除了第一种方案之后,我以想到了用文件来保存启动标志(好象一些大型的程序,诸如weblogic好象就是采用在文件中记录启动标志方式来控制重复启动的)。客流量然这种方式不需要与数据库耦合在一起,但也存在程序异常中止而无法清除启动标志的问题,所以这个方案也被枪毙了。
        我想到的第三种方案就是在JAVA中调用操作系统的查看系统进程的方式来取得系统进程,然后再检测系统进程有特殊的进程标志来判断是否重复启动。但这种方式一是看起来很别扭,再者就是Window和 *nix系统中查看系统进程的命令不一样,分成几种情况来处理,无端地增加了程序的复杂性,也不可取。
        能不能在内存中记录一个启动标志呢?理论上这应该是不可行的,因为跨JVM来相互操作内存数据是不可能。我在网上搜了一下,也没找到相关的例子。
        那能不能占用一点系统共享资源,来换取我们的目标呢?比较容易想到的系统资源并且不能重复使用的资源就是端口。我尝试采用如下方案:在程序中指定一个不常用的端口(比如:12345),在程序启动时,就指定的端口启动一个ServerSocket,这个Socket只是为了占用这个端口,不接受任何网络连接。如果试图启动第二个实例时,程序在该指定端口启动ServerSocket时就会抛异常,这时我们就可以认为系统已经启动过了,然后打印提示并直接退出程序即可。这种方式在理论上分析应该可以的,我开始动手修改程序。程序修改如下:

java 代码
  1. package cn.com.pansky.xmdswz.application.scheduler;   
  2.   
  3. import org.apache.commons.logging.Log;   
  4. import org.apache.commons.logging.LogFactory;   
  5. import org.quartz.SchedulerException;   
  6. import org.quartz.impl.StdScheduler;   
  7. import org.springframework.beans.factory.BeanFactory;   
  8. import org.springframework.context.support.ClassPathXmlApplicationContext;   
  9. import cn.com.pansky.xmdswz.system.cache.CachedTableMgr;   
  10. import cn.com.pansky.xmdswz.system.config.SystemConfig;   
  11. import cn.com.pansky.xmdswz.utility.DateUtil;   
  12. import org.quartz.JobDetail;   
  13. import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean;   
  14. import java.net.ServerSocket;   
  15. import java.io.*;   
  16.   
  17. /**  
  18.  * Title: XXXXXXX 
  19.  * Description: XXXXXXXXXXXX 
  20.  * Copyright: Copyright (c) 2006 
  21.  * Company: www.pansky.com.cn 
  22.  *  
  23.  * @author Sheng Youfu  
  24.  * @version 1.0  
  25.  */  
  26. public class Scheduler {   
  27.   private static Log log = LogFactory.getLog(Scheduler.class);   
  28.   
  29.   private static ServerSocket srvSocket = null//服务线程,用以控制服务器只启动一个实例   
  30.   
  31.   private static final int srvPort = 12345;     //控制启动唯一实例的端口号,这个端口如果保存在配置文件中会更灵活   
  32.   
  33.   /**  
  34.    * 定时任务配置文件  
  35.    */  
  36.   private static String CONFIG_FILE = "cn/com/pansky/xmdswz/application/scheduler/Scheduling-bean.xml";   
  37.   
  38.   
  39.   public Scheduler() {   
  40.     //检测系统是否只启动一个实例   
  41.     checkSingleInstance();   
  42.   
  43.     //下面读取Spring的配置文件   
  44.     SystemConfig cfg = new SystemConfig();   
  45.     String config = cfg.parseParam("SCHEDULER.CONFIG_FILE"false);   
  46.     if(config!=null && !"".equals( config.trim()))   
  47.       CONFIG_FILE = config;   
  48.     log.debug("CONFIG_FILE: "+CONFIG_FILE);   
  49.   }   
  50.   
  51.   /**  
  52.    * 主函数  
  53.    * @param args String[]  
  54.    * @throws Exception  
  55.    */  
  56.   public static void main(String[] args) throws Exception{   
  57.     Scheduler sch = new Scheduler();   
  58.     sch.execute();   
  59.   }   
  60.   
  61.   /**  
  62.    * 运行定时任务  
  63.    */  
  64.   public void execute() {   
  65.     ClassPathXmlApplicationContext appContext = new ClassPathXmlApplicationContext(new String[] {CONFIG_FILE});   
  66.     BeanFactory factory = (BeanFactory) appContext;   
  67.   
  68.     /**  
  69.      * 装载任务调度  
  70.      */  
  71.     StdScheduler scheduler = (StdScheduler) factory.getBean("schedulerFactoryBean");   
  72.     //先暂停所有任务,等待装载缓存代码表   
  73.     try {   
  74.       scheduler.pauseAll();   
  75.     } catch (SchedulerException ex) {   
  76.       log.error("",ex);   
  77.     }   
  78.   
  79.     /**  
  80.      * 装载缓存代码表  
  81.      */  
  82.     CachedTableMgr cachedtableMgr = (CachedTableMgr) factory.getBean("cachedTableMgr");   
  83.     try {   
  84.       cachedtableMgr.loadCodeTable();   
  85.     } catch (Exception ex) {   
  86.       log.fatal("Load cached table failed. System will exit.", ex);   
  87.       System.exit(0);   
  88.     }   
  89.   
  90.     //重新恢复所有任务   
  91.     try {   
  92.       scheduler.resumeAll();   
  93.     } catch (SchedulerException ex) {   
  94.       log.error("",ex);   
  95.     }   
  96.   }   
  97.   
  98.   /**  
  99.    * 检测系统是否只启动了一个实例  
  100.    */  
  101.   protected void checkSingleInstance() {   
  102.     try {   
  103.       srvSocket = new ServerSocket(srvPort); //启动一个ServerSocket,用以控制只启动一个实例   
  104.     } catch (IOException ex) {   
  105.       if(ex.getMessage().indexOf("Address already in use: JVM_Bind")>=0)   
  106.         System.out.println("在一台主机上同时只能启动一个进程(Only one instance allowed)。");   
  107.       log.fatal("", ex);   
  108.       System.exit(0);   
  109.     }   
  110.   }   
  111. }   


经过测试,程序能很好地满足我们的要求,问题解决。
         我之所以称这种方式另类,是因为这种方式以牺牲一个端口的代价来达到我们的设计要求,采用这种方式的人应该不多。但我认为,只要我们牺牲的代价与我们的目标比较起来是在可接受的范围内,这种方式就是可取的,这与我们花钱增加内存来让程序运行更快在本质应该是相同的。

你可能感兴趣的:(java,jvm,应用服务器,quartz,asp.net)