Redis專題-并發(fā)/秒殺
開局一張圖,內(nèi)容全靠“編”。
昨天晚上在群友里看到有人在討論庫存并發(fā)的問題,看到這里我就決定寫一篇關(guān)于redis秒殺的文章。
1、理論部分
我們看看一般我們庫存是怎么出問題的
其實redis提供了兩種解決方案:加鎖和原子操作。
1.1、加鎖
加鎖:其實非常常見,讀取數(shù)據(jù)前,客戶端先獲取鎖,再操作。
當(dāng)客戶端獲得鎖后,一直持有直到客戶端完成操作,再釋放。
怎么操作呢,客戶端使用分布式鎖來獲取鎖,(使用redis或者zookeeper來實現(xiàn)一個分布式鎖)以商品的維度來加鎖,在獲取到鎖的線程中,按順序執(zhí)行商品的庫存查詢和扣減,同時實現(xiàn)了順序性和原子性。
但是,但是,有問題:
1、如果使用redis來實現(xiàn)分布式鎖,那么鎖的時效性是個問題。太短了,業(yè)務(wù)還沒跑完鎖就釋放了。太長了,如果異常,其他業(yè)務(wù)就一直阻塞等著自動釋放。
2、如果使用zookeeper,確實不用擔(dān)心鎖釋放問題(臨時節(jié)點),而且一致性好,但是性能不高。ZK中創(chuàng)建和刪除節(jié)點只能通過Leader服務(wù)器來執(zhí)行,然后Leader服務(wù)器還需要將數(shù)據(jù)同不到所有的Follower機器上,這樣頻繁的網(wǎng)絡(luò)通信,性能的短板是非常突出的。(挖坑??后續(xù)寫一個redis和zookeeper實現(xiàn)分布式鎖的文章)
所以。。繼續(xù)往下看。。
1.2、原子操作
原子操作:執(zhí)行過程中保持原子性操作,而原子性操作是不需要加鎖的,也就是無鎖操作。所以既保證了并發(fā)也不會減少系統(tǒng)并發(fā)性能。
redis的原子操作其實也有兩種方式:
1、單命令操作:多個操作在redis中一個操作完成
2、lua:多個操作寫成lua腳本,以原子性方式執(zhí)行單個lua腳本
1.2.1、INCR/DECR
Redis 是使用單線程來串行處理客戶端的請求操作命令的,所以,當(dāng) Redis 執(zhí)行某個命令操作時,其他命令是無法執(zhí)行的,這相當(dāng)于命令操作是互斥執(zhí)行的。
Redis 的單個命令操作可以原子性地執(zhí)行,但是在實際應(yīng)用中,數(shù)據(jù)修改時可能包含多個操作,至少包括讀數(shù)據(jù)、數(shù)據(jù)增減、寫回數(shù)據(jù)三個操作,這顯然就不是單個命令操作了,那該怎么辦呢?
Redis提供INCR/DECR,將讀數(shù)據(jù)、數(shù)據(jù)增減、寫回數(shù)據(jù)三個操作合并為了一個,可以對數(shù)據(jù)進行增值 / 減值操作,而且它們本身就是單個命令操作,所以本身具有互斥性??梢灾苯訋椭覀冞M行并發(fā)控制。
// 將商量id的庫存減1
DECR id
是的,就是這么簡單就搞定了扣減庫存。
1.2.2、Lua腳本
Redis 會把整個 Lua 腳本作為一個整體執(zhí)行,在執(zhí)行的過程中不會被其他命令打斷,從而保證了 Lua 腳本中操作的原子性。
將要執(zhí)行的操作編寫到一個 Lua 腳本中,使用 Redis 的 EVAL 命令來執(zhí)行腳本。
原生 EVAL 方法的使用語法如下:
EVAL script numkeys key [key ...] arg [arg ...]
script 是我們 Lua 腳本的字符串形式,numkeys 是我們要傳入的參數(shù)數(shù)量,key 是我們的入?yún)ⅲ梢詡魅攵鄠€,arg 是額外的入?yún)ⅰ?/p>
但這種方式需要每次都傳入 Lua 腳本字符串,不僅浪費網(wǎng)絡(luò)開銷,同時 Redis 需要每次重新編譯 Lua 腳本,對于我們追求性能極限的系統(tǒng)來說,不是很完美。所以這里就要說到另一個命令 EVALSHA 了,原生語法如下:
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
可以看到其語法與 EVAL 類似,不同的是這里傳入的不是腳本字符串,而是一個加密串 sha1。這個 sha1 是從哪來的呢?它是通過另一個命令 SCRIPT LOAD 返回的,該命令是預(yù)加載腳本用的,語法為:
SCRIPT LOAD script
將 Lua 腳本先存儲在 Redis 中,并返回一個 sha1,下次要執(zhí)行對應(yīng)腳本時,只需要傳入 sha1 即可執(zhí)行對應(yīng)的腳本。這完美地解決了 EVAL 命令存在的弊端,所以我們這里也是基于 EVALSHA 方式來實現(xiàn)的。
-- 調(diào)用Redis的get指令,查詢活動庫存,其中KEYS[1]為傳入的參數(shù)1,即庫存key
local c_s = redis.call('get', KEYS[1])
-- 判斷活動庫存是否充足,其中KEYS[2]為傳入的參數(shù)2,即當(dāng)前搶購數(shù)量
if not c_s or tonumber(c_s) < tonumber(KEYS[2]) then
return 0
end
-- 如果活動庫存充足,則進行扣減操作。其中KEYS[2]為傳入的參數(shù)2,即當(dāng)前搶購數(shù)量
redis.call('decrby',KEYS[1], KEYS[2])
return 1
我們可以將腳本先寫在配置中心,代碼執(zhí)行的時候就去拉取最新的sha1?;蛘叽a里面寫死。
當(dāng)然這個腳本也可以擴展,比如加上IP限制等等。但是太多操作放在Lua里也會降低redis的并發(fā)性能,所以非并發(fā)控制就不寫到lua了。
理論看完了,實操一下吧
2、Talk is cheap. Show me the code
2.1、安裝redis
跳過,不會安裝的出門右拐。
我自己用podman。
podman run -p 6379:6379 --name my_redis --privileged=true -v D:\podman\redis\conf\redis.conf:/etc/redis/redis.conf -v D:\podman\redis\data:/data -d docker.io/library/redis redis-server /etc/redis/redis.conf --appendonly yes
2.2、代碼
在下.neter,就寫C#代碼了
[ApiController]
[Route("[controller]")]
public class HomeController : ControllerBase
{
private static string _redisConnection = "localhost:6379";
private static ConnectionMultiplexer _connMultiplexer;
private string _redisScript = @"local c_s = redis.call('get', KEYS[1])
if not c_s or tonumber(c_s) < tonumber(KEYS[2]) then
return 0
end
redis.call('decrby',KEYS[1], KEYS[2])
return 1";
private string _sha1 = string.Empty;
/// <summary>
/// 鎖
/// </summary>
private static readonly object Locker = new object();
private static int _count = 0;
private static int _rushToPurchaseCount = 0;
/// <summary>
/// 獲取 Redis 連接對象
/// </summary>
/// <returns></returns>
private IConnectionMultiplexer GetConnectionRedisMultiplexer()
{
if ((_connMultiplexer == null) || !_connMultiplexer.IsConnected)
{
lock (Locker)
{
if ((_connMultiplexer == null) || !_connMultiplexer.IsConnected)
{
_connMultiplexer = ConnectionMultiplexer.Connect(_redisConnection);
}
}
}
return _connMultiplexer;
}
[HttpPost("/Init")]
public IActionResult Init()
{
GetConnectionRedisMultiplexer();
return Ok();
}
[HttpPost]
public async Task<IActionResult> Post()
{
System.Diagnostics.Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
var db = _connMultiplexer.GetDatabase();
var cache = db.ScriptEvaluateAsync(_redisScript,
new RedisKey[] { "key999", "1" });
var results = (string[]?)await cache;
if (results[0] == "1")
{
Interlocked.Increment(ref _rushToPurchaseCount);
Console.WriteLine($"恭喜您搶到了,{_rushToPurchaseCount}");
}
else
{
Console.WriteLine("很遺憾,您沒有搶到");
}
return Ok();
}
}
我們在redis中新增5個庫存
配置一下Jmeter,100個線程3秒內(nèi)跑完
家人們!準備開槍!3!2!1!上鏈接!??????
讓我們恭喜這5位大冤種??
Jmeter聚合報告
redis庫存為0
好了,到這里就先結(jié)束了。拜拜文章來源:http://www.zghlxwxcb.cn/news/detail-659890.html
github StackExchange手把手帶你搭建秒殺系統(tǒng)-不差毫厘:秒殺的庫存與限購Redis 核心技術(shù)與實戰(zhàn)-無鎖的原子操作:Redis如何應(yīng)對并發(fā)訪問?.Net Core使用分布式緩存Redis:Lua腳本文章來源地址http://www.zghlxwxcb.cn/news/detail-659890.html
到了這里,關(guān)于Redis專題-秒殺的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!