多業務場景下對于redis分布式鎖的一些思考

現在讓你寫一個Redis分布式鎖
大概率你會先寫一個框架

public Boolean setIfAbsent(String key, Object value,Long timeout) {try {return Boolean.TRUE.equals(objectRedisTemplate.opsForValue().setIfAbsent(key, value,timeout,TimeUnit.SECONDS));} catch (Exception e) {log.error("", e);return false;}}
 private void assessInstance(){InitThreadPoolUtil.execute(() -> {while (true) {try {Boolean isMaster = setIfAbsent(RedisKeyConstant.ASSESS_INSTANCE_ONE, "admin", 2 * 60L);logger.error("執行插入===" + isMaster);if (isMaster) {// ...業務代碼略if (redisService.hasKey(RedisKeyConstant.ASSESS_INSTANCE_ONE)) {redisService.del(RedisKeyConstant.ASSESS_INSTANCE_ONE);}}} catch (Exception e) {logger.error("評估規劃發送通知失敗:", e);}try {Thread.sleep(1000 * 60 * 1);} catch (InterruptedException e) {logger.error("線程休眠異常,異常信息為:", e);}}});}

但是這樣就完了嗎?
我們來評審一下此代碼健壯性:

可以看到這是從線程池中取一個線程去執行該業務代碼。那么我給你的場景是處理訂單業務,那么你就會面對高并發情況,若某一刻發起了10個訂單請求,那么就會有10個線程進入while循環。但是有且僅有一個線程會獲取鎖,并執行業務代碼。其他9個線程會一直等待,一旦有鎖釋放,這9個線程會立刻搶鎖。

我們給redis的鎖定義了一個超時時間,某線程獲取鎖后最多使用 10s,然后必須釋放鎖。
此外你還知道執行該業務代碼最多需要10s。等于你上網時間剛清零你本局游戲剛結束。
這樣其他9個線程最多需要10s就可以獲取到鎖。

所以會出現一種現象,A線程獲取到了鎖后,開始執行業務代碼。其他9個線程會一直重試嘗試獲取鎖,累計10s。為了避免頻繁嘗試獲取鎖消耗資源,我們暫時設置線程第一次未獲取鎖后,需要休眠2s才能重新請求獲取鎖。這樣就降低了這9個線程重試請求鎖的頻率。

對于用戶而言,一個用戶的訂單正在處理,其他9個用戶的訂單需要等待10s,推算下來,最后一個用戶的訂單被處理時,已經等待了90s。如果我是用戶,我可不希望等待這么長的時間且無法進行任何操作。

我更希望等待更少的時間,比如20s沒反應,我可以繼續提交訂單。像不像搶演唱會票的過程:進入訂單界面,提交的時候一直轉圈圈,等待5s后顯示訂單提交失敗,然后你會重新提交訂單。

此外,上述代碼還有個局限性:提交了10個訂單,將會有1個線程執行業務代碼,9個線程一直在等待。
執行業務代碼的線程生命周期如下:嘗試獲取鎖—>獲取鎖---->執行業務代碼----->等待被自動回收
等待的線程生命周期如下: 休眠—>嘗試獲取鎖—>休眠---->嘗試獲取鎖—>…

可以發現等待的線程是始終無法被自動回收,除非執行完業務代碼,操作系統才能判斷:該線程已經沒有被使用了,可以自動歸還到線程池。(線程池自動管理線程的生命周期)

對于用戶而言,他等待時間太久。對于系統而言,大量資源被此處占用、消耗。

所以我們必須優化。如何優化呢?
A線程會占用鎖10s,其余9個線程會一直等待。現在我要求,一旦發現6s后,鎖還沒被釋放,等待的線程就退出等待。而用戶就可以重新提交訂單了。

我們來捋一捋:A線程搶到了鎖后,(超時時間也就是等待時間未超過6s)B線程先睡眠2s,再重新獲取鎖失敗,(超時時間也就是等待時間未超過6s)再睡眠2s,重新獲取鎖失敗,(超時時間也就是等待時間未超過6s)再睡眠2s,重新獲取鎖失敗,(超時時間也就是等待時間超過6s),不再嘗試獲取鎖,返回信息:訂單提交失敗。

推理下來,一個用戶最多等待10s,變成了最多等待6s。那么10個訂單同時提交而最后一個用戶只需等待50s。想要再縮短等待時間,可以將超時時間從6s縮短到2s,這樣10個訂單同時提交而最后一個用戶只需等待18s。

當然你也可以將業務處理時間優化,這里不討論。

代碼如下

 private void assessInstance(){// 初始時間long startTime = System.currentTimeMillis();InitThreadPoolUtil.execute(() -> {while (true) {try {Boolean isMaster = setIfAbsent(RedisKeyConstant.ASSESS_INSTANCE_ONE, "admin", 2 * 60L);logger.error("執行插入===" + isMaster);if (isMaster) {// ...業務代碼略// 嘗試超過了設定值之后直接跳出循環,避免上新鎖時間過長// 例如A線程上新鎖,花費了10s,這10s內B線程無法獲取鎖,就會一直在循環里重試,設置超時時間為2s,// 一旦B線程重試超過2s就退出循環且生命周期結束。if (System.currentTimeMillis() - startTime > timeout) {return false;}if (redisService.hasKey(RedisKeyConstant.ASSESS_INSTANCE_ONE)) {redisService.del(RedisKeyConstant.ASSESS_INSTANCE_ONE);}}} catch (Exception e) {logger.error("評估規劃發送通知失敗:", e);}try {Thread.sleep(1000 * 60 * 1);} catch (InterruptedException e) {logger.error("線程休眠異常,異常信息為:", e);}}});}

這是針對高并發場景下以上代碼實現Redis鎖的問題。有些場景下使用上述代碼完全沒問題。
例如服務啟動后,需要初始化一些數據。單機環境只會執行一次初始化數據,什么都不需要考慮。

若是集群模式,有三個機子。當然只能一臺leader機子執行一次初始化數據,其余2個機子不需要執行初始化數據,所以必須上分布式鎖,且不存在高并發場景。

上述的代碼直接使用了redis的一些原生api,我們嘗試將其封裝一層供自己使用

/*** 全局鎖,包括鎖的名稱*/
public class Lock {private String name;private String value;public Lock(String name, String value) {this.name = name;this.value = value;}public String getName() {return name;}public String getValue() {return value;}}

搞一個redis分布式鎖的工具類

import com.sun.org.slf4j.internal.Logger;
import com.sun.org.slf4j.internal.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;import java.util.concurrent.TimeUnit;/*** 分布式鎖*/@Component
public class DistributedLockHandler {private static final Logger logger = LoggerFactory.getLogger(DistributedLockHandler.class);/*** 單個業務持有鎖的時間30s,防止死鎖*/private final static long LOCK_EXPIRE = 30 * 1000L;/*** 默認30ms嘗試一次*/private final static long LOCK_TRY_INTERVAL = 30L;/*** 默認嘗試20s*/private final static long LOCK_TRY_TIMEOUT = 20 * 1000L;@Autowiredprivate StringRedisTemplate template;/*** 嘗試獲取全局鎖** @param lock 鎖的名稱* @return true 獲取成功,false獲取失敗*/public boolean tryLock(Lock lock){return getLock(lock, LOCK_TRY_TIMEOUT, LOCK_TRY_INTERVAL, LOCK_EXPIRE);}/*** 嘗試獲取全局鎖** @param lock    鎖的名稱* @param timeout 獲取超時時間 單位ms* @return true 獲取成功,false獲取失敗*/public boolean tryLock(Lock lock, long timeout) {return getLock(lock, timeout, LOCK_TRY_INTERVAL, LOCK_EXPIRE);}/*** 嘗試獲取全局鎖** @param lock           鎖的名稱* @param timeout        獲取鎖的超時時間* @param tryInterval    多少毫秒嘗試獲取一次* @param lockExpireTime 鎖的過期* @return true 獲取成功,false獲取失敗*/public boolean tryLock(Lock lock, long timeout, long tryInterval, long lockExpireTime) {return getLock(lock, timeout, tryInterval, lockExpireTime);}/*** 操作redis獲取全局鎖** @param lock           鎖的名稱* @param timeout        獲取的超時時間* @param tryInterval    多少ms嘗試一次* @param lockExpireTime 獲取成功后鎖的過期時間* @return true 獲取成功,false獲取失敗*/public boolean getLock(Lock lock, long timeout, long tryInterval, long lockExpireTime){// 1. 鎖名不為空if (StringUtils.isEmpty(lock.getName()) || StringUtils.isEmpty(lock.getValue())) {return false;}// 2. 系統時間long startTime = System.currentTimeMillis();try{do{// 不存在鎖,上新鎖if (!template.hasKey(lock.getName())) {ValueOperations<String, String> ops = template.opsForValue();ops.setIfAbsent(lock.getName(), lock.getValue(), lockExpireTime, TimeUnit.MILLISECONDS);return true;} else {//已存在鎖logger.error("lock is exist!!!");}// 嘗試超過了設定值之后直接跳出循環,避免上新鎖時間過長// 例如A線程上新鎖,花費了10s,這10s內B線程無法獲取鎖,就會一直在循環里重試,設置超時時間為3s,一旦B線程重試超過3s就退出循環且生命周期結束。if (System.currentTimeMillis() - startTime > timeout) {return false;}// A線程剛獲取了鎖,B線程等待A線程釋放鎖Thread.sleep(tryInterval);}while(template.hasKey(lock.getName()));  // 3. redis中是否存在鎖}catch (Exception e){logger.error(e.getMessage());return false;}return false;}/*** 釋放鎖*/public void releaseLock(Lock lock){if (!StringUtils.isEmpty(lock.getName())) {template.delete(lock.getName());}}}

測試代碼,可以看到這是我們自己封裝的最終效果

@RestController
public class testDemo {@Autowiredprivate DistributedLockHandler distributedLockHandler;@RequestMapping("/index")public void index(){Lock lock=new Lock("lynn","min");if (distributedLockHandler.tryLock(lock)) {// 1. 成功獲取鎖try {//為了演示鎖的效果,這里睡眠5000毫秒System.out.println("執行方法");Thread.sleep(5000);}catch (Exception e){e.printStackTrace();}// 2. 釋放鎖distributedLockHandler.releaseLock(lock);}}}

以上結合業務場景探討了實現Redis分布式鎖時,為何使用線程休眠,超時時間,以及針對超時時間的一些優化方案。

接下來引入一個新的問題:

若定義鎖的過期時間是10s,此時A線程獲取了鎖然后執行業務代碼,但是業務代碼消耗時間花費了15s。這就會導致A線程還沒有執行完業務代碼,A線程卻釋放了鎖(因為10s到了),第11s B線程發現鎖已經釋放,重新獲取鎖也開始執行業務代碼。

此時多個線程同時執行業務代碼,我們使用鎖就是為了保證僅有一個線程執行這一塊業務代碼,說明這個鎖是失效的!

如何處理這個情況,涉及到了鎖延期操作,下一篇文章指出!

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

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

相關文章

2024開年,手機廠商革了自己的命

文&#xff5c;劉俊宏 編&#xff5c;王一粟 2024開年&#xff0c;AI終端的號角已經由手機行業吹響。 OPPO春節期間就沒閑著&#xff0c;首席產品官劉作虎在大年三十就迫不及待地宣布&#xff0c;OPPO正式進入AI手機時代。隨后在開年后就緊急召開了AI戰略發布會&#xff0c;…

【Antd】Form 表單獲取不到 Input 的值

文章目錄 今天遇到了一個奇怪的bug&#xff0c;Form表單中的Input組件的值&#xff0c;不能被Form獲取&#xff0c;導致輸入了內容&#xff0c;但是表單提交的時候值為undefined 報錯代碼 import { Button, Form, Input } from antd; import React from react;const App: Rea…

GaussDB SQL調優:建立合適的索引

背景 GaussDB是華為公司傾力打造的自研企業級分布式關系型數據庫&#xff0c;該產品具備企業級復雜事務混合負載能力&#xff0c;同時支持優異的分布式事務&#xff0c;同城跨AZ部署&#xff0c;數據0丟失&#xff0c;支持1000擴展能力&#xff0c;PB級海量存儲等企業級數據庫…

SQL中為什么不要使用1=1

最近看幾個老項目的SQL條件中使用了11&#xff0c;想想自己也曾經這樣寫過&#xff0c;略有感觸&#xff0c;特別拿出來說道說道。 編寫SQL語句就像炒菜&#xff0c;每一種調料的使用都可能會影響菜品的最終味道&#xff0c;每一個SQL條件的加入也可能會影響查詢的執行效率。那…

昨天Google發布了最新的開源模型Gemma,今天我來體驗一下

前言 看看以前寫的文章&#xff0c;業余搞人工智能還是很早之前的事情了&#xff0c;之前為了高工資&#xff0c;一直想從事人工智能相關的工作都沒有實現。現在終于可以安靜地系統地學習一下了。也是一邊學習一邊寫博客記錄吧。 昨天Google發布了最新的開源模型Gemma&#xf…

電商數據采集的幾個標準

面對體量巨大的電商數據&#xff0c;很多品牌會選擇對自己有用的數據進行分析&#xff0c;比如在控價過程中&#xff0c;需要對商品的價格數據進行監測&#xff0c;或者是需要做數據分析時&#xff0c;則需要采集到商品的價格、銷量、評價量、標題、店鋪名等信息&#xff0c;數…

Unity中.Net與Mono的關系

什么是.NET .NET是一個開發框架&#xff0c;它遵循并采用CIL(Common Intermediate Language)和CLR(Common Language Runtime)兩種約定&#xff0c; CIL標準為一種編譯標準&#xff1a;將不同編程語言&#xff08;C#, JS, VB等&#xff09;使用各自的編譯器&#xff0c;按照統…

JavaScript 原始值和引用值在變量復制時的異同

相比于其他語言&#xff0c;JavaScript 中的變量可謂獨樹一幟。正如 ECMA-262 所規定的&#xff0c;JavaScript 變量是松散類型的&#xff0c;而且變量不過就是特定時間點一個特定值的名稱而已。由于沒有規則定義變量必須包含什么數據類型&#xff0c;變量的值和數據類型在腳本…

mysql.service is not a native service, redirecting to systemd-sysv-install

字面意思&#xff1a;mysql.service不是本機服務&#xff0c;正在重定向到systemd sysv安裝 在CentOS上使用Systemd管理MySQL服務的具體步驟如下&#xff1a; 1、創建MySQL服務單元文件&#xff1a; 首先&#xff0c;你需要創建一個Systemd服務單元文件&#xff0c;以便Syste…

【Python筆記-設計模式】原型模式

一、說明 原型模式是一種創建型設計模式&#xff0c; 用于創建重復的對象&#xff0c;同時又能保證性能。 使一個原型實例指定了要創建的對象的種類&#xff0c;并且通過拷貝這個原型來創建新的對象。 (一) 解決問題 主要解決了對象的創建與復制過程中的性能問題。主要針對…

redhawk:使用ipf文件反標instance power

我正在「拾陸樓」和朋友們討論有趣的話題,你?起來吧? 拾陸樓知識星球入口 往期文章鏈接: Redhawk:Input Data Preparation 使用ptpx和redhawk報告功耗時差別總是很大,如果需要反標top/block的功耗值可以在gsr文件中使用BLOCK_POWER_FOR_SCALING的命令

Verilog刷題筆記35

題目&#xff1a; Create a 1-bit wide, 256-to-1 multiplexer. The 256 inputs are all packed into a single 256-bit input vector. sel0 should select in[0], sel1 selects bits in[1], sel2 selects bits in[2], etc. 解法&#xff1a; module top_module( input [255:…

Spring Cloud Alibaba-05-Gateway網關-02-斷言(Predicate)使用

Lison <dreamlison163.com>, v1.0.0, 2023.10.20 Spring Cloud Alibaba-05-Gateway網關-02-斷言(Predicate)使用 文章目錄 Spring Cloud Alibaba-05-Gateway網關-02-斷言(Predicate)使用通過時間匹配通過 Cookie 匹配通過 Header 匹配通過 Host 匹配通過請求方式匹配通…

C# CAD2016 cass10宗地Xdata數據寫入

一、 查看cass10寫入信息 C# Cad2016二次開發獲取XData信息&#xff08;二&#xff09; 一共有81條數據 XData value: QHDM XData value: 121321 XData value: SOUTH XData value: 300000 XData value: 141121JC10720 XData value: 權利人 XData value: 0702 XData value: YB…

2.居中方式總結

居中方式總結 經典真題 怎么讓一個 div 水平垂直居中 盒子居中 首先題目問到了如何進行居中&#xff0c;那么居中肯定分 2 個方向&#xff0c;一個是水平方向&#xff0c;一個是垂直方向。 水平方向居中 水平方向居中很簡單&#xff0c;有 2 種常見的方式&#xff1a; 設…

java面試題之mybatis篇

什么是ORM&#xff1f; ORM&#xff08;Object/Relational Mapping&#xff09;即對象關系映射&#xff0c;是一種數據持久化技術。它在對象模型和關系型數據庫直接建立起對應關系&#xff0c;并且提供一種機制&#xff0c;通過JavaBean對象去操作數據庫表的數據。 MyBatis通過…

MATLAB練習題:randperm函數的練習題

?講解視頻&#xff1a;可以在bilibili搜索《MATLAB教程新手入門篇——數學建模清風主講》。? MATLAB教程新手入門篇&#xff08;數學建模清風主講&#xff0c;適合零基礎同學觀看&#xff09;_嗶哩嗶哩_bilibili MATLAB中有一個非常有用的函數&#xff1a;randperm函數&…

華為算法題 go語言或者ptython

1 給定一個整數數組 nums 和一個整數目標值 target&#xff0c;請你在該數組中找出 和為目標值 target 的那 兩個 整數&#xff0c;并返回它們的數組下標。 你可以假設每種輸入只會對應一個答案。但是&#xff0c;數組中同一個元素在答案里不能重復出現。 你可以按任意順序返…

如何進行高性能架構的設計

一、前端優化 減少請求次數頁面靜態化邊緣計算 增加緩存控制&#xff1a;請求頭 減少圖像請求次數&#xff1a;多張圖片變成 一張。 減少腳本的請求次數&#xff1a;css和js壓縮&#xff0c;將多個文件壓縮成一個文件。 二、頁面靜態化 三、邊緣計算 后端優化 從三個方面進…

adb-monkey命令

目錄 adb shell monkey -p/-v 包名 次數 1、指定一個包 2、指定多個包 3、不指定包 Event percentages&#xff08;事件百分比&#xff09; 常見參數 --throttle 延遲時間 單位毫秒 --pct-touch 設定觸屏事件生成的百分比 --pct-motion 設定滑動事件生成…