繁体   English   中英

如何解决Java中的事务日志?

[英]How to solve transactional logging in Java?

我想实现以下目标:

在事务内部,我想生成多个日志消息。 只有在成功提交事务时才应写入这些日志消息。 如果事务回滚,则不得记录日志消息。

我找不到任何东西来实现这个(使用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实现为事务感知消息队列的接收器可以解决时间问题还是只会使事情变得更复杂?

更新:

我认为记录到数据库然后定期将该数据库中的日志条目写入评论中建议的定期任务中的文件,这是解决此问题的一个非常好的解决方案:

优点:

  • 正常的实施能力
  • 与TransactionManager集成
  • 日志文件中的日志条目可以按时间戳排序

缺点:

  • 日志文件不是最新的(取决于periodic-task-interval)
  • 数据库结构依赖
  • 记录简单的非事务性事件将变为依赖于dbconnection
  • 可能更大的整体日志开销

以下是我发布的包装器的优缺点:

优点:

  • 数据库和框架独立
  • 简单的实施
  • 日志文件始终是最新的

缺点:

  • 日志文件中的日志条目不是按事件时间戳排序,而是按“事务兼容” - 时间戳排序(长事务导致非常混合的日志文件。
  • 必须“手动”调用rollback()commit() ,这可能导致编程错误(如果忘记调用这些方法,可能会出现OutOfMemoryError)

我认为这两者的组合,比如在“包装器” - 方法中缓冲日志记录比使用上述两种方法中的一种更糟糕,因为可能存在不一致的日志文件(由于应用程序崩溃而忘记了日志条目)。

我现在的决定是保留我的“包装”。 以下原因对此决定至关重要(按重要性排序):

  1. 我更喜欢总是最新的日志文件,而不是完美排序的日志条目
  2. 在我的案例中,长期交易非常罕见
  3. 我能够将rollback()commit()的使用减少到只有几个方法。
  4. 这个解决方案现在已经存在。

顺便说一下:我想提高我的英语水平。 因此,如果您发现我的文章中有任何错误,如果您指出它们,我将很高兴。

更新:

简化我正在使用它:

/*
 * 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();
        }
    }
}

这仍然不是完美的,但现在它对我来说似乎是一个“足够好的解决方案”。 下一步将使用方面来装饰我的事务方法。

更新:

我在我的问题中添加了这个,因为我对接受我自己的答案感到不好,尽管其他人让我在路上。

我现在基本上使用AOP方法使用以下Logger。 (在我的实际应用程序中,我有多个这样的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()

缺点:

  • (日志文件中的日志条目不按事件时间戳排序,而是按“transaction-completition”-timestamp排序。我认为这不是一个很大的缺点,因为DB-actions实际上是在提交事务和LogRecords时发生的。仍然正确订购了一笔交易。)

如果您希望Logger仅在提交事务时“提交”其日志消息,那么您最好的解决方案是让您的Logger成为XAResource并使用XA事务。 这意味着您的XALogger会从事务管理器收到准备/提交通知。 这样,您的XAResource就成为参与XA转换的“资源管理器”。 您的XALogger需要在事务管理器中注册,就像注册JMS和JDBC资源一样。 您可以编写JCA实现,也可以(更简单)使用当前的java.transaction.Transaction注册java.transaction.Transaction

transaction.enlistResource(myXALogger)

并让它实现XAResource (JavaDoc)

为什么不创建一个方面并实现建议,特别是在返回建议之后,检查一下自2.0以来可用的spring文档:

Types of advice:

Before advice: Advice that executes before a join point, but which does not have the ability to prevent execution flow proceeding to the join point (unless it throws an exception).

After returning advice: Advice to be executed after a join point completes normally: for example, if a method returns without throwing an exception.

After throwing advice: Advice to be executed if a method exits by throwing an exception.

After (finally) advice: Advice to be executed regardless of the means by which a join point exits (normal or exceptional return).

Around advice: Advice that surrounds a join point such as a method invocation. This is the most powerful kind of advice. Around advice can perform custom behavior before and after the method invocation. It is also responsible for choosing whether to proceed to the join point or to shortcut the advised method execution by returning its own return value or throwing an exception.

如果您只需要记录每件事情是否正常,那么创建建议并保持代码清洁:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
    // ...
    }
}

所以,我想知道评论留给你的地方:) @MichaelBorgwardt建议登录数据库,@ cherouvim补充说你应该定期将数据库中的日志条目写入文件。

它还可以在事务由不同的线程提交之后直接开始将日志条目转换为文件(这样可以避免提交和在文件中提交它之间的差距)。

另一种可能性是为每个事务编写一个日志文件。 在'回滚'上删除它。 在'commit'中,您将它附加到您现有的日志文件中(或者您决定不使用单个日志文件,而是使用logdir并在'commit'上移动单个日志文件)。

目前还不清楚你用rec.setMillis(System.currentTimeMillis());实现了什么rec.setMillis(System.currentTimeMillis()); 记录的事件发生在记录创建时,而不是在提交时。 它只是缓存在RAM中直到提交时间。 通过以这种方式覆盖它,您可以记录提交时间而不是事件发生时间。 在阅读日志时,您必须有一些实际排序的指示,否则您将无法解释因果关系。

您的解决方案也会遇到崩溃恢复问题。 事务管理器将处理事务资源的提交,但是易失性日志消息将丢失。 根据您的要求,这可能是也可能是不可容忍的。

一节关于Logging in Spring Reference。

它显示了如何配置不同的日志框架,其中包括log4j

在您的情况下,配置的最后一行是:

log4j.logger.org.springframework.transactions=DEBUG

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM