【发布时间】:2012-04-01 19:30:49
【问题描述】:
我想实现以下目标:
在事务内部,我想生成多条日志消息。仅当事务成功提交时才应写入这些日志消息。如果事务回滚,则不得记录日志消息。
我找不到任何东西来实现这一点(使用 spring、hibernate、atomikos),所以我写了这个小包装器(我省略了几个方便的方法):
public class TransactionLogger {
private Logger logger;
private Map<Long, LinkedList<LogRecord>> threadBuffers =
new HashMap<Long, LinkedList<LogRecord>>();
public TransactionLogger(Logger logger) {
this.logger = logger;
}
private void addRecord(LogRecord rec) {
LinkedList<LogRecord> list =
threadBuffers.get(Thread.currentThread().getId());
if (list == null) {
list = new LinkedList<LogRecord>();
threadBuffers.put(Thread.currentThread().getId(), list);
}
list.add(rec);
}
private LinkedList<LogRecord> getRecords() {
if (threadBuffers.containsKey(Thread.currentThread().getId())) {
return threadBuffers.remove(Thread.currentThread().getId());
} else {
return new LinkedList<LogRecord>();
}
}
public void commit() {
for (LogRecord rec : getRecords()) {
rec.setLoggerName(logger.getName());
logger.log(rec);
}
}
public void rollback() {
getRecords();
}
/**
* If the resulting log entry should contain the sourceMethodName
* you should use logM(Level,String,String) instead,
* otherwise TransactionLogger.commit() will get
* inferred as sourceMethodName.
*/
public void log(Level l, String sourceClassName, String msg) {
LogRecord rec = new LogRecord(l, msg);
rec.setSourceClassName(sourceClassName);
addRecord(rec);
}
/**
* Same as log(Level,String,String), but the sourceMethodName gets set.
*/
public void logM(Level l, String sourceClassName, String msg) {
StackTraceElement[] trace = Thread.currentThread().getStackTrace();
LogRecord rec = new LogRecord(l, msg);
rec.setSourceClassName(sourceClassName);
if (trace != null && trace.length > 1) {
rec.setSourceMethodName(trace[2].getMethodName());
}
addRecord(rec);
}
}
您如何看待这种方法?它是否有任何重大或次要的缺陷或问题? 或者更好的是,有没有现成的解决方案?
更新:
由于我也在使用 JTA,所以我有了一个新想法。将 TransactionLogger 实现为事务感知消息队列的接收者会解决时间问题还是只会让事情变得更复杂?
更新:
我认为记录到数据库,然后按照 cmets 中的建议定期将数据库中的日志条目写入文件中,这是解决此问题的一个很好的方法:
专业人士:
- 正常实施提供
- 与 TransactionManager 集成
- 日志文件中的日志条目可以按时间戳排序
缺点:
- 日志文件不是最新的(取决于周期性任务间隔)
- 依赖于数据库结构
- 简单的非事务性事件的日志记录变得依赖于 dbconnection
- 可能更大的整体日志记录开销
以下是我看到的包装器的优缺点:
专业人士:
- 独立于数据库和框架
- 简单的实现
- 日志文件始终是最新的
缺点:
- 日志文件中的日志条目不是按事件时间戳排序,而是按“事务完成”时间戳排序(长事务会导致日志文件非常混乱。
-
rollback()和commit()必须“手动”调用,这可能导致编程错误(如果忘记调用这些方法,可能会出现 OutOfMemoryError)
我认为这两者的结合,比如在“包装器”方法中缓冲日志记录比使用上述两种方法中的一种更糟糕,因为可能存在不一致的日志文件(由于应用程序崩溃而忘记了日志条目) .
我现在的决定是保留我的“包装”。以下原因对此决定至关重要(按重要性排序):
- 我更喜欢始终保持最新的日志文件,而不是完全有序的日志条目
- 就我而言,长事务非常少见
- 我能够将
rollback()和commit()的使用减少到几种方法。 - 这个解决方案现在已经存在。
顺便说一句:我想提高我的英语水平。因此,如果您发现我的文章中有任何错误,请指出,我会很高兴。
更新:
简单来说,我是这样使用它的:
/*
* This objects contains one or more TransactionLogger(s) and exposes rollback()
* and commit() through rollbackLogs() and commitLogs().
*/
@Autowired
private ITransactionalServer server;
public void someMethod(String someParam) {
boolean rollback = false;
try {
/*
* This method is for example annotated with @Transactional.
*/
return server.someTransactionalMethod(someParam);
} catch (Exception ex) {
logError(ex);
rollback = true;
} finally {
if (rollback) {
server.rollbackLogs();
} else {
server.commitLogs();
}
}
}
这仍然不是完美的,但现在对我来说似乎是一个“足够好的解决方案”。下一步将使用方面来装饰我的事务方法。
更新:
我在我的问题中添加了这个,因为我对接受我自己的答案感到很难过,尽管其他人让我在路上。
我现在基本上使用以下 Logger 的 AOP 方法。 (在我的实际应用程序中,我有不止一个这样的 Logger,所有这些 Logger 都由一个自定义的单例管理器管理。):
public class AopLogger extends Logger {
public static AopLogger getLogger(String name) {
LogManager manager = LogManager.getLogManager();
Object l = manager.getLogger(name);
if (l == null) {
manager.addLogger(new AopLogger(name));
}
l = manager.getLogger(name);
return (AopLogger)l;
}
private Map<Long, LinkedList<LogRecord>> threadBuffers = new HashMap<Long, LinkedList<LogRecord>>();
public AopLogger(String name) {
super(name, null);
}
public void beginTransaction() {
LinkedList<LogRecord> list = threadBuffers.get(Thread.currentThread().getId());
if (list == null) {
list = new LinkedList<LogRecord>();
threadBuffers.put(Thread.currentThread().getId(), list);
}
}
private void addRecord(LogRecord rec) {
LinkedList<LogRecord> list = threadBuffers.get(Thread.currentThread().getId());
if (list != null) {
list.add(rec);
} else {
super.log(record);
}
}
private LinkedList<LogRecord> getRecords() {
if (threadBuffers.containsKey(Thread.currentThread().getId())) {
return threadBuffers.remove(Thread.currentThread().getId());
} else {
return new LinkedList<LogRecord>();
}
}
public void commit() {
for (LogRecord rec : getRecords()) {
rec.setMillis(System.currentTimeMillis());
super.log(rec);
}
}
public void rollback() {
getRecords();
}
public void log(LogRecord record) {
addRecord(record);
}
}
还有这个方面:
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
@Service
@Aspect
@Order(10)
public class AopLogManager implements Ordered {
@Autowired
private AopLogger logger;
private Logger errorLogger = Logger.getLogger("ExceptionLogger");
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object logTransaction(ProceedingJoinPoint pjp) throws Throwable {
logger.beginTransaction();
Exception ex = null;
try {
return pjp.proceed();
} catch (Exception e) {
ex = e;
throw e;
} finally {
if (ex != null) {
logger.rollback();
errorLogger.severe(ex.getMessage());
} else {
logger.commit();
}
}
}
private int mOrder;
@Override
public int getOrder() {
return mOrder;
}
public void setOrder(int order) {
mOrder = order;
}
}
在我的 applicationContext.xml 我有以下几行:
<aop:aspectj-autoproxy />
<tx:annotation-driven transaction-manager="springTransactionManager" order="5"/>
到目前为止,这工作正常。
专业人士:
- 独立于数据库和框架
- 简单的实现
- 日志文件始终是最新的
-
rollback()和commit()在每次交易后自动调用
缺点:
- (日志文件中的日志条目不是按事件时间戳排序,而是按“事务完成”时间戳排序。我认为这不是一个很大的缺点,因为数据库操作确实发生在事务提交的时间并且一个事务的 LogRecords 仍然是正确排序的。)
【问题讨论】:
-
如果运行多线程,您会丢失日志文件中事件的确切顺序吗?
-
可能是的!这是一个很好的观点,我稍后会测试它。为了避免进一步的问题,我将在 commit() 中设置毫秒,直到我了解更多信息为止。谢谢!
-
最明显的解决方案似乎是登录到数据库表...
-
是的,我也想到了这个,我更喜欢这个解决方案,但不幸的是我必须登录到文件......
-
@cherouvim:因为我必须能够查看日志,即使数据库已关闭、损坏或其他任何情况……这是一个要求,我的软件必须满足。
标签: java spring logging transactions