【问题标题】:How to solve transactional logging in Java?如何解决 Java 中的事务日志?
【发布时间】: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)

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

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

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

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

更新:

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

我现在基本上使用以下 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


【解决方案1】:

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

您的解决方案在崩溃恢复情况下也存在问题。事务管理器将处理事务资源的提交,但易失性日志消息将丢失。这可能会或可能不会被容忍,具体取决于您的要求。

【讨论】:

  • 该解决方案尚未设计为集成到事务管理器中。我会在 WE 上发布更多信息。
【解决方案2】:

所以,我想了解一下 cmets 离开你的地方 :) @MichaelBorgwardt 建议登录到数据库,@cherouvim 补充说您应该定期将数据库中的日志条目写入文件。

也可以在事务由不同的线程提交后直接开始将日志条目转换为文件(这样可以避免提交和将其放入文件之间的间隙)。

另一种可能性是为每个事务编写一个日志文件。在“回滚”时,您将其删除。在“提交”时,您将其附加到您已经存在的日志文件中(或者您决定无论如何都没有单个日志文件,而是一个 logdir,并且在“提交”时您移动单个日志文件)。

【讨论】:

    【解决方案3】:

    Spring 参考中有a section about Logging

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

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

    log4j.logger.org.springframework.transactions=DEBUG
    

    【讨论】:

    • 我认为您误解了问题/主题。这不是关于如何获取日志输出,而是关于如何不记录在事务期间创建的消息(例如用户代码中说“做了这个和那个”),这是回滚的......如果你知道任何日志记录-能够做到这一点的框架......
    【解决方案4】:

    为什么不创建一个方面并实现一个建议,尤其是在返回建议后,请查看自 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() {
        // ...
        }
    }
    

    【讨论】:

    • 您的意思是使用 Aspects 而不是 try-catch-finally-block 吗?那是我的下一步。但是事务提交和回滚不是也通过方面完成的吗?首先调用哪个方面?
    • 好的,我的意思是方面。当您使用一个方面时,您正在使用过滤器,如果您使用 eclipse 调试您的应用程序,您可以看到完整的调用链。事务实际上也是一个方面,在您的方法、接口和类中使用 @Transaction 注释很容易实现,请参阅 Spring 文档:link
    • 如果您对您的 dao 层有任何疑问,请提出关于它的完整问题 XD。回滚它用于持久性、dao 和/或服务层的测试,我在测试类的注释中使用过,请参阅文档link
    • 我真的不明白你的最后一个问题!!,当你使用方面时,你拦截了一个方法的调用,它在方法代码执行之前执行,我在上面指出了含义您可以使用这些建议来管理您在方法调用中的操作方式。
    • 见 Spring Doc link 对不起,我把我的回答弄碎了,因为这个网站的计时器。希望对你有帮助
    【解决方案5】:

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

    transaction.enlistResource(myXALogger)

    并让它实现 XAResource (JavaDoc)

    【讨论】:

    • 您的回答似乎很有希望。不幸的是,我现在没有时间尝试。如果我使用 @Transactional-annotation(或基于 xml 的 JTA 配置(atomikos),我将不得不使用 JCA 实现,对吧?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-10-26
    • 1970-01-01
    • 2019-01-04
    • 1970-01-01
    • 2022-01-25
    • 2022-12-28
    • 2013-07-27
    相关资源
    最近更新 更多