1. 程式人生 > 實用技巧 >kafka在高併發的情況下,如何避免訊息丟失和訊息重複?

kafka在高併發的情況下,如何避免訊息丟失和訊息重複?

xmake是一個基於Lua的輕量級現代化c/c++的專案構建工具,主要特點是:語法簡單易上手,提供更加可讀的專案維護,實現跨平臺行為一致的構建體驗。

本文主要詳細講解下,如何通過新增自定義的指令碼,在指令碼域實現更加複雜靈活的定製。

配置分離

xmake.lua採用二八原則實現了描述域、指令碼域兩層分離式配置。

什麼是二八原則呢,簡單來說,大部分專案的配置,80%的情況下,都是些基礎的常規配置,比如:add_cxflags, add_links等,
只有剩下不到20%的地方才需要額外做些複雜來滿足一些特殊的配置需求。

而這剩餘的20%的配置通常比較複雜,如果直接充斥在整個xmake.lua裡面,會把整個專案的配置整個很混亂,非常不可讀。

因此,xmake通過描述域、指令碼域兩種不同的配置方式,來隔離80%的簡單配置以及20%的複雜配置,使得整個xmake.lua看起來非常的清晰直觀,可讀性和可維護性都達到最佳。

描述域

對於剛入門的新手使用者,或者僅僅是維護一些簡單的小專案,通過完全在描述配置就已經完全滿足需求了,那什麼是描述域呢?它長這樣:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    add_defines("DEBUG")
    add_syslinks("pthread")

一眼望去,其實就是個 set_xxx/add_xxx的配置集,對於新手,完全可以不把它當做lua指令碼,僅僅作為普通的,但有一些基礎規則的配置檔案就行了。

這是不是看著更像配置檔案了?其實描述域就是配置檔案,類似像json等key/values的配置而已,所以即使完全不會lua的新手,也是能很快上手的。

而且,對於通常的專案,僅通過set_xxx/add_xxx去配置各種專案設定,已經完全滿足需求了。

這也就是開頭說的:80%的情況下,可以用最簡單的配置規則去簡化專案的配置,提高可讀性和可維護性,這樣對使用者和開發者都會非常的友好,也更加直觀。

如果我們要針對不同平臺,架構做一些條件判斷怎麼辦?沒關係,描述域除了基礎配置,也是支援條件判斷,以及for迴圈的:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    add_defines("DEBUG")
    if is_plat("linux", "macosx") then
        add_links("pthread", "m", "dl")
    end
target("test")
    set_kind("binary")
    add_files("src/*.c")
    add_defines("DEBUG")
    for _, name in ipairs({"pthread", "m", "dl"}) do
        add_links(name)
    end

這是不是看著有點像lua了?雖說,平常可以把它當做普通配置問題,但是xmake畢竟基於lua,所以描述域還是支援lua的基礎語言特性的。

!> 不過需要注意的是,描述域雖然支援lua的指令碼語法,但在描述域儘量不要寫太複雜的lua指令碼,比如一些耗時的函式呼叫和for迴圈

並且在描述域,主要目的是為了設定配置項,因此xmake並沒有完全開放所有的模組介面,很多介面在描述域是被禁止呼叫的,
即使開放出來的一些可呼叫介面,也是完全只讀的,不耗時的安全介面,比如:os.getenv()等讀取一些常規的系統資訊,用於配置邏輯的控制。

!> 另外需要注意一點,xmake.lua是會被多次解析的,用於在不同階段解析不同的配置域:比如:option(), target()等域。

因此,不要想著在xmake.lua的描述域,寫複雜的lua指令碼,也不要在描述域呼叫print去顯示資訊,因為會被執行多遍,記住:會被執行多遍!!!

指令碼域

限制描述域寫複雜的lua,各種lua模組和介面都用不了?怎麼辦?這個時候就是指令碼域出場的時候了。

