redis的鎖機制和秒殺問題實踐
最近學習了redis的鎖機制並且進行了秒殺案例解決超賣的實踐
multi/exec/discard
Redis Multi 命令用於標記一個事務塊的開始。
事務塊內的多條命令會按照先後順序被放進一個隊列當中,最後由 EXEC 命令原子性(atomic)地執行。
總結的說,redis是單執行緒的,每一 個命令都是在佇列中順序執行的,而Multi會讓一系列指令按照順序執行,且不允許其他指令插隊,讓這一串指令具備了原子性
multi:開啟一個事務,接下來輸入的指令(本客戶端內)將不會返回值,直到呼叫了exec命令
exec: 執行一個事務,返回陣列,即multi後所有指令的返回值
discard:拋棄這個事務
樂觀鎖與watch命令
redis的鎖機制是基於樂觀鎖,樂觀鎖即為一個欄位增加了版本號,每次更新都會原子性地把版本號加一,監聽這個版本號後如果發現最終更新與這個版本號不一致將會不執行這次更新。
watch與multi
redis中watch命令是用樂觀鎖實現的鎖機制,需要和multi命令配合使用。
使用方法:
watch key
multi
command 1
command 2
...
exec
效果:watch key即用樂觀鎖監聽這個key,如果multi中的指令執行時會修改這個key,若發現key被別人修改過就會放棄這次事務。總而言之就是watch和multi之間key被修改就會放棄這次事務。
秒殺問題的解決
背景
為了加快搶購速度,避免資料庫系統壓力瞬間增加,會在搶購時將資料庫中商品庫存讀入redis,所有搶購操作在redis中操作,這個小demo就是模擬redis中搶購的過程。
設計思路
使用者從redis中讀取商品餘額,如果不存在key則說明搶購還沒開始。開始搶購時向redis中新增商品餘額,使用者搶購到商品後庫存減一,並且將該使用者的id加入搶購名單set集合中,我的程式碼是由redisTemplate寫的
可能出現的問題有:
- 超賣:如果有兩個執行緒同時進入了減少庫存的程式碼且沒做處理就可能導致庫存賣到負值
- 重複買:同一個使用者連續傳送多個請求且同時進入了購買邏輯
- 剩餘庫存:如果用樂觀鎖做了控制導致使用者很容易就搶購失敗了,使用者體驗會很糟糕。這裡用自旋的方式降低搶購失敗發生的概率
程式碼如下
@PostMapping("/kill")
public String testSecKill(@RequestBody SecKillDto secKillDto) {
Integer prodId = secKillDto.getProdId();
Integer userId = secKillDto.getUserId();
String prodKey = "sk:prod:" + prodId + ":int";
String userKey = String.format("sk:buylist:%d:set:int", prodId);
Integer prodNum = (Integer) redisTemplate.boundValueOps(prodKey).get();
if(prodNum == null) {
return "還沒有開始";
}
for(int i = 0; i < casTime; i++) {
Object exec = redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
operations.watch(prodKey);
if(redisTemplate.boundSetOps(userKey).isMember(userId)) {
return "不能重複搶";
}
if(((Integer) redisTemplate.boundValueOps(prodKey).get()).compareTo(0) <= 0) {
return "已經搶光了";
}
operations.multi();
operations.boundValueOps(prodKey).decrement();
operations.boundSetOps(userKey).add(userId);
List exec1 = operations.exec();
return exec1;
}
});
if(exec instanceof List && (exec == null || ((List)exec).size() == 0)) {
System.out.println("失敗");
} else if(exec instanceof String) {
return (String) exec;
} else {
return "搶購成功";
}
}
return "搶購失敗";
}
說明:一定要在watch後判斷是否重複搶和是否搶光,因為如果在watch之前做判斷,判斷和watch之間可能值被減少,實際watch的值比判斷的值要小,導致有可能watch的就是0。watch之後再判斷可以保證watch之後如果是0就不執行
使用JMeter做壓測
1. 下載JMeter並解壓
http://jmeter.apache.org/download_jmeter.cgi
2. 新建測試
開啟JMeter的GUI,即JMeter.bat,新建測試,就不多贅述了
壓測後看看結果,有沒有超賣或者重複搶購,應該是沒有的