1. 程式人生 > 其它 >Redis佇列實現Java版秒殺系統(無指令碼、可用於生產)

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裡同樣沒寫個重試,搶不到鎖就失敗,醉了,不過就算寫重新搶鎖的機制,那麼幾十個上百個執行緒不斷搶鎖,想想是個挺恐怖的事,更別提高併發了。

正題:使用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<String> args) {
        return stringRedisTemplate.opsForList().leftPushAll(listName, args);
    }

}

  

解釋一下哈 這個類的父類是我自己寫的service層,不是提供好的

主要使用的是最後兩個方法,最後一個方法,在佇列頭部新增物件,如果沒有這個佇列,他會創建出來這個佇列,然後將一個集合統統塞到這個redis佇列中。倒數第二個方法每呼叫一次,會刪除佇列中最後一個元素,然後返回這個元素的值,如果佇列中已經沒有元素了(佇列已經沒了)那麼他會返回null,他們都是原子操作。

如此,每個請求都無需經過加鎖操作,直接利用redis的單執行緒特性,即可實現高併發下的秒殺:請求到達redis,redis會逐個執行,每一次執行要麼返回一個值,要麼返回null。很顯然,返回值的就是搶到了,返回null的就是沒搶到。而且可以靈活的為這個佇列新加入一些元素(老闆發話再加100臺)或者直接把這個佇列刪了(老闆說不行,不賣了)都不會對程式碼產生任何影響。

其中對應的redis操作指令分別是:

  • 在佇列左側新增:lpush
  • 在佇列右側消費:rpop

老闆不賣了:del (笑)

接下來貼出十分簡單的使用方法

先貼在任務開始時向redis中插入一個大佇列

List<String> 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的資源逼的,忍餓怒出此文。