自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

業(yè)務(wù)腳本:為什么說(shuō)可編程訂閱式緩存服務(wù)更有用?

存儲(chǔ) 數(shù)據(jù)管理
在那些讀多寫(xiě)多的服務(wù)當(dāng)中,實(shí)時(shí)交互類(lèi)的服務(wù)數(shù)量相當(dāng)多,并且這類(lèi)服務(wù)對(duì)于數(shù)據(jù)的實(shí)時(shí)性有著很高的要求。若是采用集中型緩存的方式,往往很難滿(mǎn)足此類(lèi)服務(wù)所提出的各項(xiàng)需求。

我們已經(jīng)習(xí)慣了使用緩存集群對(duì)數(shù)據(jù)做緩存。然而,這種常見(jiàn)的內(nèi)存緩存服務(wù)存在諸多不便之處。首先,集群會(huì)獨(dú)占大量的內(nèi)存。這意味著在資源有限的情況下,可能會(huì)對(duì)其他系統(tǒng)資源的分配造成壓力,影響整體系統(tǒng)的性能和穩(wěn)定性。

其次,不能原子修改緩存的某一個(gè)字段。在一些對(duì)數(shù)據(jù)一致性要求較高的場(chǎng)景中,這可能會(huì)引發(fā)數(shù)據(jù)不一致的問(wèn)題,從而影響業(yè)務(wù)的正常運(yùn)行。

再者,多次通訊有網(wǎng)絡(luò)損耗。尤其是在頻繁進(jìn)行數(shù)據(jù)交互的情況下,網(wǎng)絡(luò)損耗可能會(huì)導(dǎo)致數(shù)據(jù)傳輸延遲增加,降低系統(tǒng)的響應(yīng)速度。

另外,很多時(shí)候我們獲取數(shù)據(jù)并不需要全部字段,但因?yàn)榫彺娌恢С趾Y選,在批量獲取數(shù)據(jù)的場(chǎng)景下性能就會(huì)下降很多。這種情況在數(shù)據(jù)量較大時(shí)尤為明顯,會(huì)嚴(yán)重影響系統(tǒng)的效率。

而這些問(wèn)題在讀多寫(xiě)多的場(chǎng)景下,會(huì)更加突出。那么,有什么方式能夠解決這些問(wèn)題呢?

緩存即服務(wù)

可編程訂閱式緩存服務(wù)意味著我們能夠自行實(shí)現(xiàn)一個(gè)數(shù)據(jù)緩存服務(wù),進(jìn)而直接提供給業(yè)務(wù)服務(wù)使用。這種實(shí)現(xiàn)方式具有獨(dú)特的優(yōu)勢(shì),它可以依據(jù)業(yè)務(wù)的具體需求,主動(dòng)地對(duì)數(shù)據(jù)進(jìn)行緩存,并且能夠提供一些數(shù)據(jù)整理以及計(jì)算的服務(wù)。

雖然自行實(shí)現(xiàn)這樣的數(shù)據(jù)緩存服務(wù)過(guò)程較為繁瑣,然而其帶來(lái)的優(yōu)勢(shì)卻是顯著的。除了吞吐能力能夠得到提升之外,我們還可以實(shí)現(xiàn)眾多有趣的定制功能。比如,我們可以根據(jù)業(yè)務(wù)的特定邏輯對(duì)數(shù)據(jù)進(jìn)行個(gè)性化的整理和優(yōu)化,使其更符合業(yè)務(wù)的使用場(chǎng)景。

同時(shí),它還具備更好的計(jì)算能力。這使得我們?cè)谔幚頂?shù)據(jù)時(shí),能夠更加高效地進(jìn)行各種復(fù)雜的計(jì)算操作,為業(yè)務(wù)提供更強(qiáng)大的數(shù)據(jù)支持。

甚至,它可以讓我們的緩存直接對(duì)外提供基礎(chǔ)數(shù)據(jù)的查詢(xún)服務(wù)。這樣一來(lái),業(yè)務(wù)在獲取基礎(chǔ)數(shù)據(jù)時(shí)無(wú)需再通過(guò)繁瑣的流程從其他數(shù)據(jù)源獲取,大大提高了數(shù)據(jù)獲取的效率和便捷性,進(jìn)一步提升了整個(gè)業(yè)務(wù)系統(tǒng)的性能和靈活性。

圖片圖片

上圖展示了一個(gè)自實(shí)現(xiàn)的緩存功能結(jié)構(gòu)。不得不說(shuō),這種緩存的性能和效果更為出色,究其原因在于它對(duì)數(shù)據(jù)的處理方式與傳統(tǒng)模式大相徑庭。

