【问题标题】:When using HornetQ for audit logging to requests to DB, start log entry is not always committed before end log entry当使用 HornetQ 对 DB 请求进行审计日志记录时,开始日志条目并不总是在结束日志条目之前提交
【发布时间】:2014-09-08 13:22:56
【问题描述】:

我有一个使用 JBoss AS 7、Hibernate 和 HornetQ 的应用程序。我们使用 HornetQ 来处理进入应用程序的所有请求的日志记录。当一个请求进来时,我们发送一个“开始日志条目”消息,当使用该消息时,记录请求的 UUID、它的开始时间以及与请求相关的其他各种数据。当请求完成时,我们发送一条“结束日志条目”消息,当被消费时从数据库中查找起始日志条目(带有 UUID),并将结束时间添加到请求中。

经常发生的问题是在“结束日志条目”中完成的数据库查找找不到匹配的起始条目,因为实体管理器实际上还没有将起始日志条目提交/刷新到数据库。在结束日志条目尝试查找它之前,我需要保证开始日志条目在数据库中。有什么想法可以确保发生这种情况吗?

以下是我们拥有的相关类:

LogFilter.java - 这是 web.xml 中定义的过滤器,用于拦截所有请求并记录它们

public class LogFilter implements Filter {
    @Inject
    LoggingService ls;

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
      // do stuff to create the log entry

      try {
        ls.startLogEntry(logEntry);
        filterChain.doFilter(servletRequest, servletResponse);
      } finally {
          try {
            ls.endLogEntry(logEntry);
          } catch (Exception e2) {
            // log stacktrace to the server logs
          }
      }
   }
}

LoggingService.java - 注入 LogFilter 的服务

public class LoggingService {
  private static Log logger = LogFactory.getLog(LoggingService.class);
  @PersistenceContext(unitName="p1")
  private EntityManager em;

  // find a log entry by its generated uuid when the request started
  public LogEntry getLogEntry(String uuid) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<LogEntry> c = cb.createQuery(LogEntry.class);
    Root<LogEntry> root = c.from(LogEntry.class);

    c.where(cb.equal(root.get(LogEntry_.uuid), uuid));

    TypedQuery<LogEntry> tq = em.createQuery(c);
    return JpaUtil.getSingleResultOrNull(tq);
  }

  // send message to write the log entry without the end time
  public void startLogEntry(LogEntry logEntry) {
    AuditLogProducer.sendLogEntryToQueue(logEntry);
  }

  public void endLogEntry(LogEntry logEntry) {
    if (logEntry != null) {
      logEntry.setRequestEnd(new Date());
      AuditLogProducer.sendLogEntryToQueue(logEntry);
    }
  }
}

AuditLogProducer.java - 负责将消息发送到队列的类

public class AuditLogProducer {
  private static final String QUEUE_LOOKUP = "jboss/queue/AuditLogQueue";
  private static final String CONNECTION_FACTORY = "ConnectionFactory";
  private static Log log = LogFactory.getLog(AuditLogProducer.class);

  public static void sendLogEntryToQueue(LogEntry logEntry) {
    QueueSession session = null;
    QueueConnection connection = null;

    try {
      Context context = new InitialContext();
      QueueConnectionFactory factory = (QueueConnectionFactory) context.lookup(CONNECTION_FACTORY);
      connection = factory.createQueueConnection();
      session = connection.createQueueSession(false, QueueSession.AUTO_ACKNOWLEDGE);
      Queue queue = (Queue) context.lookup(QUEUE_LOOKUP);

      QueueSender sender = session.createSender(queue);
      ObjectMessage message = session.createObjectMessage();

      message.setObject(logEntry);
      sender.send(message);
      session.close();
    } catch (Exception e) {
      log.error("problem sending message to queue: " + e.getMessage());
    } finally {
      if (session != null) {
        try {
          session.close();
          connection.close();
        } catch (Exception e) { }
      }
    }
  }
}

AuditLogConsumer.java - 负责从队列中消费消息的类

@MessageDriven(
  activationConfig={
      @ActivationConfigProperty(
        propertyName="destinationType",
        propertyValue="javax.jms.Queue"
      ), @ActivationConfigProperty(
        propertyName="destination",
        propertyValue="queue/AuditLogQueue"
      )
    }
)
public class AuditLogConsumer implements MessageListener {
  private static Log log = LogFactory.getLog(AuditLogConsumer.class);

  @Inject
  LogBuilder logBuilder;

  @Inject
  LogPolisher logPolisher;

  @Override
  public void onMessage(Message message) {
    if (message instanceof ObjectMessage) {
      ObjectMessage msg = (ObjectMessage) message;
      LogEntry logEntry = null;

      try {
        logEntry = (LogEntry) msg.getObject();
      } catch (JMSException e) {
        log.error("Problem retrieving JMS message from the AuditLogQueue: " + e.getMessage(), e);
        return;
      }

      if (logEntry.getRequestEnd() == null) {
        logBuilder.insertLogEntry(logEntry);
      } else {
        logPolisher.updateLogEntry(logEntry);
      }
    }
  }
}

