在游戲開發中,需要用到大量且更新不頻繁的配置數據,而把業務拆分到多個服務后,各個服務可能只用到其中的少部分數據,此時每個服務加載所有數據會浪費大量內存。sharedata模塊就是為了解決這種需求設計的,其原理是:將共享lua數據存放到一個c結構里,所有服務都共享這個c結構的內存塊,各個服務可以獲取這個共享內存對象,然后就可以像讀取普通lua表一樣讀取數據。
1. 創建共享數據
調用sharedata.new(name, value) api創建共享數據對象,主要2個參數:name,名字;v,可以是一張lua table,也可以是lua文本代碼,也可以是lua文件,最終都會轉化成一個table,然后調用c層接口(第15行)返回一個c結構共享內存,共享內存用引用計數管理其生命周期。
1 -- lualib/skynet/sharedata.lua 2 function sharedata.new(name, v, ...) 3 skynet.call(service, "lua", "new", name, v, ...) 4 end 5 6 function CMD.new(name, t, ...) 7 local dt = type(t) 8 local value 9 ... 10 newobj(name, value) 11 end 12 13 local function newobj(name, tbl) 14 assert(pool[name] == nil) 15 local cobj = sharedata.host.new(tbl) 16 sharedata.host.incref(cobj) 17 local v = { value = tbl , obj = cobj, watch = {} } 18 objmap[cobj] = v 19 pool[name] = v 20 pool_count[name] = { n = 0, threshold = 16 } 21 end
主要數據結構如下:struct table,是返回給lua層的c結構。
// lualib-src/lua-sharedata.c struct table { //單個共享對象結構,供lua層操作 int sizearray; //一維數據長度 int sizehash; //hash數據長度 uint8_t *arraytype; //一維數據類型,integer、boolean、string or table union value * array; //一維數據值 struct node * hash; //hash數據值,是一個數組,每個元素是一個hash數據信息 lua_State * L; //lua虛擬棧,可獲取共享的lua table數據 }; struct node { //table里一個key-value值對應的數據信息 union value v; int key; // integer key or index of string table int next; // next slot index uint32_t keyhash; uint8_t keytype; // key type must be integer or string uint8_t valuetype; // value type can be number/string/boolean/table uint8_t nocolliding; // 0 means colliding slot };
共享數據創建完后,各個服務通過sharedata.query(name)查詢共享對象,對象引用計數加一,使用者可以向讀取lua表一樣讀取數據,但其實際上是一個userdata(c結構),所以在corelib里定義了userdata的元表,元表中包含__index、__len、__pairs等方法。比如,__index方法會調用到c層的接口獲取指定key對應的value(第5行)。
1 -- lualib/skynet/sharedata/corelib.lua 2 local index = core.index 3 function meta:__index(key) 4 local obj = getcobj(self) 5 local v = index(obj, key) 6 ... 7 return v 8 end
2. 更新共享對象
當需要更新共享對象時,調用sharedata.update(name, v, ...) api,其原理是:創建一個新的共享對象(第17行),然后把舊對象標記為dirty(第20行)。
1 -- lualib/skynet/sharedata.lua 2 function sharedata.update(name, v, ...) 3 skynet.call(service, "lua", "update", name, v, ...) 4 end 5 6 function CMD.update(name, t, ...) 7 local v = pool[name] 8 local watch, oldcobj 9 if v then 10 watch = v.watch 11 oldcobj = v.obj 12 objmap[oldcobj] = true 13 sharedata.host.decref(oldcobj) 14 pool[name] = nil 15 pool_count[name] = nil 16 end 17 CMD.new(name, t, ...) 18 local newobj = pool[name].obj 19 if watch then 20 sharedata.host.markdirty(oldcobj) 21 for _,response in pairs(watch) do 22 response(true, newobj) 23 end 24 end 25 collect10sec() -- collect in 10 sec 26 end
此時,已引用該對象的服務並不會馬上更新,而是等到下一次使用該共享對象才會判斷是否要刷新,即惰性更新。
第4行,如果對象標記為dirty,說明需要更新,調用c庫接口進行更新。
1 -- lualib/skynet/sharedata/corelib.lua 2 local function getcobj(self) 3 local obj = self.__obj 4 if isdirty(obj) then 5 local newobj, newtbl = needupdate(self.__gcobj) 6 if newobj then 7 local newgcobj = newtbl.__gcobj 8 local root = findroot(self) 9 update(root, newobj, newgcobj) 10 if obj == self.__obj then 11 error ("The key [" .. genkey(self) .. "] doesn't exist after update") 12 end 13 obj = self.__obj 14 end 15 end 16 return obj 17 end
對於舊對象,如果沒有服務引用(即引用計數為0時),在下一次collectobj時會刪掉它並清理內存(第12行)。
1 -- service/sharedatad.lua 2 local function collectobj() 3 while true do 4 skynet.sleep(100) -- sleep 1s 5 if collect_tick <= 0 then 6 collect_tick = 600 -- reset tick count to 600 sec 7 collectgarbage() 8 for obj, v in pairs(objmap) do 9 if v == true then 10 if sharedata.host.getref(obj) <= 0 then 11 objmap[obj] = nil 12 sharedata.host.delete(obj) 13 end 14 end 15 end 16 else 17 collect_tick = collect_tick - 1 18 end 19 end 20 end
調用sharedata.delete(name) 將共享對象的引用計算減一。