PHP-Redis擴(kuò)展遇到的一個(gè)小坑

年前在做項(xiàng)目的時(shí)候遇到了一個(gè)小坑,就是把一個(gè)超過(guò)14位的十進(jìn)制數(shù)字作為score和member存到redis的sorted set的時(shí)候郊酒,用PHP讀取數(shù)據(jù)的時(shí)候score會(huì)被轉(zhuǎn)換成浮點(diǎn)數(shù),最末兩位的數(shù)字會(huì)消失,導(dǎo)致當(dāng)用score作為分頁(yè)標(biāo)志的時(shí)候會(huì)出現(xiàn)分頁(yè)異常的情況礁阁。

1 事故現(xiàn)場(chǎng)


1.1 插入數(shù)據(jù)
redis> zadd sorted_set_key_test 3888153779537508 3888153779537508
(integer) 1
redis> zrange sorted_set_key_test 0 -1 withscores
1) "3888153779537508"
2) "3888153779537508"
redis>
1.2 讀取數(shù)據(jù)
//redis.php
<?php
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379);
    var_dump($redis->ZRANGE("zset_test", 0, -1, true));
[root@localhost ~]# php redis.php
array(1) {
  [3888153779537508]=>
  float(3.8881537795375E+15)
}
[root@localhost ~]#

2 分析過(guò)程


2.1 redis存儲(chǔ)的大小限制

redis中sorted set的scores取值范圍

?根據(jù)上面的文檔:Redis里sorted set類(lèi)型的score支持整形和浮點(diǎn)型,其中整形的-(2^53) 到+(253)夕冲,而3888153779537508并沒(méi)有超過(guò)253(18014398509481983)這個(gè)范圍

2.2 通過(guò)tcpdump抓包氮兵,確定問(wèn)題是在PHP中
tcpdump -i lo port 6379 -q -X
tcpdump抓包結(jié)果

?如圖所示,第一個(gè)紅框內(nèi)為php向redis服務(wù)器發(fā)送請(qǐng)求的數(shù)據(jù)包歹鱼,第二個(gè)紅框內(nèi)為redis返回?cái)?shù)據(jù)結(jié)果的數(shù)據(jù)包泣栈,包內(nèi)容如下:

[root@localhost ~]# telnet 127.0.0.1 6379
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
zrange zset_test 0 -1 withscores
*2
$16
3888153779537508
$16
3888153779537508

發(fā)現(xiàn)redis返回的是字符串類(lèi)型的一串?dāng)?shù)字

2.3 通過(guò)查PHP-redis擴(kuò)展的源碼,發(fā)現(xiàn)是擴(kuò)展的問(wèn)題
//redis.c  line:1966-1971
/* {{{ proto array Redis::zRange(string key,int start,int end,bool scores=0) */
PHP_METHOD(Redis, zRange)
{
    generic_zrange_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU, "ZRANGE",
        redis_zrange_cmd);
}

首先找到了實(shí)現(xiàn)$redis->zrange的函數(shù),發(fā)現(xiàn)調(diào)用了generic_zrange_cmd的函數(shù)

//redis_cluster.c line:1586-1617
/* Generic implementation for ZRANGE, ZREVRANGE, ZRANGEBYSCORE, ZREVRANGEBYSCORE */
static void generic_zrange_cmd(INTERNAL_FUNCTION_PARAMETERS, char *kw,
                               zrange_cb fun)
{
    redisCluster *c = Z_REDIS_OBJ_P(getThis());
    cluster_cb cb;
    char *cmd; int cmd_len; short slot;
    int withscores=0;

    if(fun(INTERNAL_FUNCTION_PARAM_PASSTHRU, c->flags, kw, &cmd, &cmd_len,
           &withscores, &slot, NULL)==FAILURE)
    {
        efree(cmd);
        RETURN_FALSE;
    }

    if(cluster_send_command(c,slot,cmd,cmd_len TSRMLS_CC)<0 || c->err!=NULL) {
        efree(cmd);
        RETURN_FALSE;
    }

    efree(cmd);

    cb = withscores ? cluster_mbulk_zipdbl_resp : cluster_mbulk_resp;
    if (CLUSTER_IS_ATOMIC(c)) {
        cb(INTERNAL_FUNCTION_PARAM_PASSTHRU, c, NULL);
    } else {
        void *ctx = NULL;
        CLUSTER_ENQUEUE_RESPONSE(c, slot, cb, ctx);
        RETURN_ZVAL(getThis(), 1, 0);
    }
}

在這個(gè)函數(shù)的1609行南片,判斷如果有withscores掺涛,調(diào)用cluster_mbulk_zipdbl_resp

//cluster_library.c line:2207-2214
/* Handling key,value to key=>value where the values are doubles */
PHP_REDIS_API void
cluster_mbulk_zipdbl_resp(INTERNAL_FUNCTION_PARAMETERS, redisCluster *c,
                          void *ctx)
{
    cluster_gen_mbulk_resp(INTERNAL_FUNCTION_PARAM_PASSTHRU, c,
        mbulk_resp_loop_zipdbl, NULL);
}

