提示:可跳過(guò)背景信息,直接跳到標(biāo)題三閱讀
一. 分布式鎖使用場(chǎng)景
在服務(wù)器后端程序開(kāi)發(fā)中脯丝,分布式鎖主要用于多臺(tái)機(jī)器的多個(gè)進(jìn)程/線程的并發(fā)執(zhí)行問(wèn)題(處理同一數(shù)據(jù))商膊。比如同時(shí)用戶下單時(shí)多個(gè)并發(fā)請(qǐng)求,進(jìn)行扣減同一商品庫(kù)存操作宠进。
并發(fā)執(zhí)行偽代碼
----------
//1.獲取商品庫(kù)存數(shù)量
$num = getNum($pruduct_id)
//2.庫(kù)存相關(guān)邏輯
if ($num < 10) {
//商品購(gòu)買失敗
return false;
}
//3.扣減庫(kù)存
setNum($num - 1);
----------
上邊偽代碼在并發(fā)執(zhí)行的時(shí)候晕拆,先getNum、再setNum材蹬,這并非一個(gè)原子操作实幕,會(huì)出現(xiàn)同時(shí)獲取到的庫(kù)存數(shù)量都滿足要求,然后都進(jìn)行減庫(kù)存的情況堤器。
二. 并發(fā)問(wèn)題解決方案
本質(zhì)上的解決思路是昆庇,把多個(gè)異步并發(fā)執(zhí)行的請(qǐng)求變?yōu)橥桨错樞驁?zhí)行。
1. 在數(shù)據(jù)庫(kù)層面處理
加鎖將查詢和修改兩條語(yǔ)句合為一個(gè)原子操作闸溃,比如mysql的select ... for update語(yǔ)句整吆。
2. 在應(yīng)用程序?qū)用嫣幚?php/java/go)
一般的,有以下兩種方案:
- 排隊(duì)機(jī)制(異步消息隊(duì)列方案)辉川。將并發(fā)的請(qǐng)求順序入消息隊(duì)列表蝙,然后開(kāi)起一個(gè)單獨(dú)進(jìn)程,逐個(gè)消費(fèi)隊(duì)列內(nèi)容乓旗。
并發(fā)執(zhí)行偽代碼
----------
pushMes(list,'商品1扣減庫(kù)存');
return '商品購(gòu)買中'
----------
單進(jìn)程異步去消費(fèi)隊(duì)列
---------
while(PopMes(list))
{
//1.獲取商品庫(kù)存數(shù)量
$num = getNum($pruduct_id);
//2.庫(kù)存相關(guān)邏輯
if ($num < 10) {
//商品購(gòu)買失敗
return false;
}
//3.扣減庫(kù)存
setNum($num - 1);
//通知購(gòu)買情況
notify();
}
---------
- 爭(zhēng)搶鎖機(jī)制府蛇。多個(gè)請(qǐng)求同時(shí)爭(zhēng)搶一個(gè)分布式的鎖,拿到鎖的請(qǐng)求執(zhí)行完成后釋放鎖屿愚,未拿到鎖的請(qǐng)求循環(huán)sleep一段時(shí)間汇跨,去等待鎖釋放、爭(zhēng)搶鎖妆距。
并發(fā)執(zhí)行偽代碼
----------
times = 0;
while(times < 10) {
//獲取鎖
if (getLock()) {
//1.獲取商品庫(kù)存數(shù)量
$num = getNum($pruduct_id);
//2.庫(kù)存相關(guān)邏輯
if ($num < 10) {
//商品購(gòu)買失敗
return false;
}
//3.扣減庫(kù)存
setNum($num - 1);
//釋放鎖
releaseLock();
return true;
} else {
times = times + 1;
//等待一段時(shí)間
sleep(0.01);
}
}
----------
三. 本文新方案穷遂,分布式非爭(zhēng)搶阻塞鎖(同步隊(duì)列機(jī)制)
1. 概念解讀
- 首先鎖是分布式的
- 阻塞鎖指的是,不能拿到鎖的時(shí)候毅厚,會(huì)阻塞程序的執(zhí)行直至拿到鎖
- 非爭(zhēng)搶指的是塞颁,等待拿鎖的過(guò)程是不用爭(zhēng)搶的,通過(guò)同步隊(duì)列實(shí)現(xiàn)(相對(duì)異步消息隊(duì)列而言)
2. 實(shí)現(xiàn)原理
- 分布式:創(chuàng)建一個(gè)redis隊(duì)列來(lái)存儲(chǔ)一個(gè)key,作為一個(gè)可用鎖胃惜。
- 阻塞非爭(zhēng)搶拿鎖:通過(guò)redis的brpop命令來(lái)阻塞獲取一個(gè)鎖
-
釋放鎖:拿到鎖執(zhí)行完對(duì)應(yīng)業(yè)務(wù)后吼虎,將鎖資源存入redis隊(duì)列
BRPOP 是一個(gè)阻塞的列表彈出原語(yǔ)。 它是 RPOP的阻塞版本贪婉,因?yàn)檫@個(gè)命令會(huì)在給定list無(wú)法彈出任何元素的時(shí)候阻塞連接。關(guān)于redis brpop的非爭(zhēng)搶和阻塞特性的實(shí)現(xiàn),在后邊的文章分析妆棒。
3. 代碼實(shí)現(xiàn)(php)
注: 下面代碼僅為事例代碼,具體應(yīng)用還要考慮其他問(wèn)題。比如加鎖后程序異常退出糕珊,釋放鎖失效的問(wèn)題动分。
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
//商品id
$pruduct_id = 1;
//鎖的名稱
$lock_key = 'lock_' . $pruduct_id;
//產(chǎn)品庫(kù)存在redis中存儲(chǔ)的key
$store_key = 'product_' . $pruduct_id;
//初始設(shè)置商品庫(kù)存為2000
$redis->setnx($store_key, 2000);
//獲取鎖最多阻塞10s
$lock = getLock($lock_key, 10);
//記錄請(qǐng)求數(shù)量
$redis->incr('request_num');
if ($lock) {
$num = $redis->get($store_key);
if (is_numeric($num) && $num > 10) {
//減庫(kù)存
$num--;
$redis->set($store_key, $num);
}
//釋放鎖資源
releaseLock($lock_key);
}
/**
* 阻塞非爭(zhēng)搶獲取一個(gè)鎖
* @param string $key 鎖的名稱
* @param string $timeout 最大阻塞時(shí)間(秒),超過(guò)時(shí)間將不再等待拿鎖
* @return bool 獲取鎖成功/失敗
*/
function getLock($key = 'lock1', $timeout)
{
global $redis;
//第一次請(qǐng)求, 鎖標(biāo)識(shí)不存在的情況红选,直接拿到鎖
$lock = $redis->setnx($key, 1);
if (!$lock) {
//非第一次請(qǐng)求澜公,阻塞等待拿到鎖
$lock = $redis->brpop($key . '_list', $timeout);
}
return (bool)$lock;
}
/**
* 爭(zhēng)搶獲取一個(gè)鎖(使用setnx實(shí)現(xiàn) 拿不到鎖最多重試100次)
* @param string $key 鎖的名稱
* @param string $timeout 最大阻塞時(shí)間(秒),超過(guò)時(shí)間將不再等待拿鎖
* @return bool 獲取鎖成功/失敗
*/
function getLock2($key = 'lock1')
{
global $redis;
$lock = $redis->setnx($key, 1);
if (!$lock) {
for ($i=0; $i < 100; $i++) {
//記錄拿鎖重試次數(shù)
$redis->incr('retry');
usleep(1);
if ($redis->setnx($key, 1)) {
return true;
}
}
//記錄拿鎖失敗次數(shù)
$redis->incr('get_lock_fail');
}
return (bool)$lock;
}
/**
* 釋放鎖
* @param string $key 鎖的名稱
* @return bool 釋放鎖成功/失敗
*/
function releaseLock($key = 'lock1')
{
global $redis;
//返回可用資源到隊(duì)列
$ret = $redis->rpush($key . '_list', 'lock_item1');
return $ret;
}
/**
* 釋放爭(zhēng)搶鎖
* @param string $key 鎖的名稱
* @return bool 釋放鎖成功/失敗
*/
function releaseLock2($key = 'lock1')
{
global $redis;
//刪除鎖
$ret = $redis->del($key);
return $ret;
}
4. 測(cè)試
(1)正確性測(cè)試
使用ab測(cè)試工具喇肋,模擬并發(fā)請(qǐng)求
- 2000個(gè)請(qǐng)求 100并發(fā)
ab -n 2000 -c 100 -k http://127.0.0.1/test.php (linux或mac命令行執(zhí)行)
測(cè)試結(jié)果正確坟乾,一共成功執(zhí)行2000個(gè)請(qǐng)求,庫(kù)存只減到10蝶防。
- 1000個(gè)請(qǐng)求 100并發(fā)
ab -n 1000 -c 100 -k http://127.0.0.1/test.php (linux或mac命令行執(zhí)行)
測(cè)試結(jié)果正確甚侣,一共成功執(zhí)行1000個(gè)請(qǐng)求,庫(kù)存扣減到1000间学。
(2)和redis爭(zhēng)搶鎖對(duì)比測(cè)試
提示:示例代碼中的getLock2殷费、releaseLock2即為爭(zhēng)搶鎖例子
- 效率對(duì)比
兩種加鎖方式,分別ab測(cè)試2000個(gè)請(qǐng)求 100個(gè)并發(fā), php-fpm開(kāi)啟50個(gè)進(jìn)程
ab -n 2000 -c 100 -k http://127.0.0.1/test.php (linux或mac命令行執(zhí)行)
- 爭(zhēng)搶鎖執(zhí)行結(jié)果
將初始庫(kù)存改為3000
ab -n 2000 -c 100 -k http://127.0.0.1/test.php (linux或mac命令行執(zhí)行)
四. 最后
本文是提供了一個(gè)新的思路低葫,不完善的地方歡迎在評(píng)論區(qū)討論详羡。
五. 廣告
云服務(wù)器練手推薦
3月份騰訊云在打折促銷,新用戶1核2G云服務(wù)器99/年氮采,非新用戶可以注冊(cè)新賬號(hào)或者續(xù)費(fèi)也有優(yōu)惠殷绍。沒(méi)有云服務(wù)器的同學(xué)可以趁著打折去來(lái)一臺(tái)