使用redis實(shí)現(xiàn)點(diǎn)贊功能的幾種思路

原文發(fā)表在我的個(gè)人博客 - 使用redis實(shí)現(xiàn)點(diǎn)贊功能的幾種思路

點(diǎn)贊功能幾乎是現(xiàn)在互聯(lián)網(wǎng)產(chǎn)品的標(biāo)配了,點(diǎn)贊存在的意思還是蠻有趣的為什么社交網(wǎng)站的評(píng)價(jià)功能多采用「點(diǎn)贊」的模式?。

本文主要介紹本人工作中遇到的點(diǎn)贊需求以及使用redis的解決思路。第一種點(diǎn)贊需求是比較常規(guī)的點(diǎn)贊需求,類似于微博那種點(diǎn)贊模式,用戶可以對(duì)某條信息點(diǎn)贊、取消點(diǎn)贊、查詢是否點(diǎn)贊、被點(diǎn)贊次數(shù)等等;第二種點(diǎn)贊稍微特殊,用戶可以在一天內(nèi)對(duì)任意用戶點(diǎn)贊,取消點(diǎn)贊后不可以再次對(duì)同用戶點(diǎn)贊,第二天限制解除,可以重新對(duì)同一玩家點(diǎn)贊(也就是說(shuō)點(diǎn)贊是可以累加的),然后還有一個(gè)需求是要求可以實(shí)時(shí)查用戶獲贊次數(shù)全局的排行情況。

需求一解決思路

對(duì)于需求一,采用的是redis bitmap來(lái)實(shí)現(xiàn)。

bitmap簡(jiǎn)介

bitmap

bitmap是一連串的二進(jìn)制數(shù)字(0,1),每一位所在的位置為偏移(offset),在bitmap上可以執(zhí)行AND,OR,XOR以及其他操作。

位圖計(jì)數(shù)

位圖計(jì)數(shù)的意思是統(tǒng)計(jì)bitmap中值為1的位的個(gè)數(shù),位圖計(jì)數(shù)的效率是很高的。

redis bitmap

redis中允許使用二進(jìn)制的Key和二進(jìn)制的Value,bitmap就是二進(jìn)制的Value。

點(diǎn)贊/取消點(diǎn)贊

假設(shè)用戶的數(shù)字id為1000,對(duì)照片id為100的照片點(diǎn)贊。首先根據(jù)照片id生成贊數(shù)據(jù)存儲(chǔ)的redis key,比如生成策略為like_photo:{photo_id},id為1000的用戶點(diǎn)贊,只需要將like_photo:100的第1000位置為1即可(取消贊則置為0)。

redis setbit操作的時(shí)間復(fù)雜度為O(1),所以這種點(diǎn)贊方式十分高效。

redis.setbit('like_photo:100', 1000, 1, function(err, ret){
    // deal err and ret.
});

當(dāng)前是否點(diǎn)贊

用戶打開(kāi)圖片的時(shí)候需要查詢當(dāng)前是否點(diǎn)贊過(guò)該照片,查詢是否點(diǎn)贊可以通過(guò)redis getbit操作來(lái)實(shí)現(xiàn)。比如查詢用戶id為1000的用戶是否點(diǎn)贊過(guò)照片id為100的照片,只需要對(duì)like_photo:100bitmap的第1000位取值即可。

redis getbit操作的時(shí)間復(fù)雜度同樣是O(1)。

redis.getbit('like_photo:100', 1000, function(err, liked){
    // deal err.
    // if liked==1 liked, liked==0 not like yet.
});

查詢點(diǎn)贊總次數(shù)

比如需要顯示照片id為100的照片的獲贊次數(shù),只需要對(duì)like_photo:100bitmap進(jìn)行位圖計(jì)數(shù)操作即可。

redis bitcount操作的時(shí)間復(fù)雜度雖然是O(N)的,但是大部分?jǐn)?shù)據(jù)量的情況下是不需要擔(dān)心bitcount效率問(wèn)題的。

redis.bitcount('like_photo:100', function(err, likeCnt){
    // deal with err and likeCnt.
});

其他

redis bitmap的實(shí)現(xiàn)中還提供了bitop等其他API,可以實(shí)現(xiàn)其他一些有趣的事情。

比如要計(jì)算同時(shí)點(diǎn)贊了100和101兩張照片的用戶,可以通過(guò)如下操作實(shí)現(xiàn):

redis.bitop('AND', 'like_photo:100&101', 'like_photo:100', 'like_photo:101', function(err, ret){
    // 得到的like_photo:100&101這個(gè)臨時(shí)key中即是同時(shí)點(diǎn)贊100和101的用戶bitmap.
});

局限性

