空间站

北极心空

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

package cn.com.pansky.xmdswz.application.scheduler;    
   
import org.apache.commons.logging.Log;    
import org.apache.commons.logging.LogFactory;    
import org.quartz.SchedulerException;    
import org.quartz.impl.StdScheduler;    
import org.springframework.beans.factory.BeanFactory;    
import org.springframework.context.support.ClassPathXmlApplicationContext;    
import cn.com.pansky.xmdswz.system.cache.CachedTableMgr;    
import cn.com.pansky.xmdswz.system.config.SystemConfig;    
import cn.com.pansky.xmdswz.utility.DateUtil;    
import org.quartz.JobDetail;    
import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean;    
import java.net.ServerSocket;    
import java.io.*;    
   
/**   
 * Title: XXXXXXX  
 * Description: XXXXXXXXXXXX  
 * Copyright: Copyright (c) 2006  
 * Company: www.pansky.com.cn  
 *   
 * 
@author Sheng Youfu   
 * 
@version 1.0   
 
*/   
public class Scheduler {    
  
private static Log log = LogFactory.getLog(Scheduler.class);    
   
  
private static ServerSocket srvSocket = null//服务线程,用以控制服务器只启动一个实例    
   
  
private static final int srvPort = 12345;     //控制启动唯一实例的端口号,这个端口如果保存在配置文件中会更灵活    
   
  
/**   
   * 定时任务配置文件   
   
*/   
  
private static String CONFIG_FILE = "cn/com/pansky/xmdswz/application/scheduler/Scheduling-bean.xml";    
   
   
  
public Scheduler() {    
    
//检测系统是否只启动一个实例    
    checkSingleInstance();    
   
    
//下面读取Spring的配置文件    
    SystemConfig cfg = new SystemConfig();    
    String config 
= cfg.parseParam("SCHEDULER.CONFIG_FILE"false);    
    
if(config!=null && !"".equals( config.trim()))    
      CONFIG_FILE 
= config;    
    log.debug(
"CONFIG_FILE: "+CONFIG_FILE);    
  }    
   
  
/**   
   * 主函数   
   * 
@param args String[]   
   * 
@throws Exception   
   
*/   
  
public static void main(String[] args) throws Exception{    
    Scheduler sch 
= new Scheduler();    
    sch.execute();    
  }    
   
  
/**   
   * 运行定时任务   
   
*/   
  
public void execute() {    
    ClassPathXmlApplicationContext appContext 
= new ClassPathXmlApplicationContext(new String[] {CONFIG_FILE});    
    BeanFactory factory 
= (BeanFactory) appContext;    
   
    
/**   
     * 装载任务调度   
     
*/   
    StdScheduler scheduler 
= (StdScheduler) factory.getBean("schedulerFactoryBean");    
    
//先暂停所有任务,等待装载缓存代码表    
    try {    
      scheduler.pauseAll();    
    } 
catch (SchedulerException ex) {    
      log.error(
"",ex);    
    }    
   
    
/**   
     * 装载缓存代码表   
     
*/   
    CachedTableMgr cachedtableMgr 
= (CachedTableMgr) factory.getBean("cachedTableMgr");    
    
try {    
      cachedtableMgr.loadCodeTable();    
    } 
catch (Exception ex) {    
      log.fatal(
"Load cached table failed. System will exit.", ex);    
      System.exit(
0);    
    }    
   
    
//重新恢复所有任务    
    try {    
      scheduler.resumeAll();    
    } 
catch (SchedulerException ex) {    
      log.error(
"",ex);    
    }    
  }    
   
  
/**   
   * 检测系统是否只启动了一个实例   
   
*/   
  
protected void checkSingleInstance() {    
    
try {    
      srvSocket 
= new ServerSocket(srvPort); //启动一个ServerSocket,用以控制只启动一个实例    
    } catch (IOException ex) {    
      
if(ex.getMessage().indexOf("Address already in use: JVM_Bind")>=0)    
        System.out.println(
"在一台主机上同时只能启动一个进程(Only one instance allowed)。");    
      log.fatal(
"", ex);    
      System.exit(
0);    
    }    
  }    
}    


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

性别:
文章: 178
积分: 430
圈子: 驾驭无形的力量—软件艺法思考

       时间: 1 星期前    评级:   11111 (2位会员评分)        

确实是很不错的一个方法, 绝大部分服务器系统上, 端口相对来说还是比较cheap的.

这个问题经典的跨平台解决方法是建一个命名的系统互斥量, 它的生命周期也是跟着进程的. 不过Java平台不倾向于提供直接操作宿主系统资源的途径, 自己也是以虚拟机为全部逻辑环境, 不提供宿主系统范围的Inter-JVM-Communication机制. 用端口绑定方式来实现互斥确实有点另类, 不过对于纯Java应用来说不失为最佳的解决方案.

另可以有一点改进的地方, 就是绑定到 InetAddress.getLocalHost() 这个本机地址(在大多数OS上相当于127.0.0.1), 这样进一步不会占用服务器外部ip上的端口. 不过一般的服务器上服务进程通常也不指定具体外部地址, 而是绑定到所有本机地址的端口, 即便这样改过以后还是会影响他们的启动. 所以这个改进除非在很大型的系统上, 服务应用各自指定具体的外部地址去绑定时才有作用.

Sunteya
等级: 初级会员
Sunteya的博客:Sunteya

文章: 2
积分: 34

       时间: 1 星期前    评级:   11111 (3位会员评分)        

其实 监听同一个端口还是很常用的方法,比如 Azureus(开源Java BT 客户端) 就是这样。

另一个常用的方法是像 Eclipse 的 workspace 一样。用 RandomAccessFile 把 File 的写 锁定掉

posted on 2006-12-11 11:39 芦苇 阅读(1104) 评论(0)  编辑  收藏 所属分类: JAVA

只有注册用户登录后才能发表评论。


网站导航: