提示:可跳過背景信息,直接跳到標(biāo)題三閱讀
一. 分布式鎖使用場景
在服務(wù)器后端程序開發(fā)中,分布式鎖主要用于多臺機(jī)器的多個進(jìn)程/線程的并發(fā)執(zhí)行問題(處理同一數(shù)據(jù))。比如同時用戶下單時多個并發(fā)請求,進(jìn)行扣減同一商品庫存操作。
并發(fā)執(zhí)行偽代碼
----------
//1.獲取商品庫存數(shù)量
$num = getNum($pruduct_id)
//2.庫存相關(guān)邏輯
if ($num < 10) {
//商品購買失敗
return false;
}
//3.扣減庫存
setNum($num - 1);
----------
上邊偽代碼在并發(fā)執(zhí)行的時候,先getNum、再setNum,這并非一個原子操作,會出現(xiàn)同時獲取到的庫存數(shù)量都滿足要求,然后都進(jìn)行減庫存的情況。
二. 并發(fā)問題解決方案
本質(zhì)上的解決思路是,把多個異步并發(fā)執(zhí)行的請求變?yōu)橥桨错樞驁?zhí)行。
1. 在數(shù)據(jù)庫層面處理
加鎖將查詢和修改兩條語句合為一個原子操作,比如mysql的select ... for update語句。
2. 在應(yīng)用程序?qū)用嫣幚?php/java/go)
一般的,有以下兩種方案:
- 排隊機(jī)制(異步消息隊列方案)。將并發(fā)的請求順序入消息隊列,然后開起一個單獨進(jìn)程,逐個消費隊列內(nèi)容。
并發(fā)執(zhí)行偽代碼
----------
pushMes(list,'商品1扣減庫存');
return '商品購買中'
----------
單進(jìn)程異步去消費隊列
---------
while(PopMes(list))
{
//1.獲取商品庫存數(shù)量
$num = getNum($pruduct_id);
//2.庫存相關(guān)邏輯
if ($num < 10) {
//商品購買失敗
return false;
}
//3.扣減庫存
setNum($num - 1);
//通知購買情況
notify();
}
---------
- 爭搶鎖機(jī)制。多個請求同時爭搶一個分布式的鎖,拿到鎖的請求執(zhí)行完成后釋放鎖,未拿到鎖的請求循環(huán)sleep一段時間,去等待鎖釋放、爭搶鎖。
并發(fā)執(zhí)行偽代碼
----------
times = 0;
while(times < 10) {
//獲取鎖
if (getLock()) {
//1.獲取商品庫存數(shù)量
$num = getNum($pruduct_id);
//2.庫存相關(guān)邏輯
if ($num < 10) {
//商品購買失敗
return false;
}
//3.扣減庫存
setNum($num - 1);
//釋放鎖
releaseLock();
return true;
} else {
times = times + 1;
//等待一段時間
sleep(0.01);
}
}
----------
三. 本文新方案,分布式非爭搶阻塞鎖(同步隊列機(jī)制)
1. 概念解讀
- 首先鎖是分布式的
- 阻塞鎖指的是,不能拿到鎖的時候,會阻塞程序的執(zhí)行直至拿到鎖
- 非爭搶指的是,等待拿鎖的過程是不用爭搶的,通過同步隊列實現(xiàn)(相對異步消息隊列而言)
2. 實現(xiàn)原理
- 分布式:創(chuàng)建一個redis隊列來存儲一個key,作為一個可用鎖。
- 阻塞非爭搶拿鎖:通過redis的brpop命令來阻塞獲取一個鎖
-
釋放鎖:拿到鎖執(zhí)行完對應(yīng)業(yè)務(wù)后,將鎖資源存入redis隊列
BRPOP 是一個阻塞的列表彈出原語。 它是 RPOP的阻塞版本,因為這個命令會在給定list無法彈出任何元素的時候阻塞連接。關(guān)于redis brpop的非爭搶和阻塞特性的實現(xiàn),在后邊的文章分析。
3. 代碼實現(xiàn)(php)
注: 下面代碼僅為事例代碼,具體應(yīng)用還要考慮其他問題。比如加鎖后程序異常退出,釋放鎖失效的問題。
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
//商品id
$pruduct_id = 1;
//鎖的名稱
$lock_key = 'lock_' . $pruduct_id;
//產(chǎn)品庫存在redis中存儲的key
$store_key = 'product_' . $pruduct_id;
//初始設(shè)置商品庫存為2000
$redis->setnx($store_key, 2000);
//獲取鎖最多阻塞10s
$lock = getLock($lock_key, 10);
//記錄請求數(shù)量
$redis->incr('request_num');
if ($lock) {
$num = $redis->get($store_key);
if (is_numeric($num) && $num > 10) {
//減庫存
$num--;
$redis->set($store_key, $num);
}
//釋放鎖資源
releaseLock($lock_key);
}
/**
* 阻塞非爭搶獲取一個鎖
* @param string $key 鎖的名稱
* @param string $timeout 最大阻塞時間(秒),超過時間將不再等待拿鎖
* @return bool 獲取鎖成功/失敗
*/
function getLock($key = 'lock1', $timeout)
{
global $redis;
//第一次請求, 鎖標(biāo)識不存在的情況,直接拿到鎖
$lock = $redis->setnx($key, 1);
if (!$lock) {
//非第一次請求,阻塞等待拿到鎖
$lock = $redis->brpop($key . '_list', $timeout);
}
return (bool)$lock;
}
/**
* 爭搶獲取一個鎖(使用setnx實現(xiàn) 拿不到鎖最多重試100次)
* @param string $key 鎖的名稱
* @param string $timeout 最大阻塞時間(秒),超過時間將不再等待拿鎖
* @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;
//返回可用資源到隊列
$ret = $redis->rpush($key . '_list', 'lock_item1');
return $ret;
}
/**
* 釋放爭搶鎖
* @param string $key 鎖的名稱
* @return bool 釋放鎖成功/失敗
*/
function releaseLock2($key = 'lock1')
{
global $redis;
//刪除鎖
$ret = $redis->del($key);
return $ret;
}
4. 測試
(1)正確性測試
使用ab測試工具,模擬并發(fā)請求
- 2000個請求 100并發(fā)
ab -n 2000 -c 100 -k http://127.0.0.1/test.php (linux或mac命令行執(zhí)行)

測試結(jié)果正確,一共成功執(zhí)行2000個請求,庫存只減到10。
- 1000個請求 100并發(fā)
ab -n 1000 -c 100 -k http://127.0.0.1/test.php (linux或mac命令行執(zhí)行)

測試結(jié)果正確,一共成功執(zhí)行1000個請求,庫存扣減到1000。
(2)和redis爭搶鎖對比測試
提示:示例代碼中的getLock2、releaseLock2即為爭搶鎖例子
- 效率對比
兩種加鎖方式,分別ab測試2000個請求 100個并發(fā), php-fpm開啟50個進(jìn)程
ab -n 2000 -c 100 -k http://127.0.0.1/test.php (linux或mac命令行執(zhí)行)

- 爭搶鎖執(zhí)行結(jié)果
將初始庫存改為3000
ab -n 2000 -c 100 -k http://127.0.0.1/test.php (linux或mac命令行執(zhí)行)

四. 最后
本文是提供了一個新的思路,不完善的地方歡迎在評論區(qū)討論。
五. 廣告
云服務(wù)器練手推薦
3月份騰訊云在打折促銷,新用戶1核2G云服務(wù)器99/年,非新用戶可以注冊新賬號或者續(xù)費也有優(yōu)惠。沒有云服務(wù)器的同學(xué)可以趁著打折去來一臺

