Redis隊列實現Java版秒殺系統(無腳本、可用于生產)
寫在前面
需求是做一個秒殺系統,比如大家來搶100臺手機,先到先得。
查閱了網上很多用redis實現秒殺的demo(java語言),竟然沒一個能用的!!!
有些是php的,沒閑心研究了,現在說說為什么不能用:
- 絕大多數的DEMO都是基于redis的watch特性的事務實現①,
- 個別是基于redis分布式鎖實現②。
- 當然還有些用了腳本的,我也沒仔細看是lua還是調用redis指令,哪有那個閑心去研究哇。
照顧一下小白,分析一下為什么這幾種實現不行
1.基于watch特性的 不靠譜 實現
其實這兩種實現方式,完全可以理解為樂觀鎖(watch)和悲觀鎖(加分布式鎖)
watch事務,相當于是樂觀鎖,這種方法在并發情況下極為不靠譜,假設有100個人同時嘗試秒殺,那么極端情況下,有99個人都會失敗,只有一個能修改成功。
然而demo里甚至沒寫如果修改失敗了就重試這個功能,那顯然這失敗的99個人,已經提示失敗了,過一會回來,發現還剩了90多。那我是怎么失敗的?我替他們問問了。
并且使用這種方式實現呢,在并發量較大的時候,過多的重試線程應該會嚴重影響服務器性能。
2.基于用redis做個分布式鎖的 不靠譜 實現
這種實現方式相當于一個悲觀鎖,每次執行減減操作之前,在redis中存入一個k,v鍵值對,使用特定的名稱,并且使用setNX特性,確保搶鎖沒有安全問題,并在使用完成后釋放鎖。那么問題是,在100個人秒殺時,只有一個人搶到鎖,剩下99個人怎么辦?
demo里同樣沒寫個重試,搶不到鎖就失敗,醉了,不過就算寫重新搶鎖的機制,那么幾十個上百個線程不斷搶鎖,想想是個挺恐怖的事,更別提高并發了。
基于腳本的實現 不靠譜 實現
作為一個C系語言開發,我看不太懂,看不懂就是不靠譜,出了問題都不知道改哪里,你說靠不靠譜
正題:使用spring操作redis的list隊列實現
我用的是springboot的StringRedisTemplate,至于如何整合jedis到spring等等,去查閱其他文章吧,我就不重復寫了。
貼工具類:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Collection;
@Service
public class RedisServiceImpl<T> implements RedisService<T> {
@Autowired
private StringRedisTemplate stringRedisTemplate;
//添加字符串并設置過期時間
@Override
public void addString(String key, String value, Duration duration) {
stringRedisTemplate.opsForValue().set(key, value, duration);
}
//查找字符串
@Override
public String findString(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
//根據Key刪除
@Override
public Boolean deleteByKey(String key) {
return stringRedisTemplate.delete(key);
}
//在隊列尾部減少一個對象
@Override
public String removeOneEntryOnListRight(String listName) {
return stringRedisTemplate.opsForList().rightPop(listName);
}
//在隊列頭部新增對象
@Override
public Long addEntriesOnListLeft(String listName, Collection args) {
return stringRedisTemplate.opsForList().leftPushAll(listName, args);
}
}
解釋一下哈 這個類的父類是我自己寫的service層,不是提供好的
主要使用的是最后兩個方法,最后一個方法,在隊列頭部新增對象,如果沒有這個隊列,他會創建出來這個隊列,然后將一個集合統統塞到這個redis隊列中。倒數第二個方法每調用一次,會刪除隊列中最后一個元素,然后返回這個元素的值,如果隊列中已經沒有元素了(隊列已經沒了)那么他會返回null,他們都是原子操作。
如此,每個請求都無需經過加鎖操作,直接利用redis的單線程特性,即可實現高并發下的秒殺:請求到達redis,redis會逐個執行,每一次執行要么返回一個值,要么返回null。很顯然,返回值的就是搶到了,返回null的就是沒搶到。而且可以靈活的為這個隊列新加入一些元素(老板發話再加100臺)或者直接把這個隊列刪了(老板說不行,不賣了)都不會對代碼產生任何影響。
其中對應的redis操作指令分別是:
- 在隊列左側新增:lpush
- 在隊列右側消費:rpop
老板不賣了:del (笑)
接下來貼出十分簡單的使用方法
先貼在任務開始時向redis中插入一個大隊列
List entriesList = new LinkedList<>();
for (int i = 0; i < 100; i++){
entriesList.add("某個商品");
}
redisService.addEntriesOnListLeft("隊列名",entriesList);
突然想到這個實現即使秒殺100臺不同型號的手機(并且在秒到時就通知用戶秒到的是啥),也不用改代碼。
每次秒殺執行:
String redisResult = redisService.removeOneEntryOnListRight("隊列名");
if (null == redisResult) {
//說明沒搶到
}else{
//說明搶到了 執行搶到邏輯
}
突然發現這個實現看起來甚至比那些所謂的秒殺demo還簡單
但他既沒有并發問題,也沒有為了解決并發問題而衍生的性能問題。
雖然沒經過測試,不過我認為就算秒殺10萬臺,放到redis隊列里,應該也占用不了多少內存。
趕工分布式秒殺,沒想到如此基本的內容,竟沒找到一個靠譜的實現,從上午寫到現在(周末晚8點)一頓飯沒吃,被網上過于demo的資源逼的,忍餓怒出此文。