基于MongoDB(Mongoose)的分布式鎖實(shí)現(xiàn)

當(dāng)分布式部署的時(shí)候, 簡(jiǎn)單的本地鎖是沒辦法滿足需求的. 實(shí)現(xiàn)分布式鎖的方法多樣, 比如基于Mysql或Redis的. 本文介紹基于MongoDB的分布式互斥鎖. 實(shí)現(xiàn)中, 采用Mongoose, 若是直接MongoDB, 也是差不多的.

我們將使用以下Mongoose的Schema在MongoDB中描述鎖:

const LockSchema = new mongoose.Schema({
    _id: String, // 鎖名
    acquirer: String, // 分布式結(jié)點(diǎn)的uuid
    acquiredAt: Date, // 獲取鎖時(shí)的時(shí)間
    updatedAt: { type: Date, expires: 2 * 60, default: Date.now } // 更新時(shí)間, 2分鐘后過期自動(dòng)刪除
});
  • _id: 這個(gè)_id直接用于存儲(chǔ)鎖名, 直接利用MongoDB中_id的唯一性來保證鎖的唯一
  • acquirer: 這種用于保存分布式結(jié)點(diǎn)的uuid, 這樣方便在數(shù)據(jù)中查看是誰(shuí)在使用這把鎖, 以及刪除的時(shí)候, 聯(lián)查這個(gè)屬性, 避免刪錯(cuò)
  • acquiredAt: 獲取到鎖的時(shí)候, 存入獲取時(shí)間到這個(gè)屬性, 這樣可以和updatedAt想減, 可得知正常使用的這個(gè)鎖的節(jié)點(diǎn)已經(jīng)使用了的時(shí)長(zhǎng).
  • updatedAt: 更新時(shí)間,
    1. 初始時(shí)和acquiredAt一致. 然后節(jié)點(diǎn)在使用時(shí), 如果執(zhí)行時(shí)間比較長(zhǎng), 則每隔一段時(shí)間調(diào)用renew函數(shù)更新一次這個(gè)屬性, 避免使用時(shí)長(zhǎng)過長(zhǎng), 導(dǎo)致超過了expires時(shí)間, 而被迫釋放鎖.
    2. 設(shè)置了自動(dòng)過期時(shí)間, 也就是expires屬性, 這個(gè)屬性對(duì)應(yīng)mongoDB中的expireafterseconds的屬性. 避免節(jié)點(diǎn)獲取鎖后, 掛掉, 從而導(dǎo)致死鎖. 超時(shí)后, MongoDB會(huì)自動(dòng)刪除. 注意: MongoDB的expire調(diào)度是每分鐘一次, 所以不是一過期就立馬刪除的

具體實(shí)現(xiàn)demo

首先dblock.js實(shí)現(xiàn)如下:

// dblock.js
const mongoose = require('mongoose');
mongoose.connect(
    'mongodb://127.0.0.1:27017/test',
    { useNewUrlParser: true }
);
const LockSchema = new mongoose.Schema({
    _id: String,
    acquirer: String,
    acquiredAt: Date,
    updatedAt: { type: Date, expires: 10, default: Date.now }
});
const Lock = mongoose.model('Lock', LockSchema);

class DBLock {
    constructor() {
        this._uuid = this.uuid(); // 分布式節(jié)點(diǎn)的uuid
        console.log(this._uuid);
    }

    // 基于時(shí)間戳生成的uuid
    uuid() {
        var d = Date.now();
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(
            c
        ) {
            var r = (d + Math.random() * 16) % 16 | 0;
            d = Math.floor(d / 16);
            return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
        });
    }

    // 獲取一次鎖
    async acquire(name) {
        try {
            const lock = new Lock({
                _id: name,
                acquirer: this._uuid,
                acquiredAt: new Date(),
                updatedAt: new Date()
            });
            await lock.save();
            return true;
        } catch (e) {
            console.log('cannot acquire');
            return false;
        }
    }

    // 獲取鎖, 每3s重試一次
    async lock(name, retryInterval = 3000) {
        while (true) {
            if (await this.acquire(name)) {
                break;
            } else {
                await this.sleep(retryInterval);
            }
        }
    }

    // 解鎖
    async unlock(name) {
        await Lock.deleteMany({ _id: name, acquirer: this._uuid });
    }

    // 續(xù)期
    async renew(name) {
        let result = await Lock.updateOne(
            { _id: name, acquirer: this._uuid },
            {
                updatedAt: new Date()
            }
        );
        console.log('renew');
    }

    // 睡眠
    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}

let instance = new DBLock();
module.exports = instance;

然后測(cè)試?yán)觤ain.js:

// main.js
const dblock = require('./dblock');

async function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function main() {
    while(true) {
      try {
        await dblock.lock('send_sms');
        console.log('Locked');
        await sleep(15 * 1000);
        await dblock.renew('send_sms');
        await sleep(15 * 1000);
        console.log('unlock');
        await sleep(3 * 1000);
      } finally {
        await dblock.unlock('send_sms');

      }
    }
}


main();

分布式測(cè)試的話, 可以手動(dòng)多開幾個(gè)shell, 同時(shí)運(yùn)行這個(gè)main.js, 即可模擬分布式中的鎖的爭(zhēng)搶及使用.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市郊闯,隨后出現(xiàn)的幾起案子逐虚,更是在濱河造成了極大的恐慌畏陕,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件畔况,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)叛溢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來劲适,“玉大人楷掉,你說我怎么就攤上這事∠际疲” “怎么了烹植?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵斑鸦,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我草雕,道長(zhǎng)巷屿,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任墩虹,我火速辦了婚禮嘱巾,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘诫钓。我一直安慰自己旬昭,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布菌湃。 她就那樣靜靜地躺著问拘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪慢味。 梳的紋絲不亂的頭發(fā)上场梆,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音纯路,去河邊找鬼或油。 笑死,一個(gè)胖子當(dāng)著我的面吹牛驰唬,可吹牛的內(nèi)容都是我干的顶岸。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼叫编,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼辖佣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起搓逾,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤卷谈,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后霞篡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體世蔗,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年朗兵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了污淋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡余掖,死狀恐怖寸爆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤赁豆,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布仅醇,位于F島的核電站,受9級(jí)特大地震影響歌憨,放射性物質(zhì)發(fā)生泄漏着憨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一务嫡、第九天 我趴在偏房一處隱蔽的房頂上張望甲抖。 院中可真熱鬧,春花似錦心铃、人聲如沸准谚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)柱衔。三九已至,卻和暖如春愉棱,著一層夾襖步出監(jiān)牢的瞬間唆铐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工奔滑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留艾岂,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓朋其,卻偏偏與公主長(zhǎng)得像王浴,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子梅猿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354