为了解决基于servlet并发的日志存储(上)中提到的,无法满足多个url同时请求的的问题,因此有了这个版本的优化。
这个版本重点解决了多个请求生成多个文件的情景。其整体思路为:使用一个ConcurrentHashMap来存储每一个url对应的content,其Key为String类型,存储每一个请求的path,Value为StringBuffer类型,对应存放content的StringBuffer,每个请求在Map中查找对应的path是否存在,如果存在则直接在对应的Value中追加内容,不存在则创建文件,并将Key添加到Map中。写入文件时使用定时任务,定时的将Map写入到文件中。
以下是具体实现的代码:
Servlet与Global类与基于servlet并发的日志存储(上) 中大体相同
servlet:
package savelog.youxinpai.com;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Timer;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Servlet implementation class SaveLog
*/
@WebServlet("/SaveLog")
public class SaveLog extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* @see HttpServlet#HttpServlet()
*/
public SaveLog() {
super();
// TODO Auto-generated constructor stub
}
public void init() {
System.out.println("init Timer.");
Global.timer = new Timer(true);
Global.timer.schedule(Data.timeTask(), 0,Global.waitTime);
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
request.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.println("DIR1:"+request.getParameter("dir1"));
writer.println("DIR2:"+request.getParameter("dir2"));
writer.println("DIR3:"+request.getParameter("dir3"));
writer.println("filename:"+request.getParameter("filename"));
writer.println("content:"+request.getParameter("content"));
writer.println(request.getCharacterEncoding());
if(request.getParameter("dir1")==null){
writer.println("dir1 muxt exist");
return;
}
//String path=request.getParameter("dir1")+"\\"+request.getParameter("dir2")+"\\"+request.getParameter("dir3");
StringBuilder path=new StringBuilder();
path.append(request.getParameter("dir1"));
if(request.getParameter("dir2")!=null&&!request.getParameter("dir2").equals(""))
path.append("\\"+request.getParameter("dir2"));
if(request.getParameter("dir3")!=null&&!request.getParameter("dir3").equals(""))
path.append("\\"+request.getParameter("dir3"));
String filename=request.getParameter("filename");
String content=new String(request.getParameter("content").getBytes("ISO-8859-1"),"UTF-8");
writer.println("Path:"+path);
int mapSize = Data.createPath(path.toString(), filename, content);
writer.println("MapSize:"+mapSize);
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
doGet(request, response);
}
}
Global:
package savelog.youxinpai.com;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
public class Global {
/*写日志缓冲区*/
public static StringBuffer stringBuffer=new StringBuffer();
/*filename标志*/
public static String filename=new String("");
/*根路径*/
public static final String rootPath = "e:\\logroot";
/*保存路径和内容*/
public static ConcurrentHashMap<String,StringBuffer> pathMap=new ConcurrentHashMap<String,StringBuffer>();
/*定义Timer类*/
public static Timer timer = null;
/*定时任务等待时间 单位毫秒*/
public static long waitTime = 200;
/*存放每隔path锁的map*/
public static ConcurrentHashMap<String,Lock> lockMap=new ConcurrentHashMap<String,Lock>();
/*分隔线*/
public static String separator ="---";
}
Data类实现了对程序的优化
package savelog.youxinpai.com;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
public class Data {
/**
* 程序入口方法
* @param path
* @param filename
* @param content
* @return
*/
public static synchronized int createPath(String path, String filename, String content){
String saveDirPath=Global.rootPath+"\\"+path+"\\"+getDate();
String filePath=saveDirPath+"\\"+filename+".log";
if(Global.pathMap.containsKey(filePath)){
try{
Global.lockMap.get(filePath).lock();
Global.pathMap.put(filePath,Global.pathMap.get(filePath).append(getTime()+Global.separator+content+"\r\n"));
}catch(Exception e){
e.printStackTrace();
}finally{
Global.lockMap.get(filePath).unlock();
}
}else {
new File(saveDirPath).mkdirs();
content=getTime()+Global.separator+content+"\r\n";
createFile(filePath, content);
Global.pathMap.put(filePath, new StringBuffer());
Global.lockMap.put(filePath, new ReentrantLock());
}
return Global.pathMap.size();
}
/**
* 将map的内容写到文件
* @param ctMap
*/
public static synchronized void writeFile(ConcurrentHashMap<String,StringBuffer> ctMap){
Iterator<Entry<String,StringBuffer>> it = ctMap.entrySet().iterator();
while (it.hasNext()) {
Entry<String,StringBuffer> entry = it.next();
if(entry.getValue().length()<1)
continue;
Global.lockMap.get(entry.getKey()).lock();
try{
createFile(entry.getKey(), entry.getValue().toString());
Global.pathMap.put(entry.getKey(), new StringBuffer());
}catch(Exception e){
e.printStackTrace();
}finally {
Global.lockMap.get(entry.getKey()).unlock();
}
}
}
/**
* 根据Map的K,V写文件
* @param path
* @param content
*/
public static void createFile(String path,String content){
BufferedWriter writer = null;
try {
writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(path, true),"UTF-8"));
writer.write(content);
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 定时执行写入文件,清空Map的任务
* @return
*/
public static TimerTask timeTask(){
TimerTask timerTask=new TimerTask() {
public void run() {
writeFile(Global.pathMap);
}
};
return timerTask;
}
/**
* 获得当前日期
* @return 例:2015-10-11
*/
private static String getDate(){
Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int day = calendar.get(Calendar.DAY_OF_MONTH);
String dateStr = "" + year + "-" + month + "-" + day;
return dateStr;
}
/**
* 获得当前时间 例:2015-10-11 15:00:01
* @return
*/
private static String getTime(){
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return simpleDateFormat.format(new Date().getTime());
}
}
改进部分:
1.定时执行部分,使用了Timer类,创建定时任务,定时执行写入从Map中读取数据,写入文件的任务。
定义Timer类,等待时间和存放Path的ConcurrentHashMap
/*保存路径和内容*/
public static ConcurrentHashMap<String,StringBuffer> pathMap=new ConcurrentHashMap<String,StringBuffer>();
/*定义Timer类*/
public static Timer timer = null;
/*定时任务等待时间 单位毫秒*/
public static long waitTime = 200;
定义TimerTask方法:
/**
* 定时执行写入文件,清空Map的任务
* @return
*/
public static TimerTask timeTask(){
TimerTask timerTask=new TimerTask() {
public void run() {
writeFile(Global.pathMap);
}
};
return timerTask;
}
重写Servlet的init()方法,执行定时任务
public void init() {
System.out.println("init Timer.");
Global.timer = new Timer(true);
Global.timer.schedule(Data.timeTask(), 0,Global.waitTime);
}
2.由于涉及到了并发存储。将content写入StringBuffer–>从StringBuffer中读取内容写入到位文件–>清空StringBuffer这一复合操作,会在并发执行时会出现竞技条件问题。因此这里使用了显示锁处理StringBuffer的写入,读取和存储。为尽可能减小锁的冲突,对每一个Path加锁
定义存放锁的ConcurrentHashMap
/*存放每隔path锁的map*/
public static ConcurrentHashMap<String,Lock> lockMap=new ConcurrentHashMap<String,Lock>();
初始化每一个path的锁
Global.pathMap.put(filePath, new StringBuffer());
Global.lockMap.put(filePath, new ReentrantLock());
在写入StringBuffer时获得锁,写入结束时释放锁
try{
Global.lockMap.get(filePath).lock();
Global.pathMap.put(filePath,Global.pathMap.get(filePath).append(getTime()+Global.separator+content+"\r\n"));
}catch(Exception e){
e.printStackTrace();
}finally{
Global.lockMap.get(filePath).unlock();
}
在执行将StringBuffer写入到文件–>清空StringBuffer的操作时,从ConcurrentHashMap中获得对应的锁,在清空后释放对应锁,这样就可以保证写入StringBuffer和清除StringBuffer是互斥的。
try{
createFile(entry.getKey(), entry.getValue().toString());
Global.pathMap.put(entry.getKey(), new StringBuffer());
}catch(Exception e){
e.printStackTrace();
}finally {
Global.lockMap.get(entry.getKey()).unlock();
}
这样就可以保证不同url执行并发的效率。
在实际测试中,使用Http_load工具进行压力测试,22个不同的url,50个并发进行请求,其测试结果为每秒可以执行2000次请求,满足性能需求。
另外,以下代码会存在隐患:
}else {
new File(saveDirPath).mkdirs();
content=getTime()+Global.separator+content+"\r\n";
createFile(filePath, content);
Global.pathMap.put(filePath, new StringBuffer());
Global.lockMap.put(filePath, new ReentrantLock());
}
由于这部分符合操作并不是原子的,并发执行时会出现一些数据丢失的现象,出于实际性能需求,并发执行这段代码可以大大提高访问的效率,因此暂时不同步这段代码。