目錄
超賣簡單程式碼
超賣問題
單一伺服器單一應用程式情況下
設定synchronized
。以至於其他處理請求,一直無法拿到
通过key设置值匹配的方式解决形同虚设问题 
最终版
首頁 資料庫 Redis Springboot整合Redis如何實現超賣問題

Springboot整合Redis如何實現超賣問題

May 26, 2023 pm 07:52 PM
redis springboot

    超賣簡單程式碼

    寫一段簡單正常的超賣邏輯程式碼,多個使用者同時操作同一段數據,探究出現的問題。

    Redis中儲存一項資料訊息,請求對應接口,取得商品數量資訊;
    商品數量資訊如果大於0,則扣減1,重新儲存Redis中;
    執行程式碼測試問題。

    /**
     * Redis数据库操作,超卖问题模拟
     * @author 
     *
     */
    @RestController
    public class RedisController {
    	
    	// 引入String类型redis操作模板
    	@Autowired
    	private StringRedisTemplate stringRedisTemplate;
     
     
    	// 测试数据设置接口
    	@RequestMapping("/setStock")
    	public String setStock() {
    		stringRedisTemplate.opsForValue().set("stock", "100");
    		return "ok";
    	}
    	
    	// 模拟商品超卖代码
    	@RequestMapping("/deductStock")
    	public String deductStock() {
    		// 获取Redis数据库中的商品数量
    		Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
    		// 减库存
    		if(stock > 0) {
    			int realStock = stock -1;
    			stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
    			System.out.println("商品扣减成功,剩余商品:"+realStock);
    		}else {
    			System.out.println("库存不足.....");
    		}
    		return "end";
    	}
    }
    登入後複製

    超賣問題

    單一伺服器單一應用程式情況下

    在單一應用模式下,使用jmeter壓測。

    Springboot整合Redis如何實現超賣問題

    Springboot整合Redis如何實現超賣問題

     測試結果:

    Springboot整合Redis如何實現超賣問題

    每個請求都相當於一個線程,當幾個線程同時拿到資料時,線程A拿到庫存為84,這個時候線程B也進入程序,並且搶佔了CPU,訪問庫存為84,最後兩個線程都對庫存減一,導致最後修改為83,實際上多賣了一件

    既然線程和線程之間,數據處理不一致,能否使用synchronized加鎖測試?

    設定synchronized

    依舊還是先測試單一伺服器

    // 模拟商品超卖代码,
    	// 设置synchronized同步锁
    	@RequestMapping("/deductStock1")
    	public String deductStock1() {
    		synchronized (this) {
    			// 获取Redis数据库中的商品数量
    			Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
    			// 减库存
    			if(stock > 0) {
    				int realStock = stock -1;
    				stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
    				System.out.println("商品扣减成功,剩余商品:"+realStock);
    			}else {
    				System.out.println("库存不足.....");
    			}
    		}
    		return "end";
    	}
    登入後複製

    數量100

    Springboot整合Redis如何實現超賣問題

    Springboot整合Redis如何實現超賣問題

    Springboot整合Redis如何實現超賣問題

    ##重新壓測,得到的日誌資訊如下所示: 

     在單機模式下,加入synchronized關鍵字,的確能夠避免商品的超賣現象! 但是在分散式微服務中,針對該服務設定了集群,synchronized依舊還能保證資料的正確性嗎? 假設多個請求,被註冊中心負載平衡,每個微服務中的該處理接口,都添加有synchronized,


     依然會出現類似的

    超賣問題:

    synchronized
    只是針對

    單一伺服器

    JVM

    進行
    加鎖

    ,但是分散式是很多個不同的伺服器,導致兩個執行緒或多個在不同伺服器上共同對商品數量資訊做了操作!

    Redis實作分散式鎖定 在Redis中存在一條指令

    setnx (set if not exists)

    #setnx key value如果不存在key,則可以設定成功;否則設定失敗。 修改處理接口,增加key<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class='brush:php;toolbar:false;'>// 模拟商品超卖代码 @RequestMapping(&quot;/deductStock2&quot;) public String deductStock2() { // 创建一个key,保存至redis String key = &quot;lock&quot;; // setnx // 由于redis是一个单线程,执行命令采取“队列”形式排队! // 优先进入队列的命令先执行,由于是setnx,第一个执行后,其他操作执行失败。 boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, &quot;this is lock&quot;); // 当不存在key时,可以设置成功,回执true;如果存在key,则无法设置,返回false if (!result) { // 前端监测,redis中存在,则不能让这个抢购操作执行,予以提示! return &quot;err&quot;; } // 获取Redis数据库中的商品数量 Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(&quot;stock&quot;)); // 减库存 if(stock &gt; 0) { int realStock = stock -1; stringRedisTemplate.opsForValue().set(&quot;stock&quot;, String.valueOf(realStock)); System.out.println(&quot;商品扣减成功,剩余商品:&quot;+realStock); }else { System.out.println(&quot;库存不足.....&quot;); } // 程序执行完成,则删除这个key stringRedisTemplate.delete(key); return &quot;end&quot;; }</pre><div class="contentsignin">登入後複製</div></div>

    1、請求進入接口中,如果redis中不存在key,則會新建一個setnx;如果存在,則不會新建,同時返回錯誤編碼,不會繼續執行搶購邏輯。

    2、當創建成功後,執行搶購邏輯。

    3、搶購邏輯執行完成後,刪除資料庫中對應的

    setnx

    key。讓其他請求能夠設定並操作。 這種邏輯來說比之前單一使用syn合理的多,但是如果執行搶購操作中出現了異常,導致這個key無法被

    刪除

    。以至於其他處理請求,一直無法拿到

    key

    ,程式邏輯死鎖! 可以採取try … finally進行操作 

    /**
    	 * 模拟商品超卖代码 设置
    	 *
    	 * @return
    	 */
    	@RequestMapping("/deductStock3")
    	public String deductStock3() {
    		// 创建一个key,保存至redis
    		String key = "lock";
    		// setnx
    		// 由于redis是一个单线程,执行命令采取队列形式排队!优先进入队列的命令先执行,由于是setnx,第一个执行后,其他操作执行失败
    		boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "this is lock");
    		// 当不存在key时,可以设置成功,回执true;如果存在key,则无法设置,返回false
    		if (!result) {
    			// 前端监测,redis中存在,则不能让这个抢购操作执行,予以提示!
    			return "err";
    		}
     
    		try {
    			// 获取Redis数据库中的商品数量
    			Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
    			// 减库存
    			if (stock > 0) {
    				int realStock = stock - 1;
    				stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
    				System.out.println("商品扣减成功,剩余商品:" + realStock);
    			} else {
    				System.out.println("库存不足.....");
    			}
    		} finally {
    			// 程序执行完成,则删除这个key
    			// 放置于finally中,保证即使上述逻辑出问题,也能del掉
    			stringRedisTemplate.delete(key);
    		}
     
    		return "end";
    	}
    登入後複製

    這個邏輯相比上面其他的邏輯來說,顯得更加的嚴謹。

    但是,如果一套伺服器,因為斷電、系統崩潰等原因出現

    宕機,導致本該執行finally
    中的語句未成功執行完成! !同樣出現

    key一直存在
    ,導致

    死鎖Springboot整合Redis如何實現超賣問題

    透過超時間解決上述問題


    在設定成功
    setnx
    後,以及搶購程式碼邏輯執行前,增加key的限時。

    /**
    	 * 模拟商品超卖代码 设置setnx保证分布式环境下,数据处理安全行问题;<br>
    	 * 但如果某个代码段执行异常,导致key无法清理,出现死锁,添加try...finally;<br>
    	 * 如果某个服务因某些问题导致释放key不能执行,导致死锁,此时解决思路为:增加key的有效时间;<br>
    	 * 为了保证设置key的值和设置key的有效时间,两条命令构成同一条原子命令,将下列逻辑换成其他代码。
    	 *
    	 * @return
    	 */
    	@RequestMapping("/deductStock4")
    	public String deductStock4() {
    		// 创建一个key,保存至redis
    		String key = "lock";
    		// setnx
    		// 由于redis是一个单线程,执行命令采取队列形式排队!优先进入队列的命令先执行,由于是setnx,第一个执行后,其他操作执行失败
    		//boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "this is lock");
     
    		//让设置key和设置key的有效时间都可以同时执行
    		boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "this is lock", 10, TimeUnit.SECONDS);
     
    		// 当不存在key时,可以设置成功,回执true;如果存在key,则无法设置,返回false
    		if (!result) {
    			// 前端监测,redis中存在,则不能让这个抢购操作执行,予以提示!
    			return "err";
    		}
    		// 设置key有效时间
    		//stringRedisTemplate.expire(key, 10, TimeUnit.SECONDS);
     
    		try {
    			// 获取Redis数据库中的商品数量
    			Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
    			// 减库存
    			if (stock > 0) {
    				int realStock = stock - 1;
    				stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
    				System.out.println("商品扣减成功,剩余商品:" + realStock);
    			} else {
    				System.out.println("库存不足.....");
    			}
    		} finally {
    			// 程序执行完成,则删除这个key
    			// 放置于finally中,保证即使上述逻辑出问题,也能del掉
    			stringRedisTemplate.delete(key);
    		}
     
    		return "end";
    	}
    登入後複製

    但是上述程式碼的邏輯中依舊會有問題:#########如果處理邏輯中,出現###逾時###問題。 ###當邏輯執行時,時間超過設定key有效時間,此時會出現什麼問題? ##################### 從上圖可以清楚的發現問題:###如果一個請求執行時間超過了key的有效時間。 ###新的請求執行過來時,必然可以拿到key並設定時間;###此時的redis中保存的key並不是請求1的key,而是別的請求設定的。 ###當請求1執行完成後,此處刪除key,刪除的是別的請求設定的key! ###

    依然出现了key形同虚设的问题!如果失效一直存在,超卖问题依旧不会解决。

    通过key设置值匹配的方式解决形同虚设问题

    既然出现key形同虚设的现象,是否可以增加条件,当finally中需要执行删除操作时,获取数据判断值是否是该请求中对应的,如果是则删除,不是则不管!

    修改上述代码如下所示:

    /**
    	 * 模拟商品超卖代码 <br>
    	 * 解决`deductStock6`中,key形同虚设的问题。
    	 *
    	 * @return
    	 */
    	@RequestMapping("/deductStock5")
    	public String deductStock5() {
    		// 创建一个key,保存至redis
    		String key = "lock";
    		String lock_value = UUID.randomUUID().toString();
    		// setnx
    		//让设置key和设置key的有效时间都可以同时执行
    		boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, lock_value, 10, TimeUnit.SECONDS);
    		// 当不存在key时,可以设置成功,回执true;如果存在key,则无法设置,返回false
    		if (!result) {
    			// 前端监测,redis中存在,则不能让这个抢购操作执行,予以提示!
    			return "err";
    		}
    		try {
    			// 获取Redis数据库中的商品数量
    			Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
    			// 减库存
    			if (stock > 0) {
    				int realStock = stock - 1;
    				stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
    				System.out.println("商品扣减成功,剩余商品:" + realStock);
    			} else {
    				System.out.println("库存不足.....");
    			}
    		} finally {
    			// 程序执行完成,则删除这个key
    			// 放置于finally中,保证即使上述逻辑出问题,也能del掉
     
    			// 判断redis中该数据是否是这个接口处理时的设置的,如果是则删除
    			if(lock_value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(key))) {
    				stringRedisTemplate.delete(key);
    			}
    		}
    		return "end";
    	}
    登入後複製

    由于获得锁的线程必须执行完减库存逻辑才能释放锁,所以在此期间所有其他的线程都会由于没获得锁,而直接结束程序,导致有很多库存根本没有卖出去,所以这里应该可以优化,让没获得锁的线程等待,或者循环检查锁

    Springboot整合Redis如何實現超賣問題


    最终版

    我们将锁封装到一个实体类中,然后加入两个方法,加锁和解锁

    @Component
    public class RedisLock {
        private final Logger log = LoggerFactory.getLogger(this.getClass());
     
        private final long acquireTimeout = 10*1000;    // 获取锁之前的超时时间(获取锁的等待重试时间)
        private final int timeOut = 20;   // 获取锁之后的超时时间(防止死锁)
     
        @Autowired
        private StringRedisTemplate stringRedisTemplate;  // 引入String类型redis操作模板
     
        /**
         * 获取分布式锁
         * @return 锁标识
         */
        public boolean getRedisLock(String lockName,String lockValue) {
            // 1.计算获取锁的时间
            Long endTime = System.currentTimeMillis() + acquireTimeout;
            // 2.尝试获取锁
            while (System.currentTimeMillis() < endTime) {
                //3. 获取锁成功就设置过期时间 让设置key和设置key的有效时间都可以同时执行
                boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockName, lockValue, timeOut, TimeUnit.SECONDS);
                if (result) {
                    return true;
                }
            }
            return false;
        }
     
     
        /**
         * 释放分布式锁
         * @param lockName 锁名称
         * @param lockValue 锁值
         */
        public void unRedisLock(String lockName,String lockValue) {
            if(lockValue.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(lockName))) {
                stringRedisTemplate.delete(lockName);
            }
        }
    }
    登入後複製
    @RestController
    public class RedisController {
    	
    	// 引入String类型redis操作模板
    	@Autowired
    	private StringRedisTemplate stringRedisTemplate;
    	@Autowired
    	private RedisLock redisLock;
     
     
    	@RequestMapping("/setStock")
    	public String setStock() {
    		stringRedisTemplate.opsForValue().set("stock", "100");
    		return "ok";
    	}
     
    	@RequestMapping("/deductStock")
    	public String deductStock() {
    		// 创建一个key,保存至redis
    		String key = "lock";
    		String lock_value = UUID.randomUUID().toString();
    		try {
    			boolean redisLock = this.redisLock.getRedisLock(key, lock_value);//获取锁
    			if (redisLock)
    			{
    				// 获取Redis数据库中的商品数量
    				Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
    				// 减库存
    				if (stock > 0) {
    					int realStock = stock - 1;
    					stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
    					System.out.println("商品扣减成功,剩余商品:" + realStock);
    				} else {
    					System.out.println("库存不足.....");
    				}
    			}
    		} finally {
    			redisLock.unRedisLock(key,lock_value);   //释放锁
    		}
    		return "end";
    	}
    }
    登入後複製

    可以看到失败的线程不会直接结束,而是会尝试重试,一直到重试结束时间,才会结束

    Springboot整合Redis如何實現超賣問題


    实际上这个最终版依然存在3个问题

    1、在finally流程中,由于是先判断在处理。如果判断条件结束后,获取到的结果为true。但是在执行del操作前,此时jvm在执行GC操作(为了保证GC操作获取GC roots根完全,会暂停java程序),导致程序暂停。在GC操作完成并恢复后,执行del操作时,当前被加锁的key是否仍然存在?

    2、问题如图所示

    Springboot整合Redis如何實現超賣問題

    以上是Springboot整合Redis如何實現超賣問題的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    本網站聲明
    本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

    熱AI工具

    Undresser.AI Undress

    Undresser.AI Undress

    人工智慧驅動的應用程序,用於創建逼真的裸體照片

    AI Clothes Remover

    AI Clothes Remover

    用於從照片中去除衣服的線上人工智慧工具。

    Undress AI Tool

    Undress AI Tool

    免費脫衣圖片

    Clothoff.io

    Clothoff.io

    AI脫衣器

    Video Face Swap

    Video Face Swap

    使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

    熱門文章

    <🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
    3 週前 By 尊渡假赌尊渡假赌尊渡假赌
    北端:融合系統,解釋
    3 週前 By 尊渡假赌尊渡假赌尊渡假赌
    Mandragora:巫婆樹的耳語 - 如何解鎖抓鉤
    3 週前 By 尊渡假赌尊渡假赌尊渡假赌

    熱工具

    記事本++7.3.1

    記事本++7.3.1

    好用且免費的程式碼編輯器

    SublimeText3漢化版

    SublimeText3漢化版

    中文版,非常好用

    禪工作室 13.0.1

    禪工作室 13.0.1

    強大的PHP整合開發環境

    Dreamweaver CS6

    Dreamweaver CS6

    視覺化網頁開發工具

    SublimeText3 Mac版

    SublimeText3 Mac版

    神級程式碼編輯軟體(SublimeText3)

    熱門話題

    Java教學
    1667
    14
    CakePHP 教程
    1426
    52
    Laravel 教程
    1328
    25
    PHP教程
    1273
    29
    C# 教程
    1255
    24
    redis集群模式怎麼搭建 redis集群模式怎麼搭建 Apr 10, 2025 pm 10:15 PM

    Redis集群模式通過分片將Redis實例部署到多個服務器,提高可擴展性和可用性。搭建步驟如下:創建奇數個Redis實例,端口不同;創建3個sentinel實例,監控Redis實例並進行故障轉移;配置sentinel配置文件,添加監控Redis實例信息和故障轉移設置;配置Redis實例配置文件,啟用集群模式並指定集群信息文件路徑;創建nodes.conf文件,包含各Redis實例的信息;啟動集群,執行create命令創建集群並指定副本數量;登錄集群執行CLUSTER INFO命令驗證集群狀態;使

    redis數據怎麼清空 redis數據怎麼清空 Apr 10, 2025 pm 10:06 PM

    如何清空 Redis 數據:使用 FLUSHALL 命令清除所有鍵值。使用 FLUSHDB 命令清除當前選定數據庫的鍵值。使用 SELECT 切換數據庫,再使用 FLUSHDB 清除多個數據庫。使用 DEL 命令刪除特定鍵。使用 redis-cli 工具清空數據。

    redis怎麼讀取隊列 redis怎麼讀取隊列 Apr 10, 2025 pm 10:12 PM

    要從 Redis 讀取隊列,需要獲取隊列名稱、使用 LPOP 命令讀取元素,並處理空隊列。具體步驟如下:獲取隊列名稱:以 "queue:" 前綴命名,如 "queue:my-queue"。使用 LPOP 命令:從隊列頭部彈出元素並返回其值,如 LPOP queue:my-queue。處理空隊列:如果隊列為空,LPOP 返回 nil,可先檢查隊列是否存在再讀取元素。

    centos redis如何配置Lua腳本執行時間 centos redis如何配置Lua腳本執行時間 Apr 14, 2025 pm 02:12 PM

    在CentOS系統上,您可以通過修改Redis配置文件或使用Redis命令來限制Lua腳本的執行時間,從而防止惡意腳本佔用過多資源。方法一:修改Redis配置文件定位Redis配置文件:Redis配置文件通常位於/etc/redis/redis.conf。編輯配置文件:使用文本編輯器(例如vi或nano)打開配置文件:sudovi/etc/redis/redis.conf設置Lua腳本執行時間限制:在配置文件中添加或修改以下行,設置Lua腳本的最大執行時間(單位:毫秒)

    redis命令行怎麼用 redis命令行怎麼用 Apr 10, 2025 pm 10:18 PM

    使用 Redis 命令行工具 (redis-cli) 可通過以下步驟管理和操作 Redis:連接到服務器,指定地址和端口。使用命令名稱和參數向服務器發送命令。使用 HELP 命令查看特定命令的幫助信息。使用 QUIT 命令退出命令行工具。

    redis計數器怎麼實現 redis計數器怎麼實現 Apr 10, 2025 pm 10:21 PM

    Redis計數器是一種使用Redis鍵值對存儲來實現計數操作的機制,包含以下步驟:創建計數器鍵、增加計數、減少計數、重置計數和獲取計數。 Redis計數器的優勢包括速度快、高並發、持久性和簡單易用。它可用於用戶訪問計數、實時指標跟踪、遊戲分數和排名以及訂單處理計數等場景。

    redis過期策略怎麼設置 redis過期策略怎麼設置 Apr 10, 2025 pm 10:03 PM

    Redis數據過期策略有兩種:定期刪除:定期掃描刪除過期鍵,可通過 expired-time-cap-remove-count、expired-time-cap-remove-delay 參數設置。惰性刪除:僅在讀取或寫入鍵時檢查刪除過期鍵,可通過 lazyfree-lazy-eviction、lazyfree-lazy-expire、lazyfree-lazy-user-del 參數設置。

    如何優化debian readdir的性能 如何優化debian readdir的性能 Apr 13, 2025 am 08:48 AM

    在Debian系統中,readdir系統調用用於讀取目錄內容。如果其性能表現不佳,可嘗試以下優化策略:精簡目錄文件數量:盡可能將大型目錄拆分成多個小型目錄,降低每次readdir調用處理的項目數量。啟用目錄內容緩存:構建緩存機制,定期或在目錄內容變更時更新緩存,減少對readdir的頻繁調用。內存緩存(如Memcached或Redis)或本地緩存(如文件或數據庫)均可考慮。採用高效數據結構:如果自行實現目錄遍歷,選擇更高效的數據結構(例如哈希表而非線性搜索)存儲和訪問目錄信

    See all articles