作者杨中科是CowNew开源团队JDBMonitor项目组的开发人员。
CowNew开源团队网站 http://www.cownew.com
论坛 http://www.cownew.com/newpeng/
转载请注明此版权信息
数据库监控、日志工具JDBMonitor在介绍文档中说到“JDBMonitor另起一个线程来记录SQL,所以它不会对程序运行速度有任何影响。”,那么它是如何做到的呢?
JDBMonitor采用“生产者-消费者模型”完成此功能。学过操作系统原理的朋友对“生产者-消费者模型”一定不陌生,它是和“哲学家进餐问题”等相提并论的经典模型。
JDBMonitor的主线程做为“生产者”,产生“日志信息”这个产品,另起一个线程做为消费者,这个消费者负责“消费”日志信息,并把日志信息派发给所有的监听者,这样日志的产生和消费是独立的,所以无论日志的派发有多慢,都没有关系,都不会影响到主线程的运行速度。下面让我们深入研究一下:
(注:JDBMonitor的二进制jar包和源代码都可以从 http://www.cownew.com 下载得到。)
打开com.cownew.JDBMonitor.common.DBLogger。JDBMonitor每次截获到一条sql语句,都会调用logSQL方法,并把sql语句的信息对象传过来,这样我们就可以把logSQL方法当成生产者,logsql把消息放到channel中。LogConsumer是一个实现了Runnable接口的类,它就是日志的消费者,并且是运行于另一个线程的,它负责实时检测channel,只要channel中有东西,他就从channel取出日志对象,然后发送给各个监听器。
startConsumer中
for (;;)
{
SQLInfo info = (SQLInfo) channel.take();
for (int i = 0, n = dbListeners.length; i < n; i++)
{
dbListeners[i].logSql(info);
}
}
就是在实时检测channel中的产品。
channel是一个阻塞通道BlockedChannel的实例,这个阻塞通道主要提供两个方法offer,take,offer方法向通道中放入产品,take从通道中取产品,当通道中没有产品的时候,take方法将会阻塞,直到有产品为止。BlockedChannel是靠java的wait-notify机制实现的,原理非常简单,有兴趣的朋友看一下其实现代码就可以。
值得注意的是,LogConsumer是一个后台线程,因为如果LogConsumer不是后台线程,那么由于LogConsumer一直在无限循环里执行,所以程序就不能正常终止。而设置为后台线程后,在其他工作线程停止后,此线程就会自动终止。关于“后台线程”的知识,可以查看相关资料。
但是采用后台线程以后,又有另一个问题出现了,在主线程停止后,有可能产品通道channel中还有没有发送完的日志信息,这样就会造成漏记日志信息。
Runtime 类有一个addShutDownHook方法,可以调用此方法向JVM注册一个监听器,这个监听器必须实现Thread接口,在JVM停止之前,这个监听器将会被运行,这样我们就可以在JVM停止之前做一些事情了。
JDBMonitor就是用addShutDownHook解决此问题的。见DBLogger的静态初始化块:
Runtime.getRuntime().addShutdownHook(new Thread(){
public void run()
{
//once JVM will shutDown,this hook will pause the shutingdown
//until all the SQLInfo be dispatched
while (!channel.isEmpty())
{
try
{
Thread.sleep(SHUTDOWNSLEEPMSECOND);
} catch (Exception e)
{
throw CommonUtils.toRuntimeException(e);
}
}
...
}
});
在JVM停止之前会首先检测产品通道中有没有产品,如果还有,则会一直等到产品被消费完后再终止。
while (!channel.isEmpty())
{
try
{
Thread.sleep(SHUTDOWNSLEEPMSECOND);
} catch (Exception e)
{
throw CommonUtils.toRuntimeException(e);
}
}