RabbitMQ:在 Cloud Foundry 上啟用 Grails 全文檢索

工程 | Peter Ledbrook | 2011 年 8 月 29 日 | ...

在我的第二篇關於 Grails 和 Cloud Foundry 的部落格文章中,我介紹了 Grails Twitter 範例 的一個變體,它可以託管在 CloudFoundry.com 上。當時我提到,使用 Searchable 外掛程式進行全文檢索會將您限制為單一應用程式實例,因為搜尋索引對於每個實例都是唯一的。換句話說,您很可能根據瀏覽器路由到的應用程式實例而獲得不同的搜尋結果。

我也說過,解決此問題的一個選項是在實例之間同步搜尋索引。但這聽起來並不容易,不是嗎?碰巧的是,Cloud Foundry 中引入 RabbitMQ 服務意味著所需的程式碼變更遠小於您可能預期的。因此,讓我們看看我是如何為 Grails Twitter 狀態訊息新增全文檢索功能的。

使狀態訊息可搜尋

Searchable 外掛程式強烈假設您想要為標準 GORM 網域類別建立索引。這意味著 Hibernate/SQL。但 Grails Twitter 狀態訊息儲存在 MongoDB 中,而不是 MySQL 中。我們可以讓它們可搜尋嗎?是的,我們可以,但會犧牲一些功能。

與一般網域類別一樣,搜尋Status實例的第一步是新增searchable屬性

package org.grails.twitter

import org.grails.twitter.auth.Person

class Status {
    static mapWith = "mongo"
    static transients = ["author"]

    static searchable = {
        only = ["message", "dateCreated"]
        authorId index: "no", store: "yes"
    }
	
    String message
    Long authorId
    List<String> tags = []
    Date dateCreated
	
    Person getAuthor() {
        return Person.get(authorId)
    }

    static constraints = {
        message maxSize: 160
    }
}

在這種情況下,我希望能夠搜尋建立日期和訊息內容,但僅此而已。我也想從搜尋結果連結到訊息的作者。但是,如果authorId未建立索引,則搜尋結果將不包含發布者的 ID。因此,我將其儲存authorId在索引中,但不使其可搜尋 (index: "no")。很簡單,不是嗎?當顯示搜尋結果時,它們現在可以包含每則訊息作者的姓名。

為非 Hibernate 網域類別建立索引的一個重大限制是鏡像功能將無法運作。這表示當儲存新訊息時,它們不會自動建立索引。幸運的是,我們實際上不希望在這裡使用此行為,因此我在Config.groovy:

searchable {
    ...
    mirrorChanges = false
    bulkIndexOnStartup = false
}

中停用了鏡像和「啟動時大量建立索引」。當然,我們確實希望在啟動時為狀態訊息建立索引,因為 Cloud Foundry 上的檔案系統是暫時性的,因此搜尋索引需要在每次啟動時重建。但是自動索引也不適用於非 Hibernate 網域類別,因此我求助於在BootStrap.groovy:

...
class BootStrap {

    def searchableService
    def springSecurityService

    def init = { servletContext ->
        ...
        // Index all Hibernate mapped domain classes.
        searchableService.reindex()

        // Index all status messages.
        def statusMessages = Status.list()
        log.info "Indexing ${statusMessages.size()} status messages"
        Status.reindex(statusMessages)
        log.info "Finished indexing"
    }
    ...
}

結尾進行手動索引。程式碼不多,但足以使狀態訊息可搜尋。剩下的就是確保為新訊息建立索引,並在應用程式實例之間同步搜尋索引。

使用 RabbitMQ 同步

保持搜尋索引同步的基本模型非常簡單

每次儲存狀態訊息時,都會將訊息傳送到 RabbitMQ Broker,然後 Broker 將其轉發到所有應用程式實例。然後每個實例都會為訊息識別的Status實例建立索引。

在我們可以實作此功能之前,我們需要安裝 RabbitMQ 外掛程式

    grails install-plugin rabbitmq

下一步是使用適當的交換器和佇列設定 Broker。我之前已經寫過關於 AMQP 協定RabbitMQ 外掛程式 的部落格文章,因此我不會在此處詳細介紹交換器和佇列。只要說我們只需要一個 fanout 交換器(其中所有訊息都會路由到所有監聽器)和一個訂閱該交換器的 Grails 服務就足夠了。所以在Config.groovy我新增了