如果使用者已經完全熟悉了xmake的描述域配置,並且感覺有些滿足不了專案上的一些特殊配置維護了,那麼我們可以在指令碼域做更加複雜的配置邏輯:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    on_load(function (target)
        if is_plat("linux", "macosx") then
            target:add("links", "pthread", "m", "dl")
        end
    end)
    after_build(function (target)
        import("core.project.config")
        local targetfile = target:targetfile()
        os.cp(targetfile, path.join(config.buildir(), path.filename(targetfile)))
        print("build %s", targetfile)
    end)

只要是類似:on_xxx, after_xxx, before_xxx等字樣的function body內部的指令碼,都屬於指令碼域。

在指令碼域中,使用者可以幹任何事,xmake提供了import介面可以匯入xmake內建的各種lua模組,也可以匯入使用者提供的lua指令碼。

我們可以在指令碼域實現你想實現的任意功能,甚至寫個獨立專案出來都是可以的。

對於一些指令碼片段,不是很臃腫的話,像上面這麼內建寫寫就足夠了,如果需要實現更加複雜的指令碼,不想充斥在一個xmake.lua裡面,可以把指令碼分離到獨立的lua檔案中去維護。

例如:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    on_load("modules.test.load")
    on_install("modules.test.install")

我們可以吧自定義的指令碼放置到xmake.lua對應目錄下,modules/test/load.luamodules/test/install.lua中獨立維護。

單獨的lua指令碼檔案以main作為主入口,例如:

-- 我們也可以在此處匯入一些內建模組或者自己的擴充套件模組來使用
import("core.project.config")
import("mymodule")

function main(target)
    if is_plat("linux", "macosx") then
        target:add("links", "pthread", "m", "dl")
    end
end

這些獨立的lua腳本里面,我們還可以通過import匯入各種內建模組和自定義模組進來使用,就跟平常寫lua, java沒啥區別。

而對於指令碼的域的不同階段,on_load主要用於target載入時候,做一些動態化的配置,這裡不像描述域,只會執行一遍哦!!!

其他階段,還有很多,比如:on/after/before_build/install/package/run等,我們下面會詳細描述。

import

匯入擴充套件摸塊

在講解各個指令碼域之前,我們先來簡單介紹下xmake的模組匯入和使用方式,xmake採用import來引入其他的擴充套件模組,以及使用者自己定義的模組,它可以在下面一些地方使用:

  • 自定義指令碼(on_build, on_run ..)
  • 外掛開發
  • 模板開發
  • 平臺擴充套件
  • 自定義任務task

匯入機制如下:

  1. 優先從當前指令碼目錄下匯入
  2. 再從擴充套件類庫中匯入

匯入的語法規則:

基於.的類庫路徑規則,例如:

import("core.base.option")
import("core.base.task")

function main()
    
    -- 獲取引數選項
    print(option.get("version"))

    -- 執行任務和外掛
    task.run("hello")
end

匯入當前目錄下的自定義模組:

目錄結構:

plugin
  - xmake.lua
  - main.lua
  - modules
    - hello1.lua
    - hello2.lua

在main.lua中匯入modules

import("modules.hello1")
import("modules.hello2")

匯入後就可以直接使用裡面的所有公有介面,私有介面用_字首標示,表明不會被匯出,不會被外部呼叫到。。

除了當前目錄,我們還可以匯入其他指定目錄裡面的類庫,例如:

import("hello3", {rootdir = "/home/xxx/modules"})

為了防止命名衝突,匯入後還可以指定的別名:

import("core.platform.platform", {alias = "p"})

function main()
    -- 這樣我們就可以使用p來呼叫platform模組的plats介面,獲取所有xmake支援的平臺列表了
    print(p.plats())
end

2.1.5版本新增兩個新屬性:import("xxx.xxx", {try = true, anonymous = true})

try為true,則匯入的模組不存在的話,僅僅返回nil,並不會拋異常後中斷xmake.
anonymous為true,則匯入的模組不會引入當前作用域,僅僅在import介面返回匯入的物件引用。

測試擴充套件模組

