redis lua腳本實戰秒殺和減庫存的實現
前言
我們都知道redis是高性能高並發系統必不可少的kv中間件,它以高性能,高並發著稱,我們常常用它做緩存,將熱點數據或者是萬年不變的數據緩存到redis中,查詢的時候直接查詢redis,減輕db的壓力,分佈式系統中我們也會拿它來做分佈式鎖,分佈式id,冪等來解決一些分佈式問題,redis也支持lua腳本,而且能夠保證lua腳本執行過程中原子性,這就使得它的應用場景很多,也很典型,在redisson這個redis客戶端中,它的各種分佈式鎖底層就是使用lua來實現的。本文主要是學習一下redis lua腳本的編寫,以及在redisson這個redis客戶端中是怎樣使用的,實戰一下秒殺場景redis減庫存lua腳本的編寫,並偽真實環境壓測查看效果。
1.redisson介紹
redisson是一個redis的客戶端,它擁有豐富的功能,而且支持redis的各種模式,什麼單機,集群,哨兵的都支持,各種各樣的分佈式鎖實現,什麼分佈式重入鎖,紅鎖,讀寫鎖,信號量的,然後它操作redis的常用數據結構就跟操作jdk的各種集合一樣簡單。
這裡我們稍微演示下,不做過多的介紹,api畢竟隻是個api,有意思的都在它背後各種原理。
maven依賴
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.8.1</version> </dependency>
創建RedissonClient對象
Config config = new Config(); config.useSingleServer() .setAddress("redis://xxx:xx"); RedissonClient redissonClient = Redisson.create(config);
它的功能超級多,下面隻是列舉瞭一些常用的,還有什麼Bloom過濾器,隊列等等。
// string redissonClient.getBucket("name").set("zhangsan"); redissonClient.getBucket("name").get(); // hash RMap<Object, Object> user = redissonClient.getMap("user"); user.put("name","zhangsan"); user.put("age",11); // list RList<String> names = redissonClient.getList("names"); names.add("zhangsan"); names.add("lisi"); names.add("wangwu"); // set RSet<Object> nameSet = redissonClient.getSet("names"); nameSet.add("lisi"); nameSet.add("lisi"); //lock RLock lock = redissonClient.getLock("lock"); lock.lock(); lock.unlock(); // 分佈式id RAtomicLong id = redissonClient.getAtomicLong("id"); long l = id.incrementAndGet();
2. redis lua腳本編寫與執行
其實redis中的lua腳本並不難,你也不需要把lua語言再去重學一遍,全憑感覺就好瞭,使用的時候去查下語法就ok瞭。
腳本中就一個redis.call() 應該算是函數吧(方法也可以),比如我要使用lua腳本實現set動作,就可以這樣寫
return redis.call('set','name','zhangsan');
其實就是跟redis交互命令一個樣子,再使用lua語言做一些條件分支,循環啥的,就完成瞭一些稍微復雜的邏輯。
下面自己寫一遍扣減庫存的邏輯,你就會這個玩意瞭。
上面是介紹瞭lua腳本的編寫,下面我們介紹下這個執行。
不管是redis自己帶的那個客戶端,還是jedis,jediscluster,redistemplate,redisson這些客戶端都是支持lua腳本api的,其實就是eval,evalsha,scriptload 這幾個命令用的比較頻繁,我這裡把菜鳥教程上面關於介紹redis腳本命令截圖過來
多說無益,這裡直接使用redisson客戶端實踐一下。
public class RedisLua { private static final Config config ; private static final RedissonClient redisson ; static { config = new Config(); config.useSingleServer() .setAddress("redis://ip:port"); redisson = Redisson.create(config); } public static void main(String[] args) throws InterruptedException { redisson.getBucket("name").set(11); RScript script = redisson.getScript(); String result = script.eval(RScript.Mode.READ_ONLY, new StringCodec(),"return redis.call('get','name');", RScript.ReturnType.VALUE); System.out.println(result); } }
可以看到就是使用getScript方法獲取一個script對象,然後調用script 對象的eval方法,這個script對象其實還是用好多個方法的,evalSha等等,可以自己研究下。然後就是通過lua腳本獲取我上面set進去的name值。
我們在看上面菜鳥教程對腳本命令的介紹的時候,還發現有key… arg…這些東西,這個我認為就是動態替換(傳參)的。
比如我現在不獲取name這個key的值瞭,我要獲取age 的,或者是我要直接set一個值,這個時候我就可以lua腳本中有兩個變量與你傳的參數對等起來
KEYS[1] ARGV[1]
你可以看作是數組,不過它的位置是從1開始的。
RScript script = redisson.getScript(); List<Object> keys=new ArrayList<>(); keys.add("age"); String re = script.eval( RScript.Mode.READ_WRITE, new StringCodec(), "return redis.call('set',KEYS[1],ARGV[1]);", RScript.ReturnType.VALUE, keys,1); Object age = redisson.getBucket("age").get(); System.out.println(age);
KEYS[1] 就對應這age, ARGV[1]就對應1,同理,KEYS[2] 就對應keys集合中的第二個元素。
3.redis減庫存lua腳本
先介紹下下單減庫存是怎麼幹的吧,其實一般庫存有可用庫存與預占庫存,再下單的時候,就將可用庫存減去你購買的商品數量看看是否是小於0,如果是小於0的話,說明庫存不夠瞭,就不讓下單購買瞭,如果可用庫存充足,可用庫存減去購買商品數量,預占庫存加上你購買商品數量,當用戶超時未支付或者是手動取消訂單的時候,就會去預占庫減去用戶購買商品數,可用庫存加上商品數,其實還有一個已售庫存,商傢發貨,已售庫存加上商品數,預占減去商品數,大體上是這個邏輯。
現在可以想下,如果讓你來實現下單預占庫存的功能,你會怎麼做,數據庫三個庫存字段這個不用說瞭。
首先你得先把可售庫存查詢出來,然後與購買商品數量進行比較,如果是可售庫存大於這個購買商品數量,就可以購買,更新可售庫存與預占庫存。
如果上面這段代碼邏輯你不加一些特殊手段的處理,那ok,高並發場景下絕對會出現超賣現象。
如果是秒殺場景呢?血虧。
這個時候我們可能會增加一些特殊手段來解決,比如說加鎖,加分佈式鎖,將這一段的業務邏輯鎖住,這個時候就不會出現那種超賣現象瞭, 但是這個中情況如果售罄的話,也會一直查詢數據庫。秒殺的時候,流量那麼高,你不能讓這麼大的流量直接查庫,如果商品售罄直接返回就可以瞭,不用再查詢數據庫瞭。
一般秒殺的時候,會將商品的庫存同步推到redis中,流量過來的時候,會先扣減redis的庫存,如果redis成功瞭,才扣減數據庫中的庫存,如果redis中的庫存沒瞭,直接返回就ok,這樣大流量就不會直接沖擊數據庫瞭,那麼redis要實現這段邏輯的話,就需要lua腳本的原子性瞭。
接下來我們就實現一下lua腳本扣減庫存邏輯。
public static final String LOCK_STOCK_LUA= "local counter = redis.call('hget',KEYS[1],ARGV[1]); \n" + "local result = counter - ARGV[2];" + "if(result>=0 ) then \n" + " redis.call('hset',KEYS[1],ARGV[1],result);\n" + " redis.call('hincrby',KEYS[1],ARGV[3],ARGV[2]);\n" + " return 1;\n" + "end;\n" + "return 0;\n";
我這裡已經寫好瞭,直接貼出來。
數據設計大體是這個樣子的,使用hash數據結構
商品:{
“可售庫存”:100,
“預占庫存”:0,
“已售庫存”:0
}
local counter = redis.call('hget',KEYS[1],ARGV[1]);
獲取可售庫存數量
local result = counter - ARGV[2]; if(result>=0 ) then
可售庫存減去要購買的商品數量,如果是大於0的話,說明庫存還夠。
redis.call('hset',KEYS[1],ARGV[1],result); redis.call('hincrby',KEYS[1],ARGV[3],ARGV[2]); return 1;
重新設置可售庫存數量,增加預占庫存,然後返回1
如果是庫存不夠的話,直接返回0瞭就。
4.實戰
4.1 減庫存邏輯
減庫存邏輯其實就是先是用lua腳本減redis庫存,如果成功再去減數據庫中的真實庫存,如果減redis庫存失敗,庫存不足,就不會再走後面減真實庫存的邏輯瞭。
這塊的話,我是寫瞭一個庫存服務,實現瞭這段邏輯,但是總感覺有各種數據不一致的問題,當然不是超賣,而是少賣問題,這裡就不發出來瞭。
4.2 壓測
我們這個實戰是在阿裡雲進行的
redis選的是容器服務,按秒計費,配置是0.5c1g
mysql也是選擇的容器服務,配置是0.5c1g
庫存服務是雲服務器,按小時計費的那種,配置是2c4g,因為要部署多個服務跟實例,選擇的比較大。
壓測也是使用的阿裡雲的性能測試服務。
redis監控,可以看到,這點並發對redis來說就是毛毛雨。cpu才使用7%
雲服務器這塊手速有點慢,沒截圖出來,cpu跟內存都在50%左右。
mysql數據庫,可以看到cpu飆上去瞭,內存飆上去瞭。
數據庫數據:
可以發現並沒有出現超賣現象。
到此這篇關於redis lua腳本實戰秒殺扣減庫存的實現的文章就介紹到這瞭,更多相關redis lua戰秒殺扣減庫存內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Redis分佈式鎖Redlock的實現
- redis通過lua腳本,獲取滿足key pattern的所有值方式
- redis執行lua腳本的實現方法
- redis分佈式鎖的8大坑總結梳理
- Redisson分佈式閉鎖RCountDownLatch的使用詳細講解