当你想在web应用中使用线程的时候-我们到底能走多远系列(24)
我们到底能走多远系列(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; } }
好了,事情描述完啦,亲,你有没有好的建议?
让我们继续前行
----------------------------------------------------------------------
努力不一定成功,但不努力肯定不会成功。
共勉。