浮點(diǎn)數(shù)字節(jié)序比較

在開發(fā)LedisDB的時(shí)候猿妈,我曾考慮將zset的score使用跟redis一樣的double類型拿穴,但是卻沒想好如何將double在底層LevelDB或者RocksDB下存儲(chǔ),使其能夠支持zset中zrangebyscore等命令,所以只能考慮使用int64類型來代替稼虎。但在開發(fā)qdb的時(shí)候庐杨,最開始我們?nèi)匀恢皇侵С謎nt64选调,但最終通過努力,支持了double灵份,使其能跟redis的zset api完全兼容仁堪。其實(shí)后來發(fā)現(xiàn),實(shí)現(xiàn)很簡(jiǎn)單填渠。

LevelDB和ZSet

這里需要簡(jiǎn)單說明一下LevelDB(RocksDB只是它的一個(gè)衍生優(yōu)化版本弦聂,但最核心基本的原理還是一樣的)。

LevelDB是一個(gè)高性能的KV存儲(chǔ)庫氛什,數(shù)據(jù)在LevelDB里面是根據(jù)key來進(jìn)行排序存儲(chǔ)的莺葫,而key和value是任意的字節(jié)數(shù)組。在外部看來枪眉,LevelDB里面的數(shù)據(jù)就是一個(gè)有序map徙融,map里面的key是順序存儲(chǔ)的,如下:

{
    "key1" : "value1",
    "key2" : "value2",
    ...
}

在redis里面瑰谜,zset是一個(gè)有序集合欺冀,每一個(gè)member都對(duì)應(yīng)一個(gè)score,member是唯一的萨脑,score則可能重復(fù)隐轩。我們可以通過score進(jìn)行很多處理,譬如獲取[score1, score2]這個(gè)區(qū)間的所有元素渤早,或者獲取某個(gè)member對(duì)應(yīng)的score再整個(gè)zset里面的rank职车。為了實(shí)現(xiàn)上面的需求,我們需要在內(nèi)部用一個(gè)有序的數(shù)據(jù)結(jié)構(gòu)來存儲(chǔ)score以及其對(duì)應(yīng)的member,redis使用skip list來進(jìn)行處理悴灵,那如何將這樣的映射關(guān)系在LevelDB里面很好的處理呢扛芽?

因?yàn)長(zhǎng)evelDB將數(shù)據(jù)是按照key順序存儲(chǔ)的,所以我們只需要將score的信息綁定到key上面积瞒,就能很容易的實(shí)現(xiàn)一個(gè)簡(jiǎn)易的skip list川尖。對(duì)于一個(gè)zset里面的member,可能在LevelDB里面實(shí)際的key結(jié)構(gòu)如下:

key:score:member

對(duì)于一個(gè)zset來說茫孔,key和member都是arbitrary bytes array叮喳,可以很方便的綁定到LevelDB的key上面,但是score可是double的缰贝,如何綁定上去參與排序呢馍悟?

LedisDB ZSet int64 score

再說double之前,先來聊聊LedisDB的做法剩晴,LedisDB使用的score是int64锣咒,對(duì)于一個(gè)int64來說,計(jì)算機(jī)內(nèi)部是使用8 bytes進(jìn)行存儲(chǔ)的赞弥,下面是例子毅整,會(huì)打印不同int64數(shù)據(jù)在小端序和大端序下8字節(jié)array十六進(jìn)制表現(xiàn):

import "fmt"
import "math"
import "encoding/binary"

func p(a int64) {
    b1 := make([]byte, 8)
    b2 := make([]byte, 8)

    binary.LittleEndian.PutUint64(b1, uint64(a))
    binary.BigEndian.PutUint64(b2, uint64(a))

    fmt.Printf("%0x\t%0x\n", b1, b2)
}

func main() {
    p(0)
    p(1)
    p(0xFFFF0000)
    p(math.MaxInt64)
    
    p(-1)
    p(-200)
    p(-300)
    p(math.MinInt64)
}

輸出結(jié)果如下:

0000000000000000    0000000000000000

0100000000000000    0000000000000001
0000ffff00000000    00000000ffff0000
ffffffffffffff7f    7fffffffffffffff

ffffffffffffffff    ffffffffffffffff
38ffffffffffffff    ffffffffffffff38
d4feffffffffffff    fffffffffffffed4
0000000000000080    8000000000000000

第一列是小端序打印的結(jié)果,第二列是大端序打印的結(jié)果嗤攻。首先我們可以看到毛嫉,對(duì)于0來說,無論什么端序妇菱,都是全0承粤,對(duì)于正整數(shù)來說,我們可以看到闯团,0xFFFF0000是鐵定大于1的辛臊,但是在對(duì)應(yīng)的小端序存儲(chǔ)的bytes array里面,會(huì)發(fā)現(xiàn)如果按照字節(jié)序進(jìn)行排序0100000000000000是大于0000ffff00000000的房交,而大端序的結(jié)果0000000000000001是小于00000000ffff0000的彻舰。對(duì)于負(fù)整數(shù)也是一樣,-200是大于-300的候味,但是小端序的結(jié)果是小于刃唤,而大端序的結(jié)果是大于。

