利用java concurrent 包实现日志写数据库的并发处理
一、概述
在很多系统中,往往需要将各种操作写入数据库(比如客户端发起的操作)。
最简单的做法是,封装一个公共的写日志的api,各个操作中调用该api完成自己操作日志的入库。但因为入数据库效率比较低,如果每个操作自己入库,则会影响响应速度。而且当操作并发度很高时,往往同时有多个线程在写数据库,也会对系统有影响。
考虑的解决方案是,这个api并不实际完成入库,而是将每个操作日志信息写到一个公共的缓存中,然后应用系统起了一个独立的线程(一直运行)在后台进行入库。如果当前缓存中有记录,就写库,没有记录,就堵塞住。
这里关键的是要有一个缓存日志的数据结构,我们这里使用java current包中的并发数据结构LinkedBlockingDeque类。
二、具体代码
import java.util.ArrayList; import java.util.List; import java.util.concurrent.LinkedBlockingDeque; public class LogManager implements Runnable { //定义缓存数据结构,支持并发操作 private static LinkedBlockingDeque<ActionLog> list = new LinkedBlockingDeque<ActionLog>(); static{ //创建并启动线程,该线程实现日志的入库 new Thread(new LogManager()).start(); } public static void addActionLog(ActionLog log) { list.add(log); } @Override public void run() { List<ActionLog> items = new ArrayList<ActionLog>(); while (true) { //从缓存中取出1条日志,但不从缓存中删除。用于检查队列中是否有数据 ActionLog item = list.peek(); if (item == null && items.size() > 0) { //item为null说明缓存中没有日志了,这时如果临时队列items中有记录,就可以批量入库了 work(items); } //从缓存中取出并删除最前面的日志,如果无数据,在堵塞 ActionLog result = deleteItem(); //将取出的记录放到临时队列中 items.add(result); } } private ActionLog deleteItem() { ActionLog result = null; try { //取出并删除最前面的一条数据,如果缓存中无数据,则堵塞住 result = list.takeFirst(); } catch (InterruptedException e) { e.printStackTrace(); } return result; } private void work(List<ActionLog> items) { //TODO 完成入库。这里代码没有提供 //入库后删除临时队列中记录 items.clear(); } }
各种工作线程调用 addActionLog 方法将要入库的日志放到缓存中。由LogManager通过自己的线程控制入库。
三、小结
上述代码虽然简单,但很好的解决了前面提出的问题。不过这个解决方案存在一个问题。
如果说系统产生日志的速度超过了单个线程入库的速度,上述代码就有问题。就会造成该线程不停地入库。
这时就需要考虑增加额外的处理速度,如增加入库线程。
所以上述代码,只适合并发日志量不是特别大的情况下的场景。