一種方式我們可以在on_load等指令碼中,直接呼叫print去列印模組的呼叫結果資訊,來測試和驗證。

不過xmake還提供了xmake lua外掛可以更加靈活方便的測試指令碼。

執行指定的指令碼檔案

比如,我們可以直接指定lua指令碼來載入執行,這對於想要快速測試一些介面模組,驗證自己的某些思路,都是一個不錯的方式。

我們先寫個簡單的lua指令碼:

function main()
    print("hello xmake!")
end

然後直接執行它就行了:

$ xmake lua /tmp/test.lua

直接呼叫擴充套件模組

所有內建模組和擴充套件模組的介面,我們都可以通過xmake lua直接呼叫,例如:

$ xmake lua lib.detect.find_tool gcc

上面的命令,我們直接呼叫了import("lib.detect.find_tool")模組介面來快速執行。

執行互動命令 (REPL)

有時候在互動模式下,執行命令更加的方便測試和驗證一些模組和api,也更加的靈活,不需要再去額外寫一個指令碼檔案來載入。

我們先看下,如何進入互動模式:

# 不帶任何引數執行,就可以進入
$ xmake lua
>

# 進行表示式計算
> 1 + 2
3

# 賦值和列印變數值
> a = 1
> a
1

# 多行輸入和執行
> for _, v in pairs({1, 2, 3}) do
>> print(v)
>> end
1
2
3

我們也能夠通過 import 來匯入擴充套件模組:

> task = import("core.project.task")
> task.run("hello")
hello xmake!

如果要中途取消多行輸入,只需要輸入字元:q 就行了

> for _, v in ipairs({1, 2}) do
>> print(v)
>> q             <--  取消多行輸入,清空先前的輸入資料
> 1 + 2
3

target:on_load

自定義目標載入指令碼

在target初始化載入的時候,將會執行此指令碼,在裡面可以做一些動態的目標配置,實現更靈活的目標描述定義,例如:

target("test")
    on_load(function (target)
        target:add("defines", "DEBUG", "TEST=\"hello\"")
        target:add("linkdirs", "/usr/lib", "/usr/local/lib")
        target:add({includedirs = "/usr/include", "links" = "pthread"})
    end)

可以在on_load裡面,通過target:set, target:add 來動態新增各種target屬性,所有描述域的set_, add_配置都可以通過這種方式動態配置。

另外,我們可以呼叫target的一些介面,獲取和設定一些基礎資訊,比如:

target介面 描述
target:name() 獲取目標名
target:targetfile() 獲取目標檔案路徑
target:targetkind() 獲取目標的構建型別
target:get("defines") 獲取目標的巨集定義
target:get("xxx") 其他通過 set_/add_介面設定的target資訊,都可以通過此介面來獲取
target:add("links", "pthread") 新增目標設定
target:set("links", "pthread", "z") 覆寫目標設定
target:deps() 獲取目標的所有依賴目標
target:dep("depname") 獲取指定的依賴目標
target:opts() 獲取目標的所有關聯選項
target:opt("optname") 獲取指定的關聯選項
target:pkgs() 獲取目標的所有關聯依賴包
target:pkg("pkgname") 獲取指定的關聯依賴包
target:sourcebatches() 獲取目標的所有原始檔列表

自定義連結指令碼

這個是在v2.2.7之後新加的介面,用於定製化處理target的連結過程。

target("test")
    on_link(function (target) 
        print("link it")
    end)

target:on_build

自定義編譯指令碼

覆蓋target目標預設的構建行為,實現自定義的編譯過程,一般情況下,並不需要這麼做,除非確實需要做一些xmake預設沒有提供的編譯操作。

你可以通過下面的方式覆蓋它,來自定義編譯操作:

target("test")

    -- 設定自定義編譯指令碼
    on_build(function (target) 
        print("build it")
    end)

注:2.1.5版本之後,所有target的自定義指令碼都可以針對不同平臺和架構,分別處理,例如:

target("test")
    on_build("iphoneos|arm*", function (target)
        print("build for iphoneos and arm")
    end)