在傳統(tǒng)模式下,緩存服務(wù)并不會(huì)對(duì)數(shù)據(jù)進(jìn)行任何加工處理,所保存的是系列化的字符串。在這種情況下,大部分的數(shù)據(jù)無(wú)法直接進(jìn)行修改。當(dāng)我們利用這種緩存對(duì)外提供服務(wù)時(shí),業(yè)務(wù)服務(wù)不得不將所有數(shù)據(jù)取出至本地內(nèi)存,隨后進(jìn)行遍歷加工才能投入使用。

然而,可編程緩存卻能夠?qū)?shù)據(jù)結(jié)構(gòu)化地存儲(chǔ)在 map 中。相較于傳統(tǒng)模式下序列化的字符串,它更為節(jié)省內(nèi)存。更為便捷的是,我們的服務(wù)無(wú)需再?gòu)钠渌?wù)獲取數(shù)據(jù)來(lái)進(jìn)行計(jì)算,如此便能夠節(jié)省大量網(wǎng)絡(luò)交互所耗費(fèi)的時(shí)間,因而非常適合應(yīng)用于對(duì)實(shí)時(shí)要求極高的場(chǎng)景之中。

倘若我們的熱數(shù)據(jù)量頗為龐大,那么可以結(jié)合 RocksDB 等嵌入式引擎,憑借有限的內(nèi)存來(lái)為大量數(shù)據(jù)提供服務(wù)。除了常規(guī)的數(shù)據(jù)緩存服務(wù)之外,可編程緩存還具備諸多強(qiáng)大的功能,比如支持對(duì)緩存數(shù)據(jù)的篩選過(guò)濾、統(tǒng)計(jì)計(jì)算、查詢(xún)、分片以及數(shù)據(jù)拼合。

在此,關(guān)于查詢(xún)服務(wù),我要補(bǔ)充說(shuō)明一下。對(duì)于對(duì)外的服務(wù),建議通過(guò)類(lèi)似 Redis 的簡(jiǎn)單文本協(xié)議來(lái)提供服務(wù),因?yàn)檫@樣相較于 HTTP 協(xié)議,其性能會(huì)更為優(yōu)越。

Lua 腳本引擎

雖然緩存提供業(yè)務(wù)服務(wù)能夠提升業(yè)務(wù)的靈活度,然而這種方式也存在諸多缺點(diǎn)。其中最大的缺點(diǎn)便是業(yè)務(wù)修改后,我們需要重啟服務(wù)才能夠更新我們的邏輯。由于內(nèi)存中存儲(chǔ)了大量的數(shù)據(jù),每重啟一次,數(shù)據(jù)就需要經(jīng)歷繁瑣的預(yù)熱過(guò)程,同步代價(jià)極為高昂。

為此,我們需要對(duì)設(shè)計(jì)進(jìn)行再次升級(jí)。在這種情況下,lua 腳本引擎不失為一個(gè)上佳的選擇。lua 是一種小巧的嵌入式腳本語(yǔ)言,借助它能夠?qū)崿F(xiàn)一個(gè)高性能、可熱更新的腳本服務(wù),進(jìn)而與嵌入的服務(wù)進(jìn)行高效靈活的互動(dòng)。

我繪制了一張示意圖,用以描述如何通過(guò) lua 腳本來(lái)具體實(shí)現(xiàn)可編程緩存服務(wù):

圖片圖片

上圖所示,可以看到我們提供了 Kafka 消費(fèi)、周期任務(wù)管理、內(nèi)存緩存、多種數(shù)據(jù)格式支持、多種數(shù)據(jù)驅(qū)動(dòng)適配這些服務(wù)。不僅僅如此,為了減少由于邏輯變更導(dǎo)致的服務(wù)經(jīng)常重啟的情況,我們還以性能損耗為代價(jià),在緩存服務(wù)里嵌入了 lua 腳本引擎,借此實(shí)現(xiàn)動(dòng)態(tài)更新業(yè)務(wù)的邏輯。lua 引擎使用起來(lái)很方便,我們結(jié)合后面這個(gè)實(shí)現(xiàn)例子看一看,這是一個(gè) Go 語(yǔ)言寫(xiě)的嵌入 lua 實(shí)現(xiàn),代碼如下所示:

package main


import "github.com/yuin/gopher-lua"


// VarChange 用于被lua調(diào)用的函數(shù)
func VarChange(L *lua.LState) int {
   lv := L.ToInt(1)            //獲取調(diào)用函數(shù)的第一個(gè)參數(shù),并且轉(zhuǎn)成int
   L.Push(lua.LNumber(lv * 2)) //將參數(shù)內(nèi)容直接x2,并返回結(jié)果給lua
return 1                    //返回結(jié)果參數(shù)個(gè)數(shù)
}


