def fillReadBehind(gotItem: QItem => Unit)(gotCheckpoint: Checkpoint => Unit): Unit = { val pos = if (replayer.isDefined) replayer.get.position else writer.position val filename = if (replayerFilename.isDefined) replayerFilename.get else queueName reader.foreach { rj => if (rj.position == pos && readerFilename.get == filename) { // we've caught up. rj.close() reader = None readerFilename = None } else { readJournalEntry(rj) match { case (JournalItem.Add(item), _) => gotItem(item) case (JournalItem.Remove, _) => removesSinceReadBehind -= 1 case (JournalItem.ConfirmRemove(_), _) => removesSinceReadBehind -= 1 case (JournalItem.Continue(item, xid), _) => removesSinceReadBehind -= 1 gotItem(item) case (JournalItem.EndOfFile, _) => // move to next file and try again. val oldFilename = readerFilename.get rj.close() readerFilename = Journal.journalAfter(queuePath, queueName, readerFilename.get) reader = Some(new FileInputStream(new File(queuePath, readerFilename.get)).getChannel) log.info("Read-behind on '%s' moving from file %s to %s", queueName, oldFilename, readerFilename.get) if (checkpoint.isDefined && checkpoint.get.filename == oldFilename) { gotCheckpoint(checkpoint.get) } fillReadBehind(gotItem)(gotCheckpoint) case (_, _) => } } } }
Kestrel对于过期数据的处理很巧妙,他通过两种方式来触发对过期数据的检查,一种是定时任务,我们在Kestrel.scala看到的定时器,就是设置定期去检查数据是否过期,第二种是当消费队列中的数据时会检查数据是否过期,这两种方法检查的都是队列头的数据。
当你需要对队列中的数据进行消费或者检查过期数据时,都需要调用fillReadBehind方法,因为你的内存是有一定空间的,它不一定能够把之前所有持久化的数据都导入内存中,所以当内存中一旦有空闲空间,都会从文件中继续读取数据到内存。
另外一个有意思的地方是,Journal对象会记录一个removesSinceReadBehind对象,这个对象记录从读文件开始到现在所有 ReadFile 模式下接受的remove操作的个数,这样每在文件中读到一个remove操作,就对其进行减一,就收一个pop操作就对该变量加一。这个方法能够保证服务在重启时replay这些日志文件时,消息没有丢失,但是对于消息的重复发送,不能进行保证。
对于事物,Kestrel会在PersistentQueue中保存一个Map中,用于存储该事物<事物ID,Item>, 当确认消费时,从map中删除这个事物,如果不提交,则把该消息插入到队列的首位,并从Map中删除该数据。
Kestrel中如果对消息队列选择了持久化,那么,客户的每一个操作都会记录为日志。这个日志正如Mysql一样,通过回放能够恢复之前的数据。