Redis專題-併發/秒殺
開局一張圖,內容全靠“編”。
昨天晚上在羣友裏看到有人在討論庫存併發的問題,看到這裏我就決定寫一篇關於redis秒殺的文章。
1、理論部分
我們看看一般我們庫存是怎麼出問題的
其實redis提供了兩種解決方案:加鎖和原子操作。
1.1、加鎖
加鎖:其實非常常見,讀取數據前,客戶端先獲取鎖,再操作。
當客戶端獲得鎖後,一直持有直到客戶端完成操作,再釋放。
怎麼操作呢,客戶端使用分佈式鎖來獲取鎖,(使用redis或者zookeeper來實現一個分佈式鎖)以商品的維度來加鎖,在獲取到鎖的線程中,按順序執行商品的庫存查詢和扣減,同時實現了順序性和原子性。
但是,但是,有問題:
1、如果使用redis來實現分佈式鎖,那麼鎖的時效性是個問題。太短了,業務還沒跑完鎖就釋放了。太長了,如果異常,其他業務就一直阻塞等着自動釋放。
2、如果使用zookeeper,確實不用擔心鎖釋放問題(臨時節點),而且一致性好,但是性能不高。ZK中創建和刪除節點只能通過Leader服務器來執行,然後Leader服務器還需要將數據同不到所有的Follower機器上,這樣頻繁的網絡通信,性能的短板是非常突出的。(挖坑😂後續寫一個redis和zookeeper實現分佈式鎖的文章)
所以。。繼續往下看。。
1.2、原子操作
原子操作:執行過程中保持原子性操作,而原子性操作是不需要加鎖的,也就是無鎖操作。所以既保證了併發也不會減少系統併發性能。
redis的原子操作其實也有兩種方式:
1、單命令操作:多個操作在redis中一個操作完成
2、lua:多個操作寫成lua腳本,以原子性方式執行單個lua腳本
1.2.1、INCR/DECR
Redis 是使用單線程來串行處理客戶端的請求操作命令的,所以,當 Redis 執行某個命令操作時,其他命令是無法執行的,這相當於命令操作是互斥執行的。
Redis 的單個命令操作可以原子性地執行,但是在實際應用中,數據修改時可能包含多個操作,至少包括讀數據、數據增減、寫回數據三個操作,這顯然就不是單個命令操作了,那該怎麼辦呢?
Redis提供INCR/DECR,將讀數據、數據增減、寫回數據三個操作合併爲了一個,可以對數據進行增值 / 減值操作,而且它們本身就是單個命令操作,所以本身具有互斥性。可以直接幫助我們進行併發控制。
// 將商量id的庫存減1
DECR id
是的,就是這麼簡單就搞定了扣減庫存。
1.2.2、Lua腳本
Redis 會把整個 Lua 腳本作爲一個整體執行,在執行的過程中不會被其他命令打斷,從而保證了 Lua 腳本中操作的原子性。
將要執行的操作編寫到一個 Lua 腳本中,使用 Redis 的 EVAL 命令來執行腳本。
原生 EVAL 方法的使用語法如下:
EVAL script numkeys key [key ...] arg [arg ...]
script 是我們 Lua 腳本的字符串形式,numkeys 是我們要傳入的參數數量,key 是我們的入參,可以傳入多個,arg 是額外的入參。
但這種方式需要每次都傳入 Lua 腳本字符串,不僅浪費網絡開銷,同時 Redis 需要每次重新編譯 Lua 腳本,對於我們追求性能極限的系統來說,不是很完美。所以這裏就要說到另一個命令 EVALSHA 了,原生語法如下:
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
可以看到其語法與 EVAL 類似,不同的是這裏傳入的不是腳本字符串,而是一個加密串 sha1。這個 sha1 是從哪來的呢?它是通過另一個命令 SCRIPT LOAD 返回的,該命令是預加載腳本用的,語法爲:
SCRIPT LOAD script
將 Lua 腳本先存儲在 Redis 中,並返回一個 sha1,下次要執行對應腳本時,只需要傳入 sha1 即可執行對應的腳本。這完美地解決了 EVAL 命令存在的弊端,所以我們這裏也是基於 EVALSHA 方式來實現的。
-- 調用Redis的get指令,查詢活動庫存,其中KEYS[1]爲傳入的參數1,即庫存key
local c_s = redis.call('get', KEYS[1])
-- 判斷活動庫存是否充足,其中KEYS[2]爲傳入的參數2,即當前搶購數量
if not c_s or tonumber(c_s) < tonumber(KEYS[2]) then
return 0
end
-- 如果活動庫存充足,則進行扣減操作。其中KEYS[2]爲傳入的參數2,即當前搶購數量
redis.call('decrby',KEYS[1], KEYS[2])
return 1
我們可以將腳本先寫在配置中心,代碼執行的時候就去拉取最新的sha1。或者代碼裏面寫死。
當然這個腳本也可以擴展,比如加上IP限制等等。但是太多操作放在Lua裏也會降低redis的併發性能,所以非併發控制就不寫到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秒內跑完
家人們!準備開槍!3!2!1!上鍊接!😆😆😆
讓我們恭喜這5位大冤種😏
Jmeter聚合報告
redis庫存爲0
好了,到這裏就先結束了。拜拜
github StackExchange
手把手帶你搭建秒殺系統-不差毫釐:秒殺的庫存與限購
Redis 核心技術與實戰-無鎖的原子操作:Redis如何應對併發訪問?
.Net Core使用分佈式緩存Redis:Lua腳本