rabbitmq {
    connectionfactory {
        username = 'guest'
        password = 'guest'
        hostname = 'localhost'
    }

    queues = {
        exchange name: 'search.sync', type: fanout, durable: false
    }
}

重要部分是交換器宣告:當應用程式部署到 Cloud Foundry 時,連線工廠設定會被忽略,因為 RabbitMQ 服務在執行階段會繫結到應用程式。

傳送訊息只是一行程式碼

...
class StatusService {
    def springSecurityService
    def tagService
    
    void updateStatus(long userId, String message) {
        def status = new Status(message: message, authorId: userId).save(flush: true, failOnError: true)
        rabbitSend 'search.sync', '', "${status.id}:${status.class.name}"
        
        runAsync {
            tagService.extractTagsFromMessage(status)
        }
    }
    ...
}

而為狀態訊息建立索引的服務也沒複雜多少

package org.grails.twitter

class SyncService {
    static rabbitSubscribe = "search.sync"
    static transactional = false

    def grailsApplication
    def searchableService

    void handleMessage(String message) {
        def parts = message.split(/:/)
        if (parts.size() != 2) {
            log.error "Invalid message: $message"
            return
        }

        def domainClass = grailsApplication.getDomainClass(parts[1])
        log.debug "Reindexing instance ${parts[0]} of ${parts[1]}"
        try {
            searchableService.reindex(domainClass.clazz.get(parts[0]))
        }
        catch (Exception ex) {
            log.error "Failed to index instance ${parts[0]} of ${parts[1]}", ex
        }
    }
}

所以rabbitSend()方法用於傳送包含Status實例 ID 和類別名稱的簡單字串。在這種情況下,我們只處理Status實例,但使服務通用於所有潛在的可搜尋網域類別很有用。此外,使用 Groovy 表示我們不必進行任何令人討厭的反射:我們只需取得類別並直接在其上呼叫我們想要的方法!

的重點部分SyncServicerabbitSubscribe屬性和handleMessage()方法。前者宣告服務應訂閱交換器 "search.sync",這是我將訊息傳送到的交換器。handleMessage()方法在每次從該交換器接收到訊息時都會被調用,訊息內容作為其引數。因此,該方法會擷取類別名稱和實例 ID,並使用 GrailsDomainClass.get()方法從資料儲存區(我們的Status訊息的 MongoDB)中檢索相關實例。最後,searchableService.reindex()方法將狀態訊息新增到本機搜尋索引。當然,這會在每個應用程式實例上發生。

應用程式現在已準備好部署到 Cloud Foundry 並擴展到您允許的任意多個實例!您可以在 CloudFoundry.com 上查看結果。請注意,在 GitHub 專案中,我已完成一些 UI 工作以支援全文檢索,但這些變更與手頭的主題並非真正相關。

總結

我不得不說,我很驚訝自己只需要這麼少的程式碼就能讓搜尋索引同步。不僅如此,而且我能夠專注於如何解決問題,而不是如何編寫程式碼,因為編碼非常簡單。更重要的是,使用 Cloud Foundry 意味著部署包括建立和繫結 RabbitMQ 服務,然後執行grails prod cf-update命令以將變更推送到伺服器。簡單的事情。

正如您所看到的,RabbitMQ 可以為雲端相關問題提供創新的解決方案,而 Grails 外掛程式透過其慣例的力量使其非常易於使用。您可以在同一應用程式的不同實例、不同的 Grails 應用程式,甚至使用不同語言和框架編寫的應用程式之間進行通訊。例如,我們可以部署一個簡單的 Node.js 或 Sinatra 應用程式,記錄和顯示 "search.sync" 訊息,以便您可以追蹤它們。基本上,RabbitMQ 是您雲端工具箱中的必備項目。

取得 Spring 電子報

隨時關注 Spring 電子報

訂閱

搶先一步

VMware 提供培訓和認證,以加速您的進展。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

了解更多

即將到來的活動

查看 Spring 社群中所有即將到來的活動。

查看全部