LogBuilder.java - 单独负责将日志条目合并到数据库中的类 - 曾经属于一个类,但希望将创建和更新分为 2 个类,以确保将开始写入数据库准时(最终没有工作)

@Singleton
public class LogBuilder {
  @PersistenceContext(unitName="p1")
  private EntityManager em;

  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public void insertLogEntry(LogEntry log) {
    em.merge(log);
    em.flush();
  }
}

LogPolisher.java - 单独负责查找开始日志条目、设置结束时间以及将新日志合并到数据库的类 - newLog.setRequestEnd(requestEnd) 有时会在上一行返回 null 时失败,因为它没有找到开始日志条目(因为它还没有被写入)。

@Singleton
public class LogPolisher {
  private static Log log = LogFactory.getLog(LogPolisher.class);
  @Inject
  LoggingService loggingService;

  @PersistenceContext(unitName="p1")
  private EntityManager em;

  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public void updateLogEntry(LogEntry logEntry) {
    Date requestEnd = logEntry.getRequestEnd();

    try {
      LogEntry newLog = loggingService.getLogEntry(logEntry.getUuid());

      newLog.setRequestEnd(requestEnd);
      em.merge(newLog);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
}

LogEntry.java 只是一个普通的休眠 bean,我不会在这里浪费更多的空间来展示它的代码。

【问题讨论】:

  • 这是一个问题还是架构审查?我正在尝试确定您的问题在哪里
  • 再次查看第二段。我在问如何确保实体管理器在第二个事务发生之前刷新第一个事务
  • 但要了解您必须知道 EntityManager 在图片上的位置。无论如何..这里的表现会很糟糕。您每次都在创建消费者和生产者......如果您要使其同步,为什么还需要消息传递?这里有一个数据库会更好。如果您想要真正的消息传递,请让消费者或生产者在某处保持开放。您实际上依赖于队列顺序的事实告诉我,您将消息系统混淆为数据库。 Database=store/retrieve Messaging=deliver

标签: hibernate jboss jboss7.x messaging hornetq


【解决方案1】:

我最初不想回答这个问题,因为我很难理解您的问题。但我发现了一些可能有助于您确定问题的问题。

我 - 您似乎混淆了消息传递和数据库。这似乎很明显,但这是一个很常见的误解。消息传递用于异步传递消息。 (从不存储)。您实际上需要将数据库与队列匹配这一事实可能表明您的架构存在问题。

II - 性能会影响这些生产者/消费者...它们都是同步的,因为您正在为每个日志条目创建消费者或生产者。您应该缓存对象并在每个线程中重用它们。这可能表明正在使用反模式 (i)。

III - 我不确定这里是否使用了 XA,但某些事务管理器 (TM) 不能保证首先提交分支的顺序。新版本的事务管理器正在修复此问题。

IV - 您正在使用另一个框架 (JPA) 构建一个框架(您的日志工具)。除非你需要动态的东西。我会直接使用 JDBC。否则你会被绑定在一个框架*框架上。它变得复杂......但这只是恕我直言。

我只是想在这里为您提供一些帮助.. 即使它没有直接回答您的问题。

【讨论】:

  • 在这种情况下,消息队列的重点是将日志条目写入数据库与实际的Web请求分开(让系统尽可能将日志条目写入数据库而不是等待等待写入日志条目的 Web 请求)。数以百万计的请求在几天内到达服务器,写入/更新日志不应该成为瓶颈。
  • 显然我花了太长时间来编辑我之前的评论。让我重新开始。谢谢您的回答;更多 cmets 如下: I - 在这种情况下,消息队列的重点是将日志条目写入数据库与实际 Web 请求分开(让系统尽可能将日志条目写入数据库而不是阻止 Web请求等待写入日志条目)。数以百万计的请求在几天内到达服务器,写入/更新日志不应该阻止对用户的响应。我认为这将是使用消息队列的完美案例。
  • II - 这里的性能实际上还可以,因为消费者位于 JBoss 内的消费者池中。一旦消费者可用,它就会抓取消息队列中的下一条消息。向队列发送消息需要 1 到 4 毫秒。 III - 无论使用 XA 数据源还是常规数据源(Oracle 作为后端),我在这里遇到的问题都会发生。我很想看看 TM 将来会修复什么。
  • IV - 我们应用程序的日志记录部分是整个系统的一小部分,所有这些都使用 JPA。我认为使用 JPA 规范的目的是将数据库交互写入标准——然后不管后端数据库如何,只要它支持 JPA,我们就可以相当轻松地进行交换。
  • 但是回到消息队列的问题——保证消息按照它们被传递的顺序被消费。我可以在我们的服务器日志(jboss)中看到这种情况。所以问题是,如果它们被按顺序使用,那么在我看来,消息触发的内容(在我们的例子中,我们插入或更新日志)也应该按顺序发生。消息队列的美妙之处在于消息可以产生任何类型的活动——我不确定为什么在收到消息后无法写入数据库条目。当然,我不是唯一一个尝试做这种事情的人吗?
猜你喜欢
  • 2019-07-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-08
  • 2017-06-26
  • 2016-08-02
相关资源
最近更新 更多