func main() {
   L := lua.NewState() //新lua線(xiàn)程
defer L.Close() //程序執(zhí)行完畢自動(dòng)回收


// 注冊(cè)lua腳本可調(diào)用函數(shù)
// 在lua內(nèi)調(diào)用varChange函數(shù)會(huì)調(diào)用這里注冊(cè)的Go函數(shù) VarChange
   L.SetGlobal("varChange", L.NewFunction(VarChange))


//直接加載lua腳本
//腳本內(nèi)容為:
// print "hello world"
// print(varChange(20)) # lua中調(diào)用go聲明的函數(shù)
if err := L.DoFile("hello.lua"); err != nil {
panic(err)
   }


// 或者直接執(zhí)行string內(nèi)容
if err := L.DoString(`print("hello")`); err != nil {
panic(err)
   }
}


// 執(zhí)行后輸出結(jié)果:
//hello world
//40
//hello

從這個(gè)例子里我們可以看出,lua 引擎是可以直接執(zhí)行 lua 腳本的,而 lua 腳本可以和 Golang 所有注冊(cè)的函數(shù)相互調(diào)用,并且可以相互傳遞交換變量?;叵胍幌拢覀冏龅氖菙?shù)據(jù)緩存服務(wù),所以需要讓 lua 能夠獲取修改服務(wù)內(nèi)的緩存數(shù)據(jù),那么,lua 是如何和嵌入的語(yǔ)言交換數(shù)據(jù)的呢?我們來(lái)看看兩者相互調(diào)用交換的例子:

package main


import (
"fmt"
"github.com/yuin/gopher-lua"
)


func main() {
   L := lua.NewState()
defer L.Close()
//加載腳本
   err := L.DoFile("vardouble.lua")
if err != nil {
panic(err)
   }
// 調(diào)用lua腳本內(nèi)函數(shù)
   err = L.CallByParam(lua.P{
      Fn:      L.GetGlobal("varDouble"), //指定要調(diào)用的函數(shù)名
      NRet:    1,                        // 指定返回值數(shù)量
      Protect: true,                     // 錯(cuò)誤返回error
   }, lua.LNumber(15)) //支持多個(gè)參數(shù)
if err != nil {
panic(err)
   }
//獲取返回結(jié)果
   ret := L.Get(-1)
//清理下,等待下次用
   L.Pop(1)


//結(jié)果轉(zhuǎn)下類(lèi)型,方便輸出
   res, ok := ret.(lua.LNumber)
if !ok {
panic("unexpected result")
   }
   fmt.Println(res.String())
}


// 輸出結(jié)果:
// 30

其中 vardouble.lua 內(nèi)容為:

function varDouble(n)
return n * 2
end

過(guò)這個(gè)方式,lua 和 Golang 就可以相互交換數(shù)據(jù)和相互調(diào)用。對(duì)于這種緩存服務(wù)普遍要求性能很好,這時(shí)我們可以統(tǒng)一管理加載過(guò) lua 的腳本及 LState 腳本對(duì)象的實(shí)例對(duì)象池,這樣會(huì)更加方便,不用每調(diào)用一次 lua 就加載一次腳本,方便獲取和使用多線(xiàn)程、多協(xié)程。

Lua 腳本統(tǒng)一管理

從前面所做的講解當(dāng)中,我們能夠察覺(jué)到,在實(shí)際進(jìn)行使用的時(shí)候,lua 會(huì)有許多實(shí)例在內(nèi)存之中運(yùn)行著。

為了能夠?qū)@些實(shí)例實(shí)現(xiàn)更為妥善的管理,并且進(jìn)一步提升效率,我們最為理想的做法便是運(yùn)用一個(gè)專(zhuān)門(mén)的腳本管理系統(tǒng),來(lái)對(duì)所有 lua 的運(yùn)行實(shí)例展開(kāi)管理。通過(guò)這樣的操作,便可以達(dá)成對(duì)腳本進(jìn)行統(tǒng)一更新、實(shí)現(xiàn)編譯緩存、完成資源調(diào)度以及對(duì)單例加以控制等一系列目標(biāo)。

lua 腳本其自身特性是單線(xiàn)程的,不過(guò)它的體量非常輕,單個(gè)實(shí)例所造成的內(nèi)存損耗大概僅為 144kb 左右。在一些服務(wù)當(dāng)中,平常運(yùn)行的時(shí)候甚至能夠同時(shí)開(kāi)啟成百上千個(gè) lua 實(shí)例。

倘若要提高服務(wù)的并行處理能力,我們可以選擇啟動(dòng)多個(gè)協(xié)程,讓每一個(gè)協(xié)程都能夠獨(dú)立去運(yùn)行一個(gè) lua 線(xiàn)程。

正是出于這樣的需求,gopher-lua 庫(kù)為我們提供了一種類(lèi)似于線(xiàn)程池的實(shí)現(xiàn)方式。借助于這種方式,我們就不再需要頻繁地去創(chuàng)建以及關(guān)閉 lua 了,其官方所給出的具體例子如下:

