我們項目中有一個后臺任務處理程序,是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 等級: 
性別:  文章: 178 積分: 430 圈子: 駕馭無形的力量—軟件藝法思考

| ?????? 時間: 1 星期前 ?? 評級: ?     ?(2位會員評分) | ??? ??? |
| 確實是很不錯的一個方法, 絕大部分服務器系統上, 端口相對來說還是比較cheap的. 這個問題經典的跨平臺解決方法是建一個命名的系統互斥量, 它的生命周期也是跟著進程的. 不過Java平臺不傾向于提供直接操作宿主系統資源的途徑, 自己也是以虛擬機為全部邏輯環境, 不提供宿主系統范圍的Inter-JVM-Communication機制. 用端口綁定方式來實現互斥確實有點另類, 不過對于純Java應用來說不失為最佳的解決方案. 另可以有一點改進的地方, 就是綁定到 InetAddress.getLocalHost() 這個本機地址(在大多數OS上相當于127.0.0.1), 這樣進一步不會占用服務器外部ip上的端口. 不過一般的服務器上服務進程通常也不指定具體外部地址, 而是綁定到所有本機地址的端口, 即便這樣改過以后還是會影響他們的啟動. 所以這個改進除非在很大型的系統上, 服務應用各自指定具體的外部地址去綁定時才有作用. |
|
|
Sunteya 等級: 初級會員
文章: 2 積分: 34

| |