直接調(diào)用了cluster_gen_mbulk_resp,并且把mbulk_resp_loop_zipdbl函數(shù)作為參數(shù)傳了進(jìn)去疼进,發(fā)現(xiàn)在cluster_gen_mbulk_resp最終調(diào)用了mbulk_resp_loop_zipdbl

//cluster_library.c line:2318-2356
/* MULTI BULK loop processor where we expect key,score key, score */
int mbulk_resp_loop_zipdbl(RedisSock *redis_sock, zval *z_result,
                           long long count, void *ctx TSRMLS_DC)
{
    char *line, *key;
    int line_len, key_len;
    long long idx=0;

    // Our context will need to be divisible by 2
    if(count %2 != 0) {
        return -1;
    }

    // While we have elements
    while(count--) {
        line = redis_sock_read(redis_sock, &line_len TSRMLS_CC);
        if (line != NULL) {
            if(idx++ % 2 == 0) {
                key = line;
                key_len = line_len;
            } else {
                zval z;
                if (redis_unserialize(redis_sock,key,key_len, &z TSRMLS_CC)) {
                    convert_to_string(&z);
                    add_assoc_double_ex(z_result, Z_STRVAL(z), Z_STRLEN(z), atof(line));
                    zval_dtor(&z);
                } else {
                    add_assoc_double_ex(z_result, key, key_len, atof(line));
                }

                /* Free our key and line */
                efree(key);
                efree(line);
            }
        }
    }

    return SUCCESS;
}

發(fā)現(xiàn)薪缆,在最終處理score數(shù)據(jù)的時(shí)候調(diào)用了atof函數(shù),將redis服務(wù)器返回的字符串轉(zhuǎn)換成了浮點(diǎn)數(shù)伞广,導(dǎo)致了開(kāi)始說(shuō)的問(wèn)題拣帽。

3 總結(jié)


3.1 后續(xù)處理

當(dāng)時(shí)發(fā)現(xiàn)這個(gè)問(wèn)題之后,將score作為member的一部分存在redis里嚼锄,取出來(lái)之后减拭,從member中獲取到score,再根據(jù)這個(gè)score來(lái)進(jìn)行分頁(yè)獲取數(shù)據(jù)

3.2 什么時(shí)候會(huì)出現(xiàn)這樣的問(wèn)題

經(jīng)過(guò)簡(jiǎn)單的嘗試区丑,發(fā)現(xiàn)php最多可以保存14位精確的數(shù)據(jù)在float類(lèi)型中拧粪,如果超過(guò)14位,最末幾位會(huì)失去精度

3.3 后續(xù)吧

在github里提了issue沧侥,作者給出的回答是:



?大概是說(shuō)2.2.7之后的版本都是這么處理的可霎,理論上可以對(duì)獲取到的score進(jìn)行判斷是浮點(diǎn)數(shù)還是整數(shù),但是這樣會(huì)消耗一部分性能宴杀。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末癣朗,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子婴氮,更是在濱河造成了極大的恐慌斯棒,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件主经,死亡現(xiàn)場(chǎng)離奇詭異荣暮,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)罩驻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)穗酥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人惠遏,你說(shuō)我怎么就攤上這事砾跃。” “怎么了节吮?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵抽高,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我透绩,道長(zhǎng)翘骂,這世上最難降的妖魔是什么壁熄? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮碳竟,結(jié)果婚禮上草丧,老公的妹妹穿的比我還像新娘。我一直安慰自己莹桅,他們只是感情好昌执,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著诈泼,像睡著了一般懂拾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上铐达,一...
    開(kāi)封第一講書(shū)人閱讀 51,165評(píng)論 1 299
  • 那天委粉,我揣著相機(jī)與錄音,去河邊找鬼娶桦。 笑死,一個(gè)胖子當(dāng)著我的面吹牛汁汗,可吹牛的內(nèi)容都是我干的衷畦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼知牌,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼祈争!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起角寸,我...
    開(kāi)封第一講書(shū)人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤菩混,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后扁藕,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體沮峡,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年亿柑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了邢疙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡望薄,死狀恐怖疟游,靈堂內(nèi)的尸體忽然破棺而出胁赢,到底是詐尸還是另有隱情勇凭,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布仰剿,位于F島的核電站卧须,受9級(jí)特大地震影響另绩,放射性物質(zhì)發(fā)生泄漏儒陨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一板熊、第九天 我趴在偏房一處隱蔽的房頂上張望框全。 院中可真熱鬧,春花似錦干签、人聲如沸津辩。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)喘沿。三九已至,卻和暖如春竭贩,著一層夾襖步出監(jiān)牢的瞬間蚜印,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工留量, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留窄赋,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓楼熄,卻偏偏與公主長(zhǎng)得像忆绰,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子可岂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

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