//保存lua的LState的池子
type lStatePool struct {
    m     sync.Mutex
    saved []*lua.LState
}
// 獲取一個(gè)LState
func (pl *lStatePool) Get() *lua.LState {
    pl.m.Lock()
defer pl.m.Unlock()
    n := len(pl.saved)
if n == 0 {
return pl.New()
    }
    x := pl.saved[n-1]
    pl.saved = pl.saved[0 : n-1]
return x
}


//新建一個(gè)LState
func (pl *lStatePool) New() *lua.LState {
    L := lua.NewState()
// setting the L up here.
// load scripts, set global variables, share channels, etc...
//在這里我們可以做一些初始化
return L
}


//把Lstate對(duì)象放回到池中,方便下次使用
func (pl *lStatePool) Put(L *lua.LState) {
    pl.m.Lock()
defer pl.m.Unlock()
    pl.saved = append(pl.saved, L)
}


//釋放所有句柄
func (pl *lStatePool) Shutdown() {
for _, L := range pl.saved {
        L.Close()
    }
}
// Global LState pool
var luaPool = &lStatePool{
    saved: make([]*lua.LState, 0, 4),
}


//協(xié)程內(nèi)運(yùn)行的任務(wù)
func MyWorker() {
//通過(guò)pool獲取一個(gè)LState
   L := luaPool.Get()
//任務(wù)執(zhí)行完畢后,將LState放回pool
defer luaPool.Put(L)
// 這里可以用LState變量運(yùn)行各種lua腳本任務(wù)
//例如 調(diào)用之前例子中的的varDouble函數(shù)
   err = L.CallByParam(lua.P{
      Fn:      L.GetGlobal("varDouble"), //指定要調(diào)用的函數(shù)名
      NRet:    1,                        // 指定返回值數(shù)量
      Protect: true,                     // 錯(cuò)誤返回error
   }, lua.LNumber(15)) //這里支持多個(gè)參數(shù)
if err != nil {
panic(err) //僅供演示用,實(shí)際生產(chǎn)不推薦用panic
   }
}
func main() {
defer luaPool.Shutdown()
go MyWorker() // 啟動(dòng)一個(gè)協(xié)程
go MyWorker() // 啟動(dòng)另外一個(gè)協(xié)程
/* etc... */
}

通過(guò)這個(gè)方式我們可以預(yù)先創(chuàng)建一批 LState,讓它們加載好所有需要的 lua 腳本,當(dāng)我們執(zhí)行 lua 腳本時(shí)直接調(diào)用它們,即可對(duì)外服務(wù),提高我們的資源復(fù)用率。

變量的交互

實(shí)際上,我們的數(shù)據(jù)既能夠存儲(chǔ)在 lua 當(dāng)中,也可以存放在 Go 里面,然后通過(guò)相互調(diào)用的方式來(lái)獲取對(duì)方所存儲(chǔ)的數(shù)據(jù)。就我個(gè)人而言,更習(xí)慣把數(shù)據(jù)放在 Go 中進(jìn)行封裝處理,之后供 lua 來(lái)調(diào)用。之所以會(huì)這樣選擇,主要是因?yàn)檫@種做法相對(duì)更加規(guī)范,在管理方面也會(huì)比較便捷,畢竟腳本在運(yùn)行過(guò)程中是會(huì)存在一定損耗的。

前面也曾提到過(guò),我們會(huì)采用 struct 和 map 相互組合的方式來(lái)對(duì)一些數(shù)據(jù)進(jìn)行處理,進(jìn)而對(duì)外提供數(shù)據(jù)服務(wù)。那么,lua 和 Golang 之間究竟是如何實(shí)現(xiàn)像 struct 這一類(lèi)數(shù)據(jù)的交換呢?

在這里,我選取了官方所提供的例子,并且還額外添加了大量的注釋內(nèi)容,其目的就是為了能夠更好地幫助大家去理解這兩者之間的數(shù)據(jù)交互過(guò)程。

// go用于交換的 struct
type Person struct {
    Name string
}


//為這個(gè)類(lèi)型定義個(gè)類(lèi)型名稱(chēng)
const luaPersonTypeName = "person"