所以我們可以知道白群,對(duì)于一個(gè)整數(shù)尚胞,我們需要使用大端序的方式將其綁定到LevelDB的key上面,這樣才能參與正常的排序帜慢。但是我們又發(fā)現(xiàn)笼裳,如果直接這樣處理唯卖,負(fù)數(shù)的大端序結(jié)果是鐵定大于正數(shù)的,譬如-1的ffffffffffffffff就大于1的0000000000000001躬柬,所以為了正常的處理整數(shù)的排序拜轨,我們只需要在key上面加一個(gè)前綴標(biāo)志就可以了。LedisDB里面做了如下處理:

key<38ffffffffffffff:member
key<ffffffffffffffff:member
key=0000000000000000:member
key=0000000000000001:member

因?yàn)?code>=的ascii值大于<允青,我們將正整數(shù)和0前面加上=橄碾,將負(fù)數(shù)前面加上<,這樣在進(jìn)行字節(jié)序排序的時(shí)候昧廷,正數(shù)和0一定能排在負(fù)數(shù)的后面堪嫂,也就是完全能滿足從小到大排序的需求了偎箫。

QDB ZSet double score

上面可以知道木柬,我們使用大端序加上一個(gè)前綴標(biāo)志,就能很好的處理int64類型的score的排序淹办,但double可沒有這么簡(jiǎn)單眉枕,不然LedisDB早就支持了。

首先我們來看看double類型IEEE754規(guī)范怜森,對(duì)于一個(gè)double來說速挑,使用8 bytes存儲(chǔ),格式如下:

  • 符號(hào): 1 bit (0為正副硅,1為負(fù))
  • 指數(shù): 11 bits
  • 分?jǐn)?shù): 52 bits
double format
double format

對(duì)于一個(gè)64 bits的double姥宝,如果指數(shù)為e,那么它對(duì)應(yīng)的值計(jì)算方式為:

如果指數(shù)越大恐疲,那么double的值越大腊满,如果分?jǐn)?shù)越大,在相同指數(shù)的情況下面double值也越大培己。所以我們?nèi)绻麑ouble使用大端序存放到8字節(jié)array里面碳蛋,我們就能直接進(jìn)行字節(jié)序比較,但這個(gè)僅限于正的double情況下省咨。因?yàn)閷?duì)于負(fù)的double肃弟,它除了最高位bit為1這一點(diǎn)不一樣之外,其余完全跟相反的正數(shù)底層存儲(chǔ)方式一模一樣零蓉,所以如果我們直接將其轉(zhuǎn)為大端序比較笤受,會(huì)發(fā)現(xiàn)結(jié)果是相反的。一個(gè)簡(jiǎn)單地例子:

func d(a float64) {
    b := make([]byte, 8)

    binary.BigEndian.PutUint64(b, math.Float64bits(a))

    fmt.Printf("%064b\n", binary.BigEndian.Uint64(b))
}

func main() {
    d(0)
    
    d(1)
    d(2)
    
    d(-1)
    d(-2)
}

結(jié)果如下:

0000000000000000000000000000000000000000000000000000000000000000
0011111111110000000000000000000000000000000000000000000000000000
0100000000000000000000000000000000000000000000000000000000000000
1011111111110000000000000000000000000000000000000000000000000000
1100000000000000000000000000000000000000000000000000000000000000

這里在插入一片廣告文章敌蜂,推薦閱讀箩兽,這里

當(dāng)初LedisDB就是因?yàn)闆]有辦法很好的處理負(fù)double的排序比較問題紊册,才采用了int64比肄,但是真的沒有辦法嗎快耿?為啥int64類型的負(fù)數(shù)能通過大端序進(jìn)行字節(jié)序比較呢?我們知道芳绩,在計(jì)算機(jī)內(nèi)部掀亥,負(fù)數(shù)是使用補(bǔ)碼存儲(chǔ)的,也就是反碼加1妥色,負(fù)數(shù)的反碼搪花,就是在源碼的基礎(chǔ)上,符號(hào)位不變嘹害,其余各個(gè)位取反撮竿。對(duì)于-1來說,它的源碼為[10000001]笔呀,反碼為[11111110]幢踏,補(bǔ)碼為[11111111]。(為了簡(jiǎn)單许师,使用int8表示的)房蝉。就因?yàn)樗辛巳》吹倪@一步,我們才能直接用大端序字節(jié)序比較微渠。

所以對(duì)于double的負(fù)數(shù)搭幻,我們也僅僅需要干一件事情,就是取反逞盆,那么它的大端序字節(jié)序比較就是正常的了檀蹋。在QDB里面,對(duì)于一個(gè)double云芦,我們做了如下處理:

// We can not use lexicographically bytes comparison for negative and positive float directly.
// so here we will do a trick below.
func float64ToUint64(f float64) uint64 {
    u := math.Float64bits(f)
    if f >= 0 {
        u |= 0x8000000000000000
    } else {
        u = ^u
    }
    return u
}

func uint64ToFloat64(u uint64) float64 {
    if u&0x8000000000000000 > 0 {
        u &= ^uint64(0x8000000000000000)
    } else {
        u = ^u
    }
    return math.Float64frombits(u)
}

如果一個(gè)double數(shù)據(jù)值大于等于0俯逾,那么我們將最高位bit設(shè)置為1,而對(duì)于負(fù)數(shù)焕数,我們直接取反纱昧,就這一步簡(jiǎn)單的操作,就能讓我們完美的支持double的score了堡赔。

總結(jié)

讓QDB的zset支持double score识脆,其實(shí)很簡(jiǎn)單的一件事情,但我在開發(fā)LedisDB的時(shí)候竟然沒有想到如何去解決善已。我們很多時(shí)候灼捂,往往追求高大上的東西,譬如架構(gòu)换团,設(shè)計(jì)模式等悉稠,但忽略了計(jì)算機(jī)最基本的底層知識(shí)(上面這些其實(shí)也就是大小端序,補(bǔ)碼艘包,反碼等)的猛,所以有時(shí)候還真的重新好好把最基本的東西給回顧一下耀盗。

最后,在推薦一下QDB卦尊,QDB是一個(gè)類似Redis的KV數(shù)據(jù)庫叛拷,底層使用LevelDB/RocksDB作為存儲(chǔ)引擎,解決了Redis內(nèi)存限制問題岂却,同時(shí)能支持string忿薇,hash,list躏哩,set和zset這幾種數(shù)據(jù)結(jié)構(gòu)署浩,性能卓越,你也可以認(rèn)為它是LedisDB的升級(jí)版本扫尺。

網(wǎng)址 https://github.com/reborndb/qdb筋栋。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市器联,隨后出現(xiàn)的幾起案子二汛,更是在濱河造成了極大的恐慌婿崭,老刑警劉巖拨拓,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異氓栈,居然都是意外死亡渣磷,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門授瘦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來醋界,“玉大人,你說我怎么就攤上這事提完⌒畏模” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵徒欣,是天一觀的道長(zhǎng)逐样。 經(jīng)常有香客問我,道長(zhǎng)打肝,這世上最難降的妖魔是什么脂新? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮粗梭,結(jié)果婚禮上争便,老公的妹妹穿的比我還像新娘。我一直安慰自己断医,他們只是感情好滞乙,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布奏纪。 她就那樣靜靜地躺著,像睡著了一般斩启。 火紅的嫁衣襯著肌膚如雪亥贸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天浇垦,我揣著相機(jī)與錄音炕置,去河邊找鬼。 笑死男韧,一個(gè)胖子當(dāng)著我的面吹牛朴摊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播此虑,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼甚纲,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了朦前?” 一聲冷哼從身側(cè)響起介杆,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎韭寸,沒想到半個(gè)月后遇革,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡门粪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年铣焊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晶渠。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡凰荚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出褒脯,到底是詐尸還是另有隱情便瑟,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布番川,位于F島的核電站到涂,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏爽彤。R本人自食惡果不足惜养盗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望适篙。 院中可真熱鬧往核,春花似錦、人聲如沸嚷节。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至衩婚,卻和暖如春窜护,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背非春。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工柱徙, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人奇昙。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓护侮,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親储耐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子羊初,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 本文為筆者對(duì)在學(xué)習(xí)Redis過程中所收集資料的一個(gè)總結(jié),目的是為了以后方便回顧相關(guān)的知識(shí),大部分為非原創(chuàng)內(nèi)容什湘。特此...
    EakonZhao閱讀 14,436評(píng)論 0 9
  • Redis 數(shù)據(jù)結(jié)構(gòu)簡(jiǎn)介 Redis 可以存儲(chǔ)鍵與5種不同數(shù)據(jù)結(jié)構(gòu)類型之間的映射长赞,這5種數(shù)據(jù)結(jié)構(gòu)類型分別為Stri...
    DreamerRzc閱讀 236,862評(píng)論 26 273
  • sorted set 是 set 的一個(gè)升級(jí)版本,它在 set 的基礎(chǔ)上增加了一個(gè)順序?qū)傩悦龀罚?set 一樣 so...
    鬭闢閱讀 8,605評(píng)論 1 0
  • 字符編碼方案的演變與字節(jié)序 一得哆、字符編碼方案的演變 1. 前文已經(jīng)提及,編號(hào)字符集CCS(簡(jiǎn)稱字符集)與字符編碼方...
    笨笨阿林閱讀 1,127評(píng)論 1 3
  • 大學(xué)叫會(huì)我的是更勇敢的表達(dá)自己腹尖,更好的隱藏自己的私人生活柳恐,控制好自己的情緒。大學(xué)就是那么美好又無奈热幔!
    安靜的雨閱讀 162評(píng)論 0 0