這種方案雖然比較高效,實(shí)現(xiàn)起來(lái)也比較簡(jiǎn)單,但是也有一定的局限性。
1.需要用戶有類似于數(shù)據(jù)庫(kù)自增id的數(shù)字id,當(dāng)然如果你是從10000之類的開(kāi)始自增的,在bitmap操作的時(shí)候可以統(tǒng)一將用戶id減掉10000,這樣可以稍微節(jié)省一些redis內(nèi)存占用;
2.當(dāng)用戶量很大的時(shí)候,比如千萬(wàn)級(jí)用戶量的情況下,一個(gè)用戶的bitmap需要消耗的內(nèi)存為:10000000/8/1024/1024=1.19MB,當(dāng)bitmap數(shù)量較多的時(shí)候,內(nèi)存占用還是很可觀的。不過(guò)在用戶量較少的時(shí)候這種方案還是不錯(cuò)的~

需求二解決思路

對(duì)于需求二采用redis sorted set來(lái)實(shí)現(xiàn),sorted set也是redis中比較常用的數(shù)據(jù)結(jié)構(gòu),這里就不再介紹了。

點(diǎn)贊redis key設(shè)計(jì)

由于需求二中有當(dāng)日這種時(shí)間限制,所以在設(shè)計(jì)key的時(shí)候可以將日期作為key的一部分。

1.存儲(chǔ)MMDD日期用戶uid的獲贊數(shù)據(jù):

var genLikeKey = function(uid){
    return 'like:'+moment().format('MMDD')+':'+uid;
};

2.全局獲贊數(shù)據(jù)key:

var globalLikeRankKey = function(){
    return "GLOBAL_LIKE_RANK";
};

點(diǎn)贊、取消點(diǎn)贊

比如玩家100對(duì)玩家101點(diǎn)贊:

redis.zadd(genLikeKey(101), LIKE_TYPE.LIKE, 100, function(err, ret){
    // set expire
    redis.expireat(genLikeKey(101), getExpireAt(), function(err, ret){});
    // 累計(jì)獲贊數(shù)
    redis.zincrby(globalLikeRankKey(), 1, playerId, function(err, ret){
        // your logic here.
    });
});

取消贊:

redis.zadd(genLikeKey(101), LIKE_TYPE.UNLIKE, 100, function(err, ret){
    // set expire
    redis.expireat(genLikeKey(101), getExpireAt(), function(err, ret){});
    // 累計(jì)獲贊數(shù)
    redis.zincrby(globalLikeRankKey(), -1, playerId, function(err, ret){
        // your logic here.
    });
});

點(diǎn)贊狀態(tài)

比如查詢玩家100對(duì)玩家101的點(diǎn)贊狀態(tài):

redis.zscore(genLikeKey(101), 100, function(err, likeType){
    // likeType==LIKE_TYPE.LIKE 表示贊過(guò)了。
});

點(diǎn)贊排行

比如查詢玩家100的獲贊排行:

// 取uid100得贊次數(shù)
redis.zscore(globalLikeRankKey(), 100, function(err, likeCnt){
    // deal err first.
    // 取小于贊次數(shù)的人數(shù)
    redis.zcount(globalLikeRankKey(), 0, likeCnt, function(err, cnt){
          // 取全部人數(shù)
        redis.zcard(globalLikeRankKey(), function(err, total){
            // 排名。
            var rank = Math.floor(Number(cnt)*100/Number(total));
        });
    });
});

sorted set性能

擔(dān)心zcount操作的性能?直接看我在windows上測(cè)試的結(jié)果吧。

機(jī)器環(huán)境:
CPU:i5-3470@3.2GHz
RAM:8G
OS:64位windows 7
redis: v2.8.17

插入測(cè)試數(shù)據(jù):

var kvs = [];
// 500w測(cè)試數(shù)據(jù)
var cnt = 5000000;
for(var i=0;i<cnt;i++){
    kvs.push([i, Math.floor(Math.random()*cnt)]);
}
async.eachLimit(kvs, 10000, function(kv, cb){
    redis.zadd('TEST_SS', kv[1], kv[0], function(err, ret){
        cb(null, null);
    });
}, function(err){
    console.log('finish insert.');
    process.exit(1);
});

測(cè)試zcount效率:

var scores = [];
for(var i=0;i<10000;i++){
    scores.push(i);
}

var st = Date.now();
//10000并發(fā)zcount操作
async.each(scores, function(score, cb){
    redis.zcount('TEST_SS', 0, score, function(err, ret){
        cb(null, null);
    });
}, function(err){
    console.log('cost ', Date.now()-st, ' ms.');
});

多次測(cè)試結(jié)果:

cost 237 ms.

ps: 文中都是nodejs寫(xiě)的偽代碼,也沒(méi)有使用流程控制庫(kù),代碼不夠嚴(yán)謹(jǐn),輕拍。

-EOF-

update:
2019-04-18
更新文中“當(dāng)用戶量很大的時(shí)候,比如千萬(wàn)級(jí)用戶量的情況下,一個(gè)點(diǎn)贊bitmap需要消耗的內(nèi)存為:10000000/8/1024/1024=1.19MB”的筆誤,“一個(gè)點(diǎn)贊”應(yīng)為“一個(gè)用戶”。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容