基于redis實現的扣減庫存

2019獨角獸企業重金招聘Python工程師標準>>> hot3.png

在日常開發中有很多地方都有類似扣減庫存的操作,比如電商系統中的商品庫存,抽獎系統中的獎品庫存等。

解決方案

  1. 使用mysql數據庫,使用一個字段來存儲庫存,每次扣減庫存去更新這個字段。
  2. 還是使用數據庫,但是將庫存分層多份存到多條記錄里面,扣減庫存的時候路由一下,這樣子增大了并發量,但是還是避免不了大量的去訪問數據庫來更新庫存。
  3. 將庫存放到redis使用redis的incrby特性來扣減庫存。

?

正常的操作是:
扣庫存->成功->下單->成功
扣庫存->成功->下單->失敗->回滾庫存
扣庫存->失敗->下單失敗

分析

在上面的第一種和第二種方式都是基于數據來扣減庫存。

基于數據庫單庫存

第一種方式在所有請求都會在這里等待鎖,獲取鎖有去扣減庫存。在并發量不高的情況下可以使用,但是一旦并發量大了就會有大量請求阻塞在這里,導致請求超時,進而整個系統雪崩;而且會頻繁的去訪問數據庫,大量占用數據庫資源,所以在并發高的情況下這種方式不適用。

基于數據庫多庫存

第二種方式其實是第一種方式的優化版本,在一定程度上提高了并發量,但是在還是會大量的對數據庫做更新操作大量占用數據庫資源。

基于數據庫來實現扣減庫存還存在的一些問題:

  • 用數據庫扣減庫存的方式,扣減庫存的操作必須在一條語句中執行,不能先selec在update,這樣在并發下會出現超扣的情況。如:
update number set x=x-1 where x > 0
  • MySQL自身對于高并發的處理性能就會出現問題,一般來說,MySQL的處理性能會隨著并發thread上升而上升,但是到了一定的并發度之后會出現明顯的拐點,之后一路下降,最終甚至會比單thread的性能還要差。

  • 當減庫存和高并發碰到一起的時候,由于操作的庫存數目在同一行,就會出現爭搶InnoDB行鎖的問題,導致出現互相等待甚至死鎖,從而大大降低MySQL的處理性能,最終導致前端頁面出現超時異常。

基于redis

針對上述問題的問題我們就有了第三種方案,將庫存放到緩存,利用redis的incrby特性來扣減庫存,解決了超扣和性能問題。但是一旦緩存丟失需要考慮恢復方案。比如抽獎系統扣獎品庫存的時候,初始庫存=總的庫存數-已經發放的獎勵數,但是如果是異步發獎,需要等到MQ消息消費完了才能重啟redis初始化庫存,否則也存在庫存不一致的問題。

基于redis實現扣減庫存的具體實現

  • 我們使用redis的lua腳本來實現扣減庫存
  • 由于是分布式環境下所以還需要一個分布式鎖來控制只能有一個服務去初始化庫存
  • 需要提供一個回調函數,在初始化庫存的時候去調用這個函數獲取初始化庫存

初始化庫存回調函數(IStockCallback )

/*** 獲取庫存回調* @author yuhao.wang*/
public interface IStockCallback {/*** 獲取庫存* @return*/int getStock();
}

扣減庫存服務(StockService)