// 在LState對(duì)象中,聲明這種類(lèi)型,這個(gè)只會(huì)在初始化LState時(shí)執(zhí)行一次
// Registers my person type to given L.
func registerPersonType(L *lua.LState) {
//在LState中聲明這個(gè)類(lèi)型
    mt := L.NewTypeMetatable(luaPersonTypeName)
//指定 person 對(duì)應(yīng) 類(lèi)型type 標(biāo)識(shí)
//這樣 person在lua內(nèi)就像一個(gè) 類(lèi)聲明
    L.SetGlobal("person", mt)
// static attributes
// 在lua中定義person的靜態(tài)方法
// 這句聲明后 lua中調(diào)用person.new即可調(diào)用go的newPerson方法
    L.SetField(mt, "new", L.NewFunction(newPerson))
// person new后創(chuàng)建的實(shí)例,在lua中是table類(lèi)型,你可以把table理解為lua內(nèi)的對(duì)象
// 下面這句主要是給 table定義一組methods方法,可以在lua中調(diào)用
// personMethods是個(gè)map[string]LGFunction 
// 用來(lái)告訴lua,method和go函數(shù)的對(duì)應(yīng)關(guān)系
    L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), personMethods))
}
// person 實(shí)例對(duì)象的所有method
var personMethods = map[string]lua.LGFunction{
"name": personGetSetName,
}
// Constructor
// lua內(nèi)調(diào)用person.new時(shí),會(huì)觸發(fā)這個(gè)go函數(shù)
func newPerson(L *lua.LState) int {
//初始化go struct 對(duì)象 并設(shè)置name為 1
    person := &Person{L.CheckString(1)}
// 創(chuàng)建一個(gè)lua userdata對(duì)象用于傳遞數(shù)據(jù)
// 一般 userdata包裝的都是go的struct,table是lua自己的對(duì)象
    ud := L.NewUserData() 
    ud.Value = person //將 go struct 放入對(duì)象中
// 設(shè)置這個(gè)lua對(duì)象類(lèi)型為 person type
    L.SetMetatable(ud, L.GetTypeMetatable(luaPersonTypeName))
// 將創(chuàng)建對(duì)象返回給lua
    L.Push(ud)
//告訴lua腳本,返回了數(shù)據(jù)個(gè)數(shù)
return 1
}
// Checks whether the first lua argument is a *LUserData 
// with *Person and returns this *Person.
func checkPerson(L *lua.LState) *Person {
//檢測(cè)第一個(gè)參數(shù)是否為其他語(yǔ)言傳遞的userdata
    ud := L.CheckUserData(1)
// 檢測(cè)是否轉(zhuǎn)換成功
if v, ok := ud.Value.(*Person); ok {
return v
    }
    L.ArgError(1, "person expected")
return nil
}
// Getter and setter for the Person#Name
func personGetSetName(L *lua.LState) int {
// 檢測(cè)第一個(gè)棧,如果就只有一個(gè)那么就只有修改值參數(shù)
    p := checkPerson(L)
if L.GetTop() == 2 {
//如果棧里面是兩個(gè),那么第二個(gè)是修改值參數(shù)
        p.Name = L.CheckString(2)
//代表什么數(shù)據(jù)不返回,只是修改數(shù)據(jù)
return 0
    }
//如果只有一個(gè)在棧,那么是獲取name值操作,返回結(jié)果
    L.Push(lua.LString(p.Name))


//告訴會(huì)返回一個(gè)參數(shù)
return 1
}
func main() {
// 創(chuàng)建一個(gè)lua LState
    L := lua.NewState()
defer L.Close()


//初始化 注冊(cè)
    registerPersonType(L)
// 執(zhí)行l(wèi)ua腳本
if err := L.DoString(`
        //創(chuàng)建person,并設(shè)置他的名字
        p = person.new("Steven")
        print(p:name()) -- "Steven"
        //修改他的名字
        p:name("Nico")
        print(p:name()) -- "Nico"
    `); err != nil {
panic(err)
    }
}

可以看到,我們通過(guò) lua 腳本引擎就能很方便地完成相互調(diào)用和交換數(shù)據(jù),從而實(shí)現(xiàn)很多實(shí)用的功能,甚至可以用少量數(shù)據(jù)直接寫(xiě)成 lua 腳本的方式來(lái)加載服務(wù)。

緩存預(yù)熱與數(shù)據(jù)來(lái)源

在對(duì) lua 有了一定了解之后,接下來(lái)我們一起探討一下服務(wù)是如何加載數(shù)據(jù)的。

當(dāng)服務(wù)啟動(dòng)之時(shí),我們首先要做的就是將數(shù)據(jù)緩存加載到緩存當(dāng)中,以此來(lái)完成緩存預(yù)熱的操作。只有在數(shù)據(jù)全部加載完成之后,才會(huì)開(kāi)放對(duì)外的 API 端口,從而正式對(duì)外提供服務(wù)。

在這個(gè)加載數(shù)據(jù)的過(guò)程中,如果引入了 lua 腳本的話(huà),那么就能夠在服務(wù)啟動(dòng)之際,針對(duì)不同格式的數(shù)據(jù)開(kāi)展適配加工的工作。如此一來(lái),數(shù)據(jù)的來(lái)源也會(huì)變得更加豐富多樣。

