我們項(xiàng)目中有一個后臺任務(wù)處理程序,是java開發(fā)application,用以處理網(wǎng)站提交的一些批量數(shù)據(jù)文件,因?yàn)檫@些數(shù)據(jù)文件數(shù)據(jù)量一般都比較大,所以寫了這個批量處理程序,用以異步處理這些批量數(shù)據(jù)文件。這個程序設(shè)計成插件式的,處理各種不同數(shù)據(jù)文件的功能單獨(dú)作為一個插件,然后使用Spring來粘合各個組件,這樣就可以很方便地對該程序進(jìn)行擴(kuò)展。
??????? 今天客戶提出一個要求:需要控制這個程序在同一主機(jī)上只能啟動一個實(shí)例。
??????? 為了實(shí)現(xiàn)客戶要求,我首先想到就是在數(shù)據(jù)庫中建一張表,程序啟動時往該表中寫入一個標(biāo)志,等程序結(jié)束時再刪除標(biāo)志。但這種方式存在一個問題就是,如果程序是非正常停止或被殺進(jìn)程,那么這個標(biāo)志就不可能被清除,那下一次啟動就會誤判為重復(fù)啟動;另外,如果用數(shù)據(jù)庫來記錄啟動標(biāo)志的話,還把該程序跟數(shù)據(jù)庫緊密耦合起來,感覺很別扭。
????????排除了第一種方案之后,我以想到了用文件來保存啟動標(biāo)志(好象一些大型的程序,諸如weblogic好象就是采用在文件中記錄啟動標(biāo)志方式來控制重復(fù)啟動的)。客流量然這種方式不需要與數(shù)據(jù)庫耦合在一起,但也存在程序異常中止而無法清除啟動標(biāo)志的問題,所以這個方案也被槍斃了。
????????我想到的第三種方案就是在JAVA中調(diào)用操作系統(tǒng)的查看系統(tǒng)進(jìn)程的方式來取得系統(tǒng)進(jìn)程,然后再檢測系統(tǒng)進(jìn)程有特殊的進(jìn)程標(biāo)志來判斷是否重復(fù)啟動。但這種方式一是看起來很別扭,再者就是Window和?*nix系統(tǒng)中查看系統(tǒng)進(jìn)程的命令不一樣,分成幾種情況來處理,無端地增加了程序的復(fù)雜性,也不可取。
??????? 能不能在內(nèi)存中記錄一個啟動標(biāo)志呢?理論上這應(yīng)該是不可行的,因?yàn)榭鏙VM來相互操作內(nèi)存數(shù)據(jù)是不可能。我在網(wǎng)上搜了一下,也沒找到相關(guān)的例子。
??????? 那能不能占用一點(diǎn)系統(tǒng)共享資源,來換取我們的目標(biāo)呢?比較容易想到的系統(tǒng)資源并且不能重復(fù)使用的資源就是端口。我嘗試采用如下方案:在程序中指定一個不常用的端口(比如:12345),在程序啟動時,就指定的端口啟動一個ServerSocket,這個Socket只是為了占用這個端口,不接受任何網(wǎng)絡(luò)連接。如果試圖啟動第二個實(shí)例時,程序在該指定端口啟動ServerSocket時就會拋異常,這時我們就可以認(rèn)為系統(tǒng)已經(jīng)啟動過了,然后打印提示并直接退出程序即可。這種方式在理論上分析應(yīng)該可以的,我開始動手修改程序。程序修改如下:
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;?//服務(wù)線程,用以控制服務(wù)器只啟動一個實(shí)例????
???
??private?static?final?int?srvPort?=?12345;?????//控制啟動唯一實(shí)例的端口號,這個端口如果保存在配置文件中會更靈活????
???
??/**???
???*?定時任務(wù)配置文件???
???*/???
??private?static?String?CONFIG_FILE?=?"cn/com/pansky/xmdswz/application/scheduler/Scheduling-bean.xml";????
???
???
??public?Scheduler()?{????
????//檢測系統(tǒng)是否只啟動一個實(shí)例????
????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);????
??}????
???
??/**???
???*?主函數(shù)???
???*?@param?args?String[]???
???*?@throws?Exception???
???*/???
??public?static?void?main(String[]?args)?throws?Exception{????
????Scheduler?sch?=?new?Scheduler();????
????sch.execute();????
??}????
???
??/**???
???*?運(yùn)行定時任務(wù)???
???*/???
??public?void?execute()?{????
????ClassPathXmlApplicationContext?appContext?=?new?ClassPathXmlApplicationContext(new?String[]?{CONFIG_FILE});????
????BeanFactory?factory?=?(BeanFactory)?appContext;????
???
????/**???
?????*?裝載任務(wù)調(diào)度???
?????*/???
????StdScheduler?scheduler?=?(StdScheduler)?factory.getBean("schedulerFactoryBean");????
????//先暫停所有任務(wù),等待裝載緩存代碼表????
????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);????
????}????
???
????//重新恢復(fù)所有任務(wù)????
????try?{????
??????scheduler.resumeAll();????
????}?catch?(SchedulerException?ex)?{????
??????log.error("",ex);????
????}????
??}????
???
??/**???
???*?檢測系統(tǒng)是否只啟動了一個實(shí)例???
???*/???
??protected?void?checkSingleInstance()?{????
????try?{????
??????srvSocket?=?new?ServerSocket(srvPort);?//啟動一個ServerSocket,用以控制只啟動一個實(shí)例????
????}?catch?(IOException?ex)?{????
??????if(ex.getMessage().indexOf("Address?already?in?use:?JVM_Bind")>=0)????
????????System.out.println("在一臺主機(jī)上同時只能啟動一個進(jìn)程(Only?one?instance?allowed)。");????
??????log.fatal("",?ex);????
??????System.exit(0);????
????}????
??}????
}????
經(jīng)過測試,程序能很好地滿足我們的要求,問題解決。
???????? 我之所以稱這種方式另類,是因?yàn)檫@種方式以犧牲一個端口的代價來達(dá)到我們的設(shè)計要求,采用這種方式的人應(yīng)該不多。但我認(rèn)為,只要我們犧牲的代價與我們的目標(biāo)比較起來是在可接受的范圍內(nèi),這種方式就是可取的,這與我們花錢增加內(nèi)存來讓程序運(yùn)行更快在本質(zhì)應(yīng)該是相同的。
?
?
complystill 等級: 
性別:  文章: 178 積分: 430 圈子: 駕馭無形的力量—軟件藝法思考

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

| |