/*** 扣庫存** @author yuhao.wang*/
@Service
public class StockService {Logger logger = LoggerFactory.getLogger(StockService.class);/*** 不限庫存*/public static final long UNINITIALIZED_STOCK = -3L;/*** Redis 客戶端*/@Autowiredprivate RedisTemplate<String, Object> redisTemplate;/*** 執行扣庫存的腳本*/public static final String STOCK_LUA;static {/**** @desc 扣減庫存Lua腳本* 庫存(stock)-1:表示不限庫存* 庫存(stock)0:表示沒有庫存* 庫存(stock)大于0:表示剩余庫存** @params 庫存key* @return* 		-3:庫存未初始化* 		-2:庫存不足* 		-1:不限庫存* 		大于等于0:剩余庫存(扣減之后剩余的庫存)* 	    redis緩存的庫存(value)是-1表示不限庫存,直接返回1*/StringBuilder sb = new StringBuilder();sb.append("if (redis.call('exists', KEYS[1]) == 1) then");sb.append("    local stock = tonumber(redis.call('get', KEYS[1]));");sb.append("    local num = tonumber(ARGV[1]);");sb.append("    if (stock == -1) then");sb.append("        return -1;");sb.append("    end;");sb.append("    if (stock >= num) then");sb.append("        return redis.call('incrby', KEYS[1], 0 - num);");sb.append("    end;");sb.append("    return -2;");sb.append("end;");sb.append("return -3;");STOCK_LUA = sb.toString();}/*** @param key           庫存key* @param expire        庫存有效時間,單位秒* @param num           扣減數量* @param stockCallback 初始化庫存回調函數* @return -2:庫存不足; -1:不限庫存; 大于等于0:扣減庫存之后的剩余庫存*/public long stock(String key, long expire, int num, IStockCallback stockCallback) {long stock = stock(key, num);// 初始化庫存if (stock == UNINITIALIZED_STOCK) {RedisLock redisLock = new RedisLock(redisTemplate, key);try {// 獲取鎖if (redisLock.tryLock()) {// 雙重驗證,避免并發時重復回源到數據庫stock = stock(key, num);if (stock == UNINITIALIZED_STOCK) {// 獲取初始化庫存final int initStock = stockCallback.getStock();// 將庫存設置到redisredisTemplate.opsForValue().set(key, initStock, expire, TimeUnit.SECONDS);// 調一次扣庫存的操作stock = stock(key, num);}}} catch (Exception e) {logger.error(e.getMessage(), e);} finally {redisLock.unlock();}}return stock;}/*** 加庫存(還原庫存)** @param key    庫存key* @param num    庫存數量* @return*/public long addStock(String key, int num) {return addStock(key, null, num);}/*** 加庫存** @param key    庫存key* @param expire 過期時間(秒)* @param num    庫存數量* @return*/public long addStock(String key, Long expire, int num) {boolean hasKey = redisTemplate.hasKey(key);// 判斷key是否存在,存在就直接更新if (hasKey) {return redisTemplate.opsForValue().increment(key, num);}Assert.notNull(expire,"初始化庫存失敗,庫存過期時間不能為null");RedisLock redisLock = new RedisLock(redisTemplate, key);try {if (redisLock.tryLock()) {// 獲取到鎖后再次判斷一下是否有keyhasKey = redisTemplate.hasKey(key);if (!hasKey) {// 初始化庫存redisTemplate.opsForValue().set(key, num, expire, TimeUnit.SECONDS);}}} catch (Exception e) {logger.error(e.getMessage(), e);} finally {redisLock.unlock();}return num;}/*** 獲取庫存** @param key 庫存key* @return -1:不限庫存; 大于等于0:剩余庫存*/public int getStock(String key) {Integer stock = (Integer) redisTemplate.opsForValue().get(key);return stock == null ? -1 : stock;}/*** 扣庫存** @param key 庫存key* @param num 扣減庫存數量* @return 扣減之后剩余的庫存【-3:庫存未初始化; -2:庫存不足; -1:不限庫存; 大于等于0:扣減庫存之后的剩余庫存】*/private Long stock(String key, int num) {// 腳本里的KEYS參數List<String> keys = new ArrayList<>();keys.add(key);// 腳本里的ARGV參數List<String> args = new ArrayList<>();args.add(Integer.toString(num));long result = redisTemplate.execute(new RedisCallback<Long>() {@Overridepublic Long doInRedis(RedisConnection connection) throws DataAccessException {Object nativeConnection = connection.getNativeConnection();// 集群模式和單機模式雖然執行腳本的方法一樣,但是沒有共同的接口,所以只能分開執行// 集群模式if (nativeConnection instanceof JedisCluster) {return (Long) ((JedisCluster) nativeConnection).eval(STOCK_LUA, keys, args);}// 單機模式else if (nativeConnection instanceof Jedis) {return (Long) ((Jedis) nativeConnection).eval(STOCK_LUA, keys, args);}return UNINITIALIZED_STOCK;}});return result;}}

調用

/*** @author yuhao.wang*/
@RestController
public class StockController {@Autowiredprivate StockService stockService;@RequestMapping(value = "stock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)public Object stock() {// 商品IDlong commodityId = 1;// 庫存IDString redisKey = "redis_key:stock:" + commodityId;long stock = stockService.stock(redisKey, 60 * 60, 2, () -> initStock(commodityId));return stock >= 0;}/*** 獲取初始的庫存** @return*/private int initStock(long commodityId) {// TODO 這里做一些初始化庫存的操作return 1000;}@RequestMapping(value = "getStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)public Object getStock() {// 商品IDlong commodityId = 1;// 庫存IDString redisKey = "redis_key:stock:" + commodityId;return stockService.getStock(redisKey);}@RequestMapping(value = "addStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)public Object addStock() {// 商品IDlong commodityId = 2;// 庫存IDString redisKey = "redis_key:stock:" + commodityId;return stockService.addStock(redisKey, 2);}
}

源碼:?https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases

spring-boot-student-stock-redis 工程

參考:

  • http://www.cnblogs.com/billyxp/p/3701124.html
  • http://blog.csdn.net/jiao_fuyou/article/details/15504777
  • https://www.jianshu.com/p/48c1a92fbf3a
  • https://www.zhihu.com/question/268937734

轉載于:https://my.oschina.net/xiaominmin/blog/3060257

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/279400.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/279400.shtml
英文地址,請注明出處:http://en.pswp.cn/news/279400.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

JavaScript 使用random()生成隨機數

function myFunction() { var a Math.floor(Math.random()*10);return a;} // 記住 Math.random() 永遠不會返回 1。同時因為我們是在用 Math.floor() 向下取整&#xff0c;所以最終我們獲得的結果不可能有 20。這確保了我們獲得了一個在0到19之間的整數。 把操作連綴起來&…

plex 亂碼_Plex Media Center現在支持播客

plex 亂碼Plex is adding beta support for podcasts to iOS, Android, Roku, and Plex Web today, alongside a custom home screen for mobile users. Plex現在為iOS&#xff0c;Android&#xff0c;Roku和Plex Web的播客添加了beta支持&#xff0c;同時為移動用戶提供了自定…

Add a All Document Folder

本文出自Simmy的個人blog&#xff1a;西米在線 http://simmyonline.com/archives/54.html right clickSearch Folder-New Search Folder-Custom-Create a custom folder 本文轉simmy51CTO博客&#xff0c;原文鏈接&#xff1a;http://blog.51cto.com/helpdesk/122327&#xff0…

Oracle服務器修改IP后

機房有兩套網絡&#xff0c;一套辦公網&#xff0c;一套機房的內網&#xff0c;辦公網可以通過vpn在其他地方訪問&#xff0c;內網只能在公司辦公室訪問。團隊有同事去外地辦公&#xff0c;開發的時候需要通過客戶端直連數據庫&#xff0c;于是把數據庫服務器的網線換到辦公網的…

代理IP對直播平臺的影響與關系-國內多IP曇花一現

1.代理IP的作用1>.訪問一些單位或團體內部資源&#xff0c;如某大學FTP(前提是該代理地址在該資源 的允許訪問范圍之內)&#xff0c;使用網絡內地址段免費代理服務器&#xff0c;就可以用于對 網絡開放的各類FTP下載上傳&#xff0c;以及各類資料查詢共享等服務。國內站群整…

ios12徹底關閉siri_Siri正在iOS 12中獲取自定義語音操作

ios12徹底關閉siriSiri is about to get a lot more powerful. Custom voice commands for any app will allow you to say “Hey Siri, I lost my keys” to instantly launch an app that will help you find them. Siri將變得更加強大。 針對任何應用程序的自定義語音命令將…

spring cloud連載第一篇之bootstrap context

1. Spring Cloud Context: Application Context Services&#xff08;應用上下文服務&#xff09; 1.1 The Bootstrap Application Context&#xff08;引導上下文&#xff09; 一個spring cloud應用會創建一個“bootstrap”context&#xff0c;它是主應用的parent context。它…

過Postfix構建Exchange Server 2010郵件網關部署系列三:安裝Exchange 2010先決性條件

1.將Exchange Server 2010服務器加入域。 2.在“開始”菜單上&#xff0c;依次導航到“所有程序”>“附件”>“Windows PowerShell”。打開提升的 Windows PowerShell 控制臺并運行以下命令&#xff1a; Import-Module ServerManager 3.使用 Add-WindowsFeature cmdlet 安…

gmail收件箱標簽設置_通過多個收件箱實驗室有效管理您的Gmail

gmail收件箱標簽設置Most people have more than one email account and if you are using Gmail it’s easy to get things set up so that all of your messages can be accessed in the same place. But if you would prefer to keep things ‘together yet separate’ the …

清華生命學院 2017 就業報告:就業率僅 51%

時間&#xff1a;20170406 一、截至目前生命學院整體就業情況 1.1 系統就業率 1.2 實際排查就業率 (6092)/(68230)51.06%二、本科生就業排查 2017 屆本科生 68 人&#xff0c;已確定去向 60 人&#xff08;已登記去向 32 人&#xff09; 2.1 確定去向的 60 人中 國內深造 35 人…

程序改變了命運,程序生活一天比一天好,對未來也充滿了希望

為什么80%的碼農都做不了架構師&#xff1f;>>> 我出生在內蒙古自治區興安盟扎賚特旗寶力根花蘇木&#xff0c;那里是少數民族蒙古族聚居區&#xff0c;20-30年前與現代城市文明有些差距。當還在讀小學的時在中學當數學老師的爸爸去深圳出差學習&#xff0c;順路在…

powershell 變量_極客學院:學習PowerShell變量,輸入和輸出

powershell 變量As we move away from simply running commands and move into writing full blown scripts, you will need a temporary place to store data. This is where variables come in. 隨著我們不再只是運行命令而轉而編寫完整的腳本&#xff0c;您將需要一個臨時位…

offsetTop、offsetLeft、offsetWidth、offsetHeight、style中的樣式

< DOCTYPE html PUBLIC -WCDTD XHTML StrictEN httpwwwworgTRxhtmlDTDxhtml-strictdtd> 假設 obj 為某個 HTML 控件。 obj.offsetTop 指 obj 距離上方或上層控件的位置&#xff0c;整型&#xff0c;單位像素。 obj.offsetLeft 指 obj 距離左方或上層控件的位置&#xff0…

Mock2 moco框架的http協議get方法Mock的實現

首先在Chapter7文件夾下再新建一個startGet.json startget.json代碼如下&#xff0c;因為是get請求&#xff0c;所以要寫method關鍵字&#xff0c;有兩個&#xff0c;一個是有參數&#xff0c;一個是無參數的請求。 [{"description":"模擬一個沒有參數的get請求…

Android 干貨,強烈推薦

本文主要收集 Android開發中常用的干貨技術&#xff0c;現做出目錄&#xff0c;此文不斷更新中&#xff0c;歡迎關注、點贊、投稿。Android 四大組件與布局1. Activity 使用詳解2. Service 使用詳解3. Broadcast 使用詳解4. ContentProvider 使用詳解5. 四大布局 使用詳解6. Re…

imessage_如何在所有Apple設備上同步您的iMessage

imessageMessages in iCloud lets you sync your iMessages across all of your Apple devices using your iCloud account. Here’s how to set it up. 通過iCloud中的消息&#xff0c;您可以使用iCloud帳戶在所有Apple設備上同步iMessage。 設置方法如下。 Apple announced t…

“.Net 社區大會”(dotnetConf) 2018 Day 1 主題演講

Miguel de Icaza、Scott Hunter、Mads Torgersen三位大咖給大家帶來了 .NET Core ,C# 以及 Xamarin的精彩內容&#xff1a;6月份已經發布了.NET Core 2.1, 大會上Scott Hunter 一開始花了大量的篇幅回顧.NET Core 2.1的發布&#xff0c;社區的參與度已經非常高&#xff0c;.NET…

Windows 2003 NTP 時間服務器設置

需要在局域網中架設一臺時間同步服務器&#xff0c;統一各客戶端及服務器的系統時間&#xff0c;在網上查找大多是基于Linux下的 確&#xff2e;&#xff34;&#xff30;服務器&#xff0e;搜索&#xff0c;實驗及總結&#xff0c;寫一篇采用Windwos2003自帶的W32Time服務用于…

React 深入學習:React 更新隊列

path&#xff1a;packages/react-reconciler/src/ReactUpdateQueue.js 更新 export type Update<State> {expirationTime: ExpirationTime, // 到期時間tag: 0 | 1 | 2 | 3, // 更新類型payload: any, // 負載callback: (() > mixed) | null, // 回調函數next: Updat…

長時間曝光計算_如何拍攝好長時間曝光的照片

長時間曝光計算In long exposure photography, you take a picture with a slow shutter speed—generally somewhere between five and sixty seconds—so that any movement in the scene gets blurred. It’s a way to show the passage of time in a single image. Let’s …