通常情況下,常見(jiàn)的數(shù)據(jù)來(lái)源是由大數(shù)據(jù)挖掘周期所生成的全量數(shù)據(jù)離線(xiàn)文件。這些文件會(huì)通過(guò) NFS 或者 HDFS 進(jìn)行掛載操作,并且會(huì)定期進(jìn)行刷新,以便加載最新的文件。這種通過(guò)掛載離線(xiàn)文件來(lái)獲取數(shù)據(jù)的方式,比較適合那些數(shù)據(jù)量龐大且更新速度較為緩慢的數(shù)據(jù)。不過(guò),它也存在一定的缺點(diǎn),那就是在加載數(shù)據(jù)的時(shí)候需要對(duì)數(shù)據(jù)進(jìn)行整理工作。要是情況較為復(fù)雜的話(huà),比如對(duì)于 800M 大小的數(shù)據(jù),可能就需要花費(fèi) 1 至 10 分鐘的時(shí)間才能夠完成加載操作。

除了采用上述這種利用文件獲取數(shù)據(jù)的方式之外,我們還可以在程序啟動(dòng)之后,通過(guò)掃描數(shù)據(jù)表的方式來(lái)恢復(fù)數(shù)據(jù)。但是這樣做的話(huà),數(shù)據(jù)庫(kù)將會(huì)承受一定的壓力,所以建議使用專(zhuān)門(mén)的從庫(kù)來(lái)進(jìn)行此項(xiàng)操作。而且需要注意的是,相較于通過(guò)磁盤(pán)離線(xiàn)文件獲取數(shù)據(jù)的方式,這種掃描數(shù)據(jù)表的方式其加載速度會(huì)更慢一些。

前面所提及的那兩種數(shù)據(jù)加載方式,在速度方面都存在一定的不足。接下來(lái),我們還可以考慮另外一種做法,那就是將 RocksDB 嵌入到進(jìn)程之中。通過(guò)這樣的操作,能夠極大幅度地提升我們的數(shù)據(jù)存儲(chǔ)容量,進(jìn)而實(shí)現(xiàn)內(nèi)存與磁盤(pán)之間高性能的讀取和寫(xiě)入操作。不過(guò),這么做也是有代價(jià)的,那就是相對(duì)而言會(huì)使得查詢(xún)性能出現(xiàn)一定程度的降低。

關(guān)于 RocksDB 的數(shù)據(jù)獲取,我們可以借助大數(shù)據(jù)來(lái)生成符合 RocksDB 格式的數(shù)據(jù)庫(kù)文件,然后將其拷貝給我們的服務(wù),以便服務(wù)能夠直接進(jìn)行加載。采用這種方式的好處在于,它可以大幅減少系統(tǒng)在啟動(dòng)過(guò)程中整理以及加載數(shù)據(jù)所耗費(fèi)的時(shí)間,從而能夠?qū)崿F(xiàn)更多的數(shù)據(jù)查詢(xún)操作。

另外,倘若我們存在對(duì)于本地有關(guān)系數(shù)據(jù)進(jìn)行查詢(xún)的需求,那么還可以選擇嵌入 SQLite 引擎。通過(guò)這個(gè)引擎,我們就能夠開(kāi)展各種各樣的關(guān)系數(shù)據(jù)查詢(xún)工作。對(duì)于 SQLite 的數(shù)據(jù)生成,同樣也可以利用相關(guān)工具提前完成,生成之后便可以直接提供給我們的服務(wù)使用。但在這里需要特別注意的是,這個(gè)數(shù)據(jù)庫(kù)的數(shù)據(jù)量最好不要超過(guò) 10 萬(wàn)條,不然的話(huà),很有可能會(huì)導(dǎo)致服務(wù)出現(xiàn)卡頓的現(xiàn)象。

最后,在涉及離線(xiàn)文件加載的情況時(shí),我們最好能夠制作一個(gè)類(lèi)似于 CheckSum 的文件。制作這個(gè)文件的目的在于,在加載文件之前,可以利用它來(lái)檢查文件的完整性。因?yàn)槲覀兯褂玫氖蔷W(wǎng)絡(luò)磁盤(pán),所以不太能確定這個(gè)文件是否正在處于拷貝的過(guò)程當(dāng)中,這就需要運(yùn)用一些小技巧來(lái)確保我們的數(shù)據(jù)完整性。其中,最為簡(jiǎn)單粗暴的方式就是,在每次拷貝完畢之后,生成一個(gè)與原文件同名的文件,并且在這個(gè)新生成的文件內(nèi)部記錄一下它的 CheckSum 值,這樣一來(lái),便方便我們?cè)诩虞d文件之前進(jìn)行校驗(yàn)操作。