其中如果第一個引數為字串,那麼就是指定這個指令碼需要在哪個平臺|架構下,才會被執行,並且支援模式匹配,例如arm*匹配所有arm架構。

當然也可以只設置平臺,不設定架構,這樣就是匹配指定平臺下,執行指令碼:

target("test")
    on_build("windows", function (target)
        print("build for windows")
    end)

注:一旦對這個target目標設定了自己的build過程,那麼xmake預設的構建過程將不再被執行。

target:on_build_file

自定義編譯指令碼, 實現單檔案構建

通過此介面,可以用來hook指定target內建的構建過程,自己重新實現每個原始檔編譯過程:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    on_build_file(function (target, sourcefile, opt)
    end)

target:on_build_files

自定義編譯指令碼, 實現多檔案構建

通過此介面,可以用來hook指定target內建的構建過程,替換一批同類型原始檔編譯過程:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    on_build_files(function (target, sourcebatch, opt)
    end)

設定此介面後,對應原始檔列表中檔案,就不會出現在自定義的target.on_build_file了,因為這個是包含關係。

其中sourcebatch描述了這批同類型原始檔:

  • sourcebatch.sourcekind: 獲取這批原始檔的型別,比如:cc, as, ..
  • sourcebatch.sourcefiles(): 獲取原始檔列表
  • sourcebatch.objectfiles(): 獲取物件檔案列表
  • sourcebatch.dependfiles(): 獲取對應依賴檔案列表,存有原始檔中編譯依賴資訊,例如:xxx.d

target:on_clean

自定義清理指令碼

覆蓋target目標的xmake [c|clean}的清理操作,實現自定義清理過程。

target("test")

    -- 設定自定義清理指令碼
    on_clean(function (target) 

        -- 僅刪掉目標檔案
        os.rm(target:targetfile())
    end)

target:on_package

自定義打包指令碼

覆蓋target目標的xmake [p|package}的打包操作,實現自定義打包過程,如果你想對指定target打包成自己想要的格式,可以通過這個介面自定義它。

target("demo")
    set_kind("shared")
    add_files("jni/*.c")
    on_package(function (target) 
        os.exec("./gradlew app:assembleDebug") 
    end)

當然這個例子有點老了,這裡只是舉例說明下用法而已,現在xmake提供了專門的xmake-gradle外掛,來與gradle更好的整合。

target:on_install

自定義安裝指令碼

覆蓋target目標的xmake [i|install}的安裝操作,實現自定義安裝過程。

例如,將生成的apk包,進行安裝。

target("test")

    -- 設定自定義安裝指令碼,自動安裝apk檔案
    on_install(function (target) 

        -- 使用adb安裝打包生成的apk檔案
        os.run("adb install -r ./bin/Demo-debug.apk")
    end)

target:on_uninstall

自定義解除安裝指令碼

覆蓋target目標的xmake [u|uninstall}的解除安裝操作,實現自定義解除安裝過程。

target("test")
    on_uninstall(function (target) 
        ...
    end)

target:on_run

自定義執行指令碼

覆蓋target目標的xmake [r|run}的執行操作,實現自定義執行過程。

例如,執行安裝好的apk程式:

target("test")

    -- 設定自定義執行指令碼,自動執行安裝好的app程式,並且自動獲取裝置輸出資訊
    on_run(function (target) 
        os.run("adb shell am start -n com.demo/com.demo.DemoTest")
        os.run("adb logcat")
    end)

before_xxx和after_xxx

需要注意的是,target:on_xxx的所有介面都覆蓋內部預設實現,通常我們並不需要完全複寫,只是額外掛接自己的一些邏輯,那麼可以使用target:before_xxxtarget:after_xxx系列指令碼就行了。

所有的on_xxx都有對應的before_和after_xx版本,引數也完全一致,例如:

target("test")
    before_build(function (target)
        print("")
    end)

內建模組

在自定義指令碼中,除了使用import介面匯入各種擴充套件模組使用,xmake還提供了很多基礎的內建模組,比如:os,io等基礎操作,實現更加跨平臺的處理系統介面。

