本文主要介紹在琴房預(yù)約項(xiàng)目中所用到的資源訪問加鎖技術(shù)Redlock档桃,以及如果實(shí)現(xiàn)將redlock轉(zhuǎn)為非阻塞鎖粘拾。
1. 背景簡介
1.1 項(xiàng)目簡介
本次實(shí)踐項(xiàng)目是THU琴房預(yù)約小程序與web管理端,后端使用koa框架偎箫。主要功能有預(yù)約琴房(用戶)木柬、更改琴房可用時(shí)間(管理端)等。后端使用mysql存儲琴房信息(包含琴房可用時(shí)間串)淹办。
1.2 問題介紹
- 多個(gè)用戶可能在同一時(shí)間對同一琴房的同一時(shí)間段進(jìn)行預(yù)約
- 用戶與管理員可能同時(shí)進(jìn)行預(yù)約弄诲、更改琴房可用時(shí)間
根據(jù)上篇文章所描述的,因?yàn)榇嬖诓煌僮髦g的資源競爭,所以我們這里使用增加信號量的方式——Redlock齐遵。
2. 加鎖實(shí)踐
redlock是一個(gè)基于redis數(shù)據(jù)庫的分布式鎖寂玲。通過在redis數(shù)據(jù)庫中設(shè)定鍵值來進(jìn)行信號量的定義。
- 我們把“預(yù)約“和“更改琴房可用時(shí)間”定義為使用同一個(gè)鍵值梗摇。
- 在操作前首先獲取此鍵值拓哟,如果獲取不到,說明被占用伶授。
關(guān)于這個(gè)流程断序,包括set鍵值與”占用"和"未占用“狀態(tài)的更改是由Redlock實(shí)現(xiàn)的。
2.1 給資源加鎖
// 獲取權(quán)限
let redis = require("redis");
let client = redis.createClient("to change: your redis port","to change: your server ip");
let redlock = new Redlock([client]);
// 設(shè)置時(shí)間
let totalTime = 5000;
let key = "to change: as you like";
// 加鎖
redlock.lock(key, totalTime).then(async function(lock){
// to do
// your operation block
// release lock
lock.unlock().catch(function(err){})
}
}).catch(()=>{}) // 如果不加catch會直接報(bào)錯(cuò)終止程序
主要流程為:
- 獲取redis的操作權(quán)限
- 設(shè)定鍵值糜烹,最長等待時(shí)間(防止一直被占用)
- 執(zhí)行操作
- 釋放鎖
按照上面流程违诗,我們成功進(jìn)行了加鎖。
鍵值的設(shè)置非常自由疮蹦,我們可以通過鍵值的設(shè)置诸迟,控制加鎖覆蓋的范圍以及力度。
2.2 阻塞鎖與非阻塞鎖
在完成加鎖之后愕乎,我們進(jìn)行壓力測試阵苇,發(fā)現(xiàn)了一個(gè)非常坑的情況:Redlock是非阻塞鎖感论。
這就意味著绅项,當(dāng)一個(gè)用戶進(jìn)行預(yù)約的時(shí)候,別的用戶如果有請求就會直接失敗比肄,這樣是非常用戶不友好的快耿。所以我們需要一些操作來將redlock轉(zhuǎn)為非阻塞鎖。有兩個(gè)方案:
- 方案一:實(shí)現(xiàn)一個(gè)請求隊(duì)列以及回調(diào)函數(shù)芳绩,在資源被釋放的時(shí)候掀亥,進(jìn)行回調(diào)。
- 方案二:設(shè)置最長等待時(shí)間示括,在此時(shí)間段內(nèi)進(jìn)行輪詢,如果超過此時(shí)間痢畜,放棄請求垛膝。
對于方案一,請求隊(duì)列可能會很長丁稀,如果前面的資源不釋放吼拥,就會一直等待;對于方案二线衫,輪詢需要消耗更多的資源凿可。
我們這里說明方案二的實(shí)現(xiàn)(目前最常用的轉(zhuǎn)阻塞鎖的方案)
let redis = require("redis");
let client = redis.createClient(config.redisPort,config.serverIp);
let redlock = new Redlock([client]);
let totalTime = 5000;
let key = "to change: as you like";
let intervalTime = 50; // 輪詢時(shí)間間隔
let sleep = function(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
let tag = 0;
for(let j = 0; j<200; j++){
redlock.lock(key, totalTime).then(async function(lock){
if(tag === 1){
lock.unlock().catch(function(err){})
}
else{
tag = 1
// to do
// your operation block
// release lock
lock.unlock().catch(function(err){})
}
}).catch(()=>{})
if(tag === 1){
break
}
await sleep(intervalTime) // 設(shè)置間隔時(shí)間
}
if(tag === 0){
errorMsg = "請求超時(shí)"
return ;
}
3. 效果檢驗(yàn)
在預(yù)約與檢票單項(xiàng)測試的時(shí)候,1000個(gè)人中只有一個(gè)人成功,符合預(yù)期枯跑。
在預(yù)約與更改琴房可用時(shí)間混合測試的時(shí)候惨驶,2000個(gè)人中只有一個(gè)人成功,符合預(yù)期敛助。
以上粗卜,說明這樣加鎖,可以成功解決資源訪問競爭情況纳击。