離線(xiàn)文件在實(shí)際應(yīng)用當(dāng)中具有一定的優(yōu)勢(shì),它能夠幫助我們快速實(shí)現(xiàn)多個(gè)節(jié)點(diǎn)之間的數(shù)據(jù)共享以及數(shù)據(jù)的統(tǒng)一。倘若我們要求多個(gè)節(jié)點(diǎn)的數(shù)據(jù)要保持最終的一致性,那么就需要通過(guò)離線(xiàn)與同步訂閱相結(jié)合的方式來(lái)實(shí)現(xiàn)數(shù)據(jù)的同步操作。

訂閱式數(shù)據(jù)同步及啟動(dòng)同步

那么,咱們的數(shù)據(jù)究竟是怎樣實(shí)現(xiàn)同步更新的呢?通常而言,我們的數(shù)據(jù)是從多個(gè)基礎(chǔ)數(shù)據(jù)服務(wù)獲取而來(lái)的。要是希望能夠?qū)崟r(shí)同步數(shù)據(jù)所發(fā)生的更改情況,一般會(huì)采用訂閱 binlog 的方式,先將變更的相關(guān)信息同步到 Kafka 當(dāng)中。接著,再憑借 Kafka 的分組消費(fèi)功能,去通知那些分布在不同集群里面的緩存。

當(dāng)收到消息變更的服務(wù)接收到相關(guān)通知后,就會(huì)觸發(fā) lua 腳本,進(jìn)而依據(jù)腳本對(duì)數(shù)據(jù)展開(kāi)同步更新的操作。通過(guò) lua 腳本的運(yùn)作,我們能夠以觸發(fā)式的方式同步更新其他與之相關(guān)的緩存。

比如說(shuō),當(dāng)用戶(hù)購(gòu)買(mǎi)了一個(gè)商品的時(shí)候,我們就需要同步刷新與之相關(guān)的一些數(shù)據(jù),像是他的積分情況、訂單信息以及消息列表的個(gè)數(shù)等等,以此來(lái)確保各項(xiàng)數(shù)據(jù)的一致性和實(shí)時(shí)性。

周期任務(wù)

在談及任務(wù)管理這個(gè)話(huà)題時(shí),就不得不著重說(shuō)一說(shuō)周期任務(wù)了。周期任務(wù)在通常情況下,主要是被用于對(duì)數(shù)據(jù)統(tǒng)計(jì)進(jìn)行刷新的工作。我們只要將周期任務(wù)和 lua 自定義邏輯腳本相互結(jié)合起來(lái)加以運(yùn)用,便能夠輕松實(shí)現(xiàn)定期開(kāi)展統(tǒng)計(jì)的操作,這無(wú)疑是給我們的工作帶來(lái)了更多的便利條件。

在執(zhí)行定期任務(wù)或者進(jìn)行延遲刷新的這個(gè)過(guò)程當(dāng)中,較為常見(jiàn)的一種處理方式就是運(yùn)用時(shí)間輪來(lái)對(duì)任務(wù)加以管理。通過(guò)采用這種方式,能夠把定時(shí)任務(wù)轉(zhuǎn)化為事件觸發(fā)的形式,如此一來(lái),就可以非常便捷地對(duì)內(nèi)存當(dāng)中那些有待觸發(fā)的任務(wù)列表進(jìn)行管理了,進(jìn)而能夠?qū)崿F(xiàn)并行處理多個(gè)周期任務(wù),也就無(wú)需再通過(guò)使用 sleep 循環(huán)這種方式去不斷地進(jìn)行查詢(xún)操作了。要是您對(duì)時(shí)間輪的具體實(shí)現(xiàn)方式感興趣的話(huà),可以直接點(diǎn)擊此處進(jìn)行查看哦。

另外,前面也曾提到過(guò),我們所用到的很多數(shù)據(jù)都是借助離線(xiàn)文件來(lái)進(jìn)行批量更新的。假如是每一小時(shí)就更新一次數(shù)據(jù)的話(huà),那么在這一小時(shí)之內(nèi)新更新的數(shù)據(jù)就需要進(jìn)行同步處理。一般而言,處理的方式是這樣的:當(dāng)我們的服務(wù)啟動(dòng)并加載離線(xiàn)文件的時(shí)候,要把這個(gè)離線(xiàn)文件生成的時(shí)間給保存下來(lái),然后憑借這個(gè)保存下來(lái)的時(shí)間來(lái)對(duì)數(shù)據(jù)更新隊(duì)列當(dāng)中的消息進(jìn)行過(guò)濾操作。等到我們的隊(duì)列任務(wù)進(jìn)度追趕至接近當(dāng)前時(shí)間的時(shí)候,再開(kāi)啟對(duì)外提供數(shù)據(jù)的服務(wù)。

總結(jié)