os.cp

os.cp的行為和shell中的cp命令類似,不過更加強大,不僅支援模式匹配(使用的是lua模式匹配),而且還確保目的路徑遞迴目錄建立、以及支援xmake的內建變數。

例如:

os.cp("$(scriptdir)/*.h", "$(buildir)/inc")
os.cp("$(projectdir)/src/test/**.h", "$(buildir)/inc")

上面的程式碼將:當前xmake.lua目錄下的所有標頭檔案、工程原始碼test目錄下的標頭檔案全部複製到$(buildir)輸出目錄中。

其中$(scriptdir), $(projectdir) 這些變數是xmake的內建變數,具體詳情見:內建變數的相關文件。

*.h**.h中的匹配模式,跟add_files中的類似,前者是單級目錄匹配,後者是遞迴多級目錄匹配。

上面的複製,會把所有檔案全部展開復制到指定目錄,丟失源目錄層級,如果要按保持原有的目錄結構複製,可以設定rootdir引數:

os.cp("src/**.h", "/tmp/", {rootdir = "src"})

上面的指令碼可以按src根目錄,將src下的所有子檔案保持目錄結構複製過去。

注:儘量使用os.cp介面,而不是os.run("cp .."),這樣更能保證平臺一致性,實現跨平臺構建描述。

os.run

此介面會安靜執行原生shell命令,用於執行第三方的shell命令,但不會回顯輸出,僅僅在出錯後,高亮輸出錯誤資訊。

此介面支援引數格式化、內建變數,例如:

-- 格式化引數傳入
os.run("echo hello %s!", "xmake")

-- 列舉構建目錄檔案
os.run("ls -l $(buildir)")

os.execv

此介面相比os.run,在執行過程中還會回顯輸出,並且引數是通過列表方式傳入,更加的靈活。

os.execv("echo", {"hello", "xmake!"})

另外,此介面還支援一個可選的引數,用於傳遞設定:重定向輸出,執行環境變數設定,例如:

os.execv("echo", {"hello", "xmake!"}, {stdout = outfile, stderr = errfile, envs = {PATH = "xxx;xx", CFLAGS = "xx", curdir = "/tmp"}}

其中,stdout和stderr引數用於傳遞重定向輸出和錯誤輸出,可以直接傳入檔案路徑,也可以傳入io.open開啟的檔案物件。

另外,如果想在這次執行中臨時設定和改寫一些環境變數,可以傳遞envs引數,裡面的環境變數設定會替換已有的設定,但是不影響外層的執行環境,隻影響當前命令。

我們也可以通過os.getenvs()介面獲取當前所有的環境變數,然後改寫部分後傳入envs引數。

另外,還能通過curdir引數設定,在執行過程中修改子程序的工作目錄。

其相關類似介面還有,os.runv, os.exec, os.execv, os.iorun, os.iorunv等等,比如os.iorun可以獲取執行的輸出內容。

這塊的具體詳情和差異,還有更多os介面,都可以到:os介面文件 檢視。

io.readfile

此介面,從指定路徑檔案讀取所有內容,我們可在不開啟檔案的情況下,直接讀取整個檔案的內容,更加的方便,例如:

local data = io.readfile("xxx.txt")

io.writefile

此介面寫入所有內容到指定路徑檔案,我們可在不開啟檔案的情況下,直接寫入整個檔案的內容,更加的方便,例如:

io.writefile("xxx.txt", "all data")

path.join

此介面實現跨平臺地路徑拼接操作,將多個路徑項進行追加拼接,由於windows/unix風格的路徑差異,使用api來追加路徑更加跨平臺,例如:

print(path.join("$(tmpdir)", "dir1", "dir2", "file.txt"))

上述拼接在unix上相當於:$(tmpdir)/dir1/dir2/file.txt,而在windows上相當於:$(tmpdir)\\dir1\\dir2\\file.txt

更多內建模組詳情見:內建模組文件