我們到底能走多遠系列(24)
先不扯淡,先推薦:
如果你熱愛英文技術原文的話,這個推薦的網站絕對讓你會想抱一抱他:http://www.salttiger.com/ (也許你早就知道啦) 再一次感謝那些樂於分享和貢獻的勇士們,雖然互聯網上我們互不相識,卻通過知識,我們建立了某種超越空間時光的特殊關系,想想,這真的很有趣。
扯淡:
最近朋友在老家工作量一年,又跑來城市奮斗。可是納悶的是我數來數去,當時留在城市的人數正在逐年的下降,可這貨怎么還會來呢?
最近,想比較深入的學習事務,可是看了好多文章,卻越看越糊塗,有想起去看別的東西,有點三心兩意的感覺了。真心希望有人帶一下,輕松一點,唉。神,賜我一個大牛吧!
現在的公司,雖然是國內的,工作管理上較為開放,很多事可以自己決定,有時候自己會准備好幾個方案,和同事討論一下,選個比較優的,再去寫代碼,到也不錯。
主題:
初入web的時候,我們總是會被教育,web應用無需關心線程的問題,學好基本的框架,就可以上手啦。
其實實際項目中使用多線程的情況是很正常的,在業務復雜的應用程序中,比如如果一個業務非常耗時,我們只好采用異步的方式,避免影響web端的展示,再有定時監控數據庫字段的變化的業務,或者是batch處理(半夜處理數據庫.....)也就是定時任務啦等等。
我就把最近遇到的問題展現給各位,希望大家能給點好的意見,我都會嘗試使用,並應用到項目中去。
1,發郵件問題
項目中,注冊完畢后,需要向用戶的郵箱發送郵件,開始的代碼就是把發郵件的邏輯封裝在service層,然后action層調用完畢后,返回頁面,展現頁面。實際測試還沒有發現頁面跳轉太慢的情況,但是為了安全起見,還有一個原則就是我們不要把一些會拋諸runtimeException的邏輯放在和展現頁面的邏輯一起,異步是唯一的選擇。
多線程的實現是利用spring的框架:
線程池bean配置:
<bean id="mailTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"> <property name="corePoolSize" value="3"/> <property name="maxPoolSize" value="10"/> </bean>
直接把mailTaskExecutor注入進service層,就可以使用啦!
一下是發送郵件的簡單代碼:
public boolean sendMail4D(String email,String basePath, String cstmName,String userName,String passWord) { final MailSenderInfo mailInfo = getMailInfo(email); final Map model = new HashMap(); model.put("basePath", basePath); model.put("cstmName", cstmName); model.put("username", userName); model.put("password", passWord); model.put("dealerLogin", dealerLogin); model.put("customerTel", customerTel); model.put("cusEmailAddress", cusEmailAddress); mailTaskExecutor.execute(new Runnable(){ public void run(){ String result; try { result = VelocityEngineUtils.mergeTemplateIntoString(velocityEngine, "dealer-reg-send-mail.vm", model); mailInfo.setContent(result); SimpleMailSender sms = new SimpleMailSender(); sms.sendHtmlMail(mailInfo);// 發送文體格式 } catch (Exception e) { log.error("send mail failed,there has a Exception"); log.error(e.toString()); e.printStackTrace(); } } }); return true; }
這樣以來,在action層調用這個方法后就不用等待,郵件發送完畢后的返回了,唯一的壞處是,我們不知道郵件是否發送成功,這可能需要更多其他的代碼來彌補了。
2,數據庫字段監控問題
問題描述:
比如我們進行一個發布評論的操作,發布這個過程是通過webservice來調用另一個系統來實現的,在調用前我們把這個數據的字段置為發布中,對方回調成功后就置為發布成功,失敗就是發布失敗,但是有一種情況那就是對方出現異常,回調沒來調,這樣會導致這條數據一直為發布中... 這樣的數據就需要我這邊來判斷,然后把它置為發布失敗!
開始的想法:
每次調用對方的發布接口時,都啟用一個線程,這個線程每隔一分鍾檢查數據庫中這條數據的標志位,檢查三次,如果始終是發布中,就把它置為發布失敗。
ok,開始去實現了,想到前面提到的spring框架提供的線程池,也不是很難了吧。
一下是線程的代碼示例:(spring的線程配置與前面差不多)
import org.apache.log4j.Logger; import com.syezon.webapp.constant.BusinessConstants; import com.syezon.webapp.dao.ReleaseDao; import com.syezon.webapp.model.Release; public class CheckAdvStatusThread extends Thread{ public static Logger log = Logger.getLogger(CheckAdvStatusThread.class); private ReleaseDao releaseDao; private Long releaseId; public CheckAdvStatusThread(Long releaseId, ReleaseDao releaseDao){ this.releaseId = releaseId; this.releaseDao = releaseDao; } public void setReleaseDao(ReleaseDao releaseDao) { this.releaseDao = releaseDao; } public void run() { int i = 0; for ( ; i < 3; i++) { try { // 半分鍾 Thread.sleep(30000); Release release = releaseDao.getById(releaseId); if(release.getStatus() == BusinessConstants.RELEASE_STATUS_PUBING){ continue; }else{ break; } } catch (InterruptedException e) { log.error("there has a InterruptedException"); e.printStackTrace(); } } // 一分半鍾 if(i == 3){ releaseDao.setStatus(releaseId, BusinessConstants.RELEASE_STATUS_PUB_FAIL); } } }
代碼也沒什么好解釋了,特別要注意的是:構造方法,dao層的bean是通過構造時進來的,為什么不利用spring注入呢?事實上,試過的朋友應該多知道,在線程類中是無法注入的,可能線程啟動的方式繞過了一個正常實例產生時需要的流程吧,解決方法有:
在用多線程的時候,里面要用到Spring注入服務層,或者是邏輯層的時候,一般是注入不進去的。具體原因應該是線程啟動時沒有用到Spring實例不池。所以注入的變量值都為null。
如果在run方法里面加載application.xml,來取得bean時,由於重復加載,一般情況下會耗盡內存,拋出內存溢出錯誤。所以這的確是一個很頭痛的問題。
有一個方法可以解決注不進去的問題。就是在聲明變量的時候,前面加static。這樣在初始化的時候它就會加載application.xml,得到bean。
關於這個問題的根本機制沒有作深入的研究,好在問題解決了。
從這個例子體會到林信良說過的,沒有一個技術是完美的,不要為了Spring而Spring。不要為了注入而注入。
我沒有使用以上方式是因為,我嘗試了一下,不可行啊,但是我網上尋找的答案太過一直,所以我認為我是個特例,如果你也遇到類似的問題,大可以先嘗試一下上面的方法。
以上方法的問題:並發量大的時候會導致大量線程的啟用和銷毀,在3分鍾的時間里,真的難以想象不斷創建線程會發生什么,想想也有點怕怕啊!
親,如果是你,你怕嗎?
下班前的指導:
上級給出的意見是這樣,采用一個隊列(說白了,不就是LinkList嘛),然后啟動一個線程,這個線程對着個隊列進行檢測。每次發布的時候向這個隊列里塞信息,線程根據隊列中的信息,判斷發布中的狀態是否維持到了超時的范圍,就把它置為發布失敗,隊列刪除這信息。
上面的想法,其實很不錯,如此解決了第一種方式帶來的大部分問題。
回去想了好久,我想問一下你們,你們有類似的經驗嗎,給點提示,例子什么的啊~~
經過和同事的溝通,對方建議采用定時器更加靠譜!
目前,使用的是定時器方式解決的:
spring也支持定時器嘛,配置如下:
<!-- 需要執行的任務 --> <bean id="checkAdvStatusJob" class="org.springframework.scheduling.quartz.JobDetailBean"> <property name="jobClass" value="com.syezon.webapp.util.CheckAdvStatusMonitor"/> <property name="jobDataAsMap"> <map> <entry key="releaseDao"> <ref bean="releaseDao"/> </entry> </map> </property> </bean> <!-- 對任務的參數的設置 --> <bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean"> <property name="jobDetail" ref="checkAdvStatusJob" /> <property name="startDelay" value="180000" /><!--啟動3分鍾后再開始--> <property name="repeatInterval" value="180000" /><!--每3分鍾跑一次--> </bean> <!-- 啟動任務 --> <bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <!--<property name="triggers" ref="cronTrigger" />--> <property name="triggers" ref="simpleTrigger" /> </bean>
注意checkAdvStatusJob的配置,正真的邏輯我們是寫在jobClass里的。注入到jobClass的dao層bean只能通過上面的方式實現,不能用普通spring的property 去實現哦!releaseDao是注入到jobClass里的!下面的配置就不解釋啦。
CheckAdvStatusMonitor類,簡單的示例:
import java.util.Date; import java.util.List; import org.apache.log4j.Logger; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.scheduling.quartz.QuartzJobBean; import com.syezon.webapp.constant.BusinessConstants; import com.syezon.webapp.dao.ReleaseDao; import com.syezon.webapp.model.Release; public class CheckAdvStatusMonitor extends QuartzJobBean{ public static Logger log = Logger.getLogger(CheckAdvStatusMonitor.class); private ReleaseDao releaseDao; @Override protected void executeInternal(JobExecutionContext arg0) throws JobExecutionException { log.info("CheckAdvStatusMonitor begin"); // 查詢全部發布中狀態的發布信息 List<Release> releases = releaseDao.getByStatus(BusinessConstants.RELEASE_STATUS_PUBING); if(releases == null || releases.size() <= 0){ log.info("CheckAdvStatusMonitor end - there has no publishing release"); return; } for (Release release : releases) { Date createDate = release.getReleaseTime(); Date currentDate = new Date(); long l = currentDate.getTime() - createDate.getTime(); // 超過三分鍾 if(l > 180000){ log.info("CheckAdvStatusMonitor change a status"); releaseDao.setStatus(release.getId(), BusinessConstants.RELEASE_STATUS_PUB_FAIL); log.info("CheckAdvStatusMonitor change a status, releaseId = release.getId()"); } } log.info("CheckAdvStatusMonitor end"); } public void setReleaseDao(ReleaseDao releaseDao) { this.releaseDao = releaseDao; } }
好了,事情描述完啦,親,你有沒有好的建議?
讓我們繼續前行
----------------------------------------------------------------------
努力不一定成功,但不努力肯定不會成功。
共勉。