在那些讀多寫(xiě)多的服務(wù)當(dāng)中,實(shí)時(shí)交互類(lèi)的服務(wù)數(shù)量相當(dāng)多,并且這類(lèi)服務(wù)對(duì)于數(shù)據(jù)的實(shí)時(shí)性有著很高的要求。若是采用集中型緩存的方式,往往很難滿(mǎn)足此類(lèi)服務(wù)所提出的各項(xiàng)需求。

基于這樣的情況,在行業(yè)當(dāng)中,多數(shù)會(huì)選擇通過(guò)服務(wù)自身內(nèi)存中的數(shù)據(jù)來(lái)為實(shí)時(shí)交互服務(wù)提供支持。然而,這種做法存在一個(gè)明顯的弊端,那就是維護(hù)起來(lái)特別麻煩,一旦服務(wù)重啟,就必須要對(duì)數(shù)據(jù)進(jìn)行恢復(fù)操作。

為了能夠?qū)崿F(xiàn)業(yè)務(wù)邏輯在無(wú)需重啟的情況下就可以完成更新,在行業(yè)里通常會(huì)采用內(nèi)嵌腳本的熱更新方案。在眾多的腳本引擎當(dāng)中,較為常見(jiàn)且通用的就是 lua 腳本引擎了。lua 是一種非常流行且使用起來(lái)極為方便的腳本引擎,在行業(yè)領(lǐng)域中,許多知名的游戲以及各類(lèi)服務(wù)都借助 lua 來(lái)實(shí)現(xiàn)高性能服務(wù)的定制化業(yè)務(wù)功能,像 Nginx、Redis 等就是如此。

當(dāng)我們把 lua 和我們所定制的緩存服務(wù)相互結(jié)合起來(lái)的時(shí)候,便能夠打造出很多強(qiáng)大的功能,以此來(lái)應(yīng)對(duì)各種各樣不同的場(chǎng)景需求。

由于 lua 具備十分節(jié)省內(nèi)存的特性,所以我們可以在進(jìn)程當(dāng)中開(kāi)啟數(shù)量多達(dá)成千上萬(wàn)的 lua 小線(xiàn)程。甚至可以做到為每一個(gè)用戶(hù)都配備一個(gè) LState 線(xiàn)程,從而為客戶(hù)端提供類(lèi)似于狀態(tài)機(jī)一樣的服務(wù)。

通過(guò)運(yùn)用上述所提到的這些方法,再將 lua 和靜態(tài)語(yǔ)言之間進(jìn)行數(shù)據(jù)交換以及相互調(diào)用,并且配合上我們所實(shí)施的任務(wù)管理以及各種各樣的數(shù)據(jù)驅(qū)動(dòng)方式,這樣就能夠構(gòu)建出一個(gè)幾乎可以應(yīng)對(duì)所有情況的萬(wàn)能緩存服務(wù)了。

在此,推薦大家在一些小型項(xiàng)目當(dāng)中親自去實(shí)踐一下上述的這些內(nèi)容。相信通過(guò)這樣的實(shí)踐,會(huì)讓大家從一個(gè)與以往不同的視角去重新審視那些已經(jīng)習(xí)慣了的服務(wù),如此一來(lái),大家必定會(huì)從中獲得更多的收獲。

責(zé)任編輯:武曉燕 來(lái)源: 二進(jìn)制跳動(dòng)
相關(guān)推薦

2015-11-04 13:56:06

SDN可編程性企業(yè)

2019-11-22 09:20:34

編程經(jīng)濟(jì)技術(shù)

2014-03-26 10:49:06

SDN軟件定義網(wǎng)絡(luò)網(wǎng)絡(luò)可編程性

2009-06-19 18:51:13

ibmdwLotus

2013-08-06 14:04:46

網(wǎng)絡(luò)

2013-08-07 09:00:57

軟件定義網(wǎng)絡(luò)SDN

2023-04-04 15:46:16

云計(jì)算

2013-11-26 10:14:15

面向?qū)ο?/a>函數(shù)式

2021-05-31 20:06:57

網(wǎng)元協(xié)議網(wǎng)關(guān)

2018-02-01 04:02:41

數(shù)據(jù)中心網(wǎng)絡(luò)編程

2013-05-03 09:49:38

ASICSDN可編程ASIC

2012-06-14 10:17:16

TecTile三星

2016-09-09 13:26:07

數(shù)據(jù)編程SDN

2014-05-09 10:03:50

編程面試

2010-10-11 09:14:17

Atom

2013-10-25 09:11:28

可編程WANSDN軟件定義網(wǎng)絡(luò)

2013-03-25 14:02:09

JuniperSDN核心交換機(jī)

2015-09-14 09:01:13

Android鏡子應(yīng)用

2025-04-07 08:30:00

緩存Java開(kāi)發(fā)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)