一、redis lua介紹
Redis 提供了非常豐富的指令集,但是用戶依然不滿足,希望可以自定義擴充若干指令來完成一些特定領域的問題。Redis 為這樣的用戶場景提供了 lua 腳本支持,用戶可以向服務器發送 lua 腳本來執行自定義動作,獲取腳本的響應數據。Redis 服務器會單線程原子性執行 lua 腳本,保證 lua 腳本在處理的過程中不會被任意其它請求打斷。
二、高並發情況下減庫存的實現思路
由於lua腳本是原子性同步執行的,也就是說,我們可以將一堆操作封裝為一個操作,讓redis當做一條命令執行,這樣,我們在分布式、高並發情況下,做減庫存操作,每個客戶端在執行操作時,其他客戶端都是阻塞狀態,相當於變相實現了分布式鎖。
1、在本地緩存一份減庫存的lua腳本,每次服務啟動時,將腳本內容加載至內存;
2、請求處理時,會校驗redis-server端是否存在該腳本,若存在,返回腳本的唯一id,客戶端根據id調用腳本,並將參數傳遞過去執行
3、若redis-server端不存在該腳本,會先將腳本發送到server端緩存,返回id,進行調用
三、lua腳本的好處
1、減少網絡開銷:可以將多個請求通過腳本的形式一次發送,減少網絡時延和請求次數。
2、原子性的操作:Redis會將整個腳本作為一個整體執行,中間不會被其他命令插入。因此在編寫腳本的過程中無需擔心會出現競態條件,無需使用事務。
3、代碼復用:客戶端發送的腳步會永久存在redis中,這樣,其他客戶端可以復用這一腳本來完成相同的邏輯。
4、速度快:見 與其它語言的性能比較, 還有一個 JIT編譯器可以顯著地提高多數任務的性能; 對於那些仍然對性能不滿意的人, 可以把關鍵部分使用C實現, 然后與其集成, 這樣還可以享受其它方面的好處。
5、可以移植:只要是有ANSI C 編譯器的平台都可以編譯,你可以看到它可以在幾乎所有的平台上運行:從 Windows 到Linux,同樣Mac平台也沒問題, 再到移動平台、游戲主機,甚至瀏覽器也可以完美使用 (翻譯成JavaScript).
6、源碼小巧:20000行C代碼,可以編譯進182K的可執行文件,加載快,運行快。
四、代碼實現
本地緩存一份減庫存的lua腳本
local stockId = KEYS[1]; local decrNum = ARGV[1]; local result; print('key為', stockId); print('value為', decrNum); local crtStock = redis.call('get', stockId); print('當前庫存為 :', crtStock); if crtStock == false or crtStock < decrNum then result = -2 else result = redis.call('decrBy', stockId, decrNum) end return result;
服務啟動時,將腳本內容加載至內存,由靜態字符串DECRBY_STOCK_SCRIPT接收
/** * 減庫存腳本 */ private static String DECRBY_STOCK_SCRIPT = ""; /** * 初始化bean后,將加減庫存的lua腳本加載至內存中 */ @PostConstruct public void loadLuaScript() { InputStream certStream = null; BufferedReader br = null; try { certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("lua/decrByStock.lua"); br = new BufferedReader(new InputStreamReader(certStream, "UTF-8")); StringBuilder luaStr = new StringBuilder(); String line; while ((line = br.readLine()) != null) { luaStr.append(line).append(" "); } DECRBY_STOCK_SCRIPT = luaStr.toString(); LOGGER.info("減庫存腳本初始化加載完畢,內容為:" + DECRBY_STOCK_SCRIPT); } catch (Exception e) { LOGGER.error("初始化庫存管理Controller bean,加載操作庫存腳本失敗!" + e); } finally { if (certStream != null) { try { certStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } }
在服務啟動時,會打印相應的日志
減庫存邏輯代碼
/** * 減庫存(基於lua腳本實現) * * @param trace 請求流水 * @param stockManageReq(stockId、decrNum) * @return -1為失敗,大於-1的正整數為減后的庫存量,-2為庫存不足無法減庫存 */ @Override @ApiOperation(value = "減庫存", notes = "減庫存") @RequestMapping(value = "/decrByStock", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public int decrByStock(@RequestHeader(name = "Trace") String trace, @RequestBody StockManageReq stockManageReq) { long startTime = System.currentTimeMillis(); LOGGER.reqPrint(Log.CACHE_SIGN, Log.CACHE_REQUEST, trace, "decrByStock", JSON.toJSONString(stockManageReq)); int res = 0; String stockId = stockManageReq.getStockId(); Integer decrNum = stockManageReq.getDecrNum(); if (StringUtils.isBlank(DECRBY_STOCK_SCRIPT)) { LOGGER.error("減庫存腳本為空!操作終止"); return -1; } LOGGER.info("減庫存腳本內容為:" + DECRBY_STOCK_SCRIPT); try { if (null != stockId && null != decrNum) { stockId = PREFIX + stockId; // 加減庫存lua腳本執行 Long result = (Long) this.evalshaScript(stockId, decrNum, DECRBY_STOCK_SCRIPT); LOGGER.info("腳本執行結果,result=" + result); res = result.intValue(); } } catch (Exception e) { LOGGER.error(trace, "decr sku stock failure.", e); res = -1; } finally { LOGGER.respPrint(Log.CACHE_SIGN, Log.CACHE_RESPONSE, trace, "decrByStock", System.currentTimeMillis() - startTime, String.valueOf(res)); } return res; } /** * 加減庫存lua腳本執行 * * @param stockId 庫存id * @param changeNum 加減庫存的量 * @param script lua腳本 * @return 執行結果 */ private Object evalshaScript(String stockId, Integer changeNum, String script) { Object result = null; try (Jedis jedis = jedisPool.getWriteResource()) { if (jedis.select(0).equals("OK")) { // 將腳本緩存值redis server端,並返回腳本的唯一標識id String sha = jedis.scriptLoad(script); // 調用evalsha方法,執行腳本 result = jedis.evalsha(sha, 1, stockId, String.valueOf(changeNum)); } } return result; }
五、ab壓測
5W請求,100並發,tps達到了4500,並且沒有錯誤,相當強悍了
六、總結
lua腳本實現,可以保證正確性的同時,完全能夠保證數據的一致性,可靠性方面就需要腳本的健壯性來保證,總之,效率比redisson、zk分布式鎖要高太多,推薦使用