主頁 > 後端開發 > 聊聊redis分布式鎖的8大坑

聊聊redis分布式鎖的8大坑

2021-09-26 12:55:25 後端開發

前言

在分布式系統中,由于redis分布式鎖相對于更簡單和高效,成為了分布式鎖的首先,被用到了很多業務場景當中,

尤其是分布式配置中心:apollo、nocos等的出現,讓zookeeper的地位越來越低了,zookeeper分布式鎖復雜度更高,想把它使用好并不容易,

所以我們還是好好使用redis分布式鎖吧,

不是說用了redis分布式鎖,就可以高枕無憂了,如果沒有用好,也會引來一些意想不到的麻煩,

今天我們重點聊聊redis分布式鎖的一些坑,給有需要的朋友一個參考,

最近無意間獲得一份BAT大廠大佬寫的刷題筆記,一下子打通了我的任督二脈,越來越覺得演算法沒有想象中那么難了,

BAT大佬寫的刷題筆記,讓我offer拿到手軟

1 非原子操作

使用redis的分布式鎖,我們首先想到的可能是setNx命令,

if (jedis.setnx(lockKey, val) == 1) {
   jedis.expire(lockKey, timeout);
}

容易,三下五除二,就可以把代碼寫好,

這段代碼確實可以加鎖成功,但你有沒有發現什么問題?

加鎖操作和后面的設定超時時間是分開的,并非原子操作

假如加鎖成功,但是設定超時時間失敗了,該lockKey就變成永不失效,假如在高并發場景中,有大量的lockKey加鎖成功了,但不會失效,有可能直接導致redis記憶體空間不足,

那么,有沒有保證原子性的加鎖命令呢?

答案是:有,請看下面,

2 忘了釋放鎖

上面說到使用setNx命令加鎖操作和設定超時時間是分開的,并非原子操作,

而在redis中還有set命令,該命令可以指定多個引數,

String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
    return true;
}
return false;

其中:

  • lockKey:鎖的標識
  • requestId:請求id
  • NX:只在鍵不存在時,才對鍵進行設定操作,
  • PX:設定鍵的過期時間為 millisecond 毫秒,
  • expireTime:過期時間

set命令是原子操作,加鎖和設定超時時間,一個命令就能輕松搞定,

nice

使用set命令加鎖,表面上看起來沒有問題,但如果仔細想想,加鎖之后,每次都要達到了超時時間才釋放鎖,會不會有點不合理?加鎖后,如果不及時釋放鎖,會有很多問題,

分布式鎖更合理的用法是:

  1. 手動加鎖
  2. 業務操作
  3. 手動釋放鎖
  4. 如果手動釋放鎖失敗了,則達到超時時間,redis會自動釋放鎖,

大致流程圖如下:

那么問題來了,如何釋放鎖呢?

try{
  String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
  if ("OK".equals(result)) {
      return true;
  }
  return false;
} finally {
    unlock(lockKey);
}  

需要捕獲業務代碼的例外,然后在finally中釋放鎖,換句話說就是:無論代碼執行成功或失敗了,都需要釋放鎖,

此時,有些朋友可能會問:假如剛好在釋放鎖的時候,系統被重啟了,或者網路斷線了,或者機房斷點了,不也會導致釋放鎖失敗?

這是一個好問題,因為這種小概率問題確實存在,

但還記得前面我們給鎖設定過超時時間嗎?即使出現例外情況造成釋放鎖失敗,但到了我們設定的超時時間,鎖還是會被redis自動釋放,

但只在finally中釋放鎖,就夠了嗎?

3 釋放了別人的鎖

做人要厚道,先回答上面的問題:只在finally中釋放鎖,當然是不夠的,因為釋放鎖的姿勢,還是不對,

哪里不對?

答:在多執行緒場景中,可能會出現釋放了別人的鎖的情況,

有些朋友可能會反駁:假設在多執行緒場景中,執行緒A獲取到了鎖,如果執行緒A沒有釋放鎖,執行緒B是獲取不到鎖的,何來釋放了別人鎖之說?

答:假如執行緒A和執行緒B,都使用lockKey加鎖,執行緒A加鎖成功了,但是由于業務功能耗時時間很長,超過了設定的超時時間,這時候,redis會自動釋放lockKey鎖,此時,執行緒B就能給lockKey加鎖成功了,接下來執行它的業務操作,恰好這個時候,執行緒A執行完了業務功能,釋放了鎖lockKey,這不就出問題了,執行緒B的鎖,被執行緒A釋放了,

我想這個時候,執行緒B肯定哭暈在廁所里,并且嘴里還振振有詞,

那么,如何解決這個問題呢?

不知道你們注意到沒?在使用set命令加鎖時,除了使用lockKey鎖標識,還多設定了一個引數:requestId,為什么要需要記錄requestId呢?

答:requestId是在釋放鎖的時候用的,

if (jedis.get(lockKey).equals(requestId)) {
    jedis.del(lockKey);
    return true;
}
return false;

在釋放鎖的時候,先獲取到該鎖的值(之前設定值就是requestId),然后判斷跟之前設定的值是否相同,如果相同才允許洗掉鎖,回傳成功,如果不同,則直接回傳失敗,

換句話說就是:自己只能釋放自己加的鎖,不允許釋放別人加的鎖,

這里為什么要用requestId,用userId不行嗎?

答:如果用userId的話,對于請求來說并不唯一,多個不同的請求,可能使用同一個userId,而requestId是全域唯一的,不存在加鎖和釋放鎖亂掉的情況,

此外,使用lua腳本,也能解決釋放了別人的鎖的問題:

if redis.call('get', KEYS[1]) == ARGV[1] then 
 return redis.call('del', KEYS[1]) 
else 
  return 0 
end

lua腳本能保證查詢鎖是否存在和洗掉鎖是原子操作,用它來釋放鎖效果更好一些,

說到lua腳本,其實加鎖操作也建議使用lua腳本:

if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('hset', KEYS[1], ARGV[2], 1); 
    redis.call('pexpire', KEYS[1], ARGV[1]); 
 return nil; 
end
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)
   redis.call('hincrby', KEYS[1], ARGV[2], 1); 
   redis.call('pexpire', KEYS[1], ARGV[1]); 
  return nil; 
end; 
return redis.call('pttl', KEYS[1]);

這是redisson框架的加鎖代碼,寫的不錯,大家可以借鑒一下,

4 大量失敗請求

上面的加鎖方法看起來好像沒有問題,但如果你仔細想想,如果有1萬的請求同時去競爭那把鎖,可能只有一個請求是成功的,其余的9999個請求都會失敗,

在秒殺場景下,會有什么問題?

答:每1萬個請求,有1個成功,再1萬個請求,有1個成功,如此下去,直到庫存不足,這就變成均勻分布的秒殺了,跟我們想象中的不一樣,

如何解決這個問題呢?

此外,還有一種場景:

比如,有兩個執行緒同時上傳檔案到sftp,上傳檔案前先要創建目錄,假設兩個執行緒需要創建的目錄名都是當天的日期,比如:20210920,如果不做如何控制,這樣直接并發的創建,第二個執行緒會失敗,

有同學會說:這還不容易,加一個redis分布式鎖就能解決問題了,此外再判斷一下,如果目錄已經存在就不創建,只有目錄不存在才需要創建,

偽代碼如下:

try {
  String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
  if ("OK".equals(result)) {
    if(!exists(path)) {
       mkdir(path);
    }
    return true;
  }
} finally{
    unlock(lockKey,requestId);
}  
return false;

答:只是加redis分布式鎖是不夠的,因為第二個請求如果加鎖失敗了,接下來,是回傳失敗呢?還是回傳成功呢?


顯然肯定是不能回傳失敗的,如果回傳失敗了,這個問題還是沒有被解決,如果檔案還沒有上傳成功,直接回傳成功會有更大的問題,頭疼,到底該如何解決呢?

答:使用自旋鎖

try {
  Long start = System.currentTimeMillis();
  while(true) {
     String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
     if ("OK".equals(result)) {
        if(!exists(path)) {
           mkdir(path);
        }
        return true;
     }
     
     long time = System.currentTimeMillis() - start;
      if (time>=timeout) {
          return false;
      }
      try {
          Thread.sleep(50);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
  }
} finally{
    unlock(lockKey,requestId);
}  
return false;

在規定的時間,比如500毫秒內,自旋不斷嘗試加鎖(說白了,就是在死回圈中,不斷嘗試加鎖),如果成功則直接回傳,如果失敗,則休眠50毫秒,再發起新一輪的嘗試,如果到了超時時間,還未加鎖成功,則直接回傳失敗,

5 鎖重入問題

我們都知道redis分布式鎖是互斥的,如果我們對某個key加鎖了,如果該key對應的鎖還沒失效,再用相同key去加鎖,大概率會失敗,

沒錯,大部分場景是沒問題的,

為什么說是大部分場景呢?

因為還有這樣的場景:

假設在某個請求中,需要獲取一顆滿足條件的選單樹或者分類樹,我們以選單為例,這就需要在介面中從根節點開始,遞回遍歷出所有滿足條件的子節點,然后組裝成一顆選單樹,

需要注意的是選單不是一成不變的,在后臺系統中運營同學可以動態添加、修改和洗掉選單,為了保證在并發的情況下,每次都可能獲取最新的資料,這里可以加redis分布式鎖,

加redis分布式鎖的思路是對的,但接下來問題來了,在遞回方法中遞回遍歷多次,每次都是加的同一把鎖,遞回第一層當然是可以加鎖成功的,但遞回第二層、第三層…第N層,不就會加鎖失敗了?

遞回方法中加鎖的偽代碼如下:

private int expireTime = 1000;

public void fun(int level,String lockKey,String requestId){
  try{
     String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
     if ("OK".equals(result)) {
        if(level<=10){
           this.fun(++level,lockKey,requestId);
        } else {
           return;
        }
     }
     return;
  } finally {
     unlock(lockKey,requestId);
  }
}

如果你直接這么用,看起來好像沒有問題,但最終執行程式之后發現,等待你的結果只有一個:出現例外

因為從根節點開始,第一層遞回加鎖成功,還沒釋放說,就直接進入第二層遞回,因為requestId作為key的鎖已經存在,所以第二層遞回大概率會加鎖失敗,然后回傳到第一層,第一層接下來正常釋放鎖,然后整個遞回方法直接回傳了,

這下子,大家知道出現什么問題了吧?

沒錯,遞回方法其實只執行了第一層遞回就回傳了,其他層遞回由于加鎖失敗,根本沒法執行,

那么這個問題該如何解決呢?

答:使用可重入鎖

我們以redisson框架為例,它的內部實作了可重入鎖的功能,

古時候有句話說得好:為人不識陳近南,便稱英雄也枉然,

我說:分布式鎖不識redisson,便稱好鎖也枉然,哈哈哈,只是自娛自樂一下,

由此可見,redisson在redis分布式鎖中的江湖地位很高,

偽代碼如下:

private int expireTime = 1000;

public void run(String lockKey) {
  RLock lock = redisson.getLock(lockKey);
  this.fun(lock,1);
}

public void fun(RLock lock,int level){
  try{
      lock.lock(5, TimeUnit.SECONDS);
      if(level<=10){
         this.fun(lock,++level);
      } else {
         return;
      }
  } finally {
     lock.unlock();
  }
}

上面的代碼也許并不完美,這里只是給了一個大致的思路,如果大家有這方面需求的話,可以引數一下,

接下來,聊聊redisson可重入鎖的實作原理,

加鎖主要是通過以下腳本實作的:

if (redis.call('exists', KEYS[1]) == 0) 
then  
   redis.call('hset', KEYS[1], ARGV[2], 1);        redis.call('pexpire', KEYS[1], ARGV[1]); 
   return nil; 
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) 
then  
  redis.call('hincrby', KEYS[1], ARGV[2], 1); 
  redis.call('pexpire', KEYS[1], ARGV[1]); 
  return nil; 
end;
return redis.call('pttl', KEYS[1]);

其中:

  • KEYS[1]: 鎖名
  • ARGV[1]: 過期時間
  • ARGV[2]: uuid + “:” + threadId,可認為是requestId
  1. 先判斷如果鎖名不存在,則加鎖,
  2. 然后判斷判斷如果鎖名和requestId值都存在,則使用hincrby命令給該鎖名和requestId值計數,每次都加1,注意一下,這里就是重入鎖的關鍵,鎖重入一次就加1,
  3. 如果鎖名存在,但值不是requestId,則回傳過期時間,

釋放鎖主要是通過以下腳本實作的:

if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) 
then 
  return nil
end
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1)
if (counter > 0) 
then 
    redis.call('pexpire', KEYS[1], ARGV[2]);       return 0; 
 else 
   redis.call('del', KEYS[1]); 
   redis.call('publish', KEYS[2], ARGV[1]); 
   return 1; 
end; 
return nil
  1. 先判斷如果鎖名和requestId值不存在,則時間回傳,
  2. 如果鎖名和requestId值存在,則重入鎖減1,
  3. 如果減1后,重入鎖的value值還大于0,說明還有參考,則重試設定過期時間,
  4. 如果減1后,重入鎖的value值還等于0,則可以洗掉鎖,然后發訊息通知等待執行緒搶鎖,

再次強調一下,如果你們系統可以容忍資料暫時不一致,不加鎖也行,我在這里只是舉個例子,本節內容并不適用于所有場景,

6 鎖競爭問題

如果有大量寫入的場景,使用普通的redis分布式鎖是沒有問題的,

但如果有些業務場景,寫入的操作比較少,反而有大量讀取的操作,直接使用普通的redis分布式鎖,性能會不會不太好?

我們都知道,鎖的粒度越粗,多個執行緒搶鎖時競爭就越激烈,造成多個執行緒鎖等待的時間也就越長,性能也就越差,

所以,提升redis分布式鎖性能的第一步,就是要把鎖的粒度變細,

6.1 讀寫鎖

眾所周知,加鎖的目的是為了保證,在并發環境中讀寫資料的安全性,即不會出現資料錯誤或者不一致的情況,

但在絕大多數實際業務場景中,一般是讀資料的頻率遠遠大于寫資料,而執行緒間的并發讀操作是并不涉及并發安全問題,我們沒有必要給讀操作加互斥鎖,只要保證讀寫、寫寫并發操作上鎖是互斥的就行,這樣可以提升系統的性能,

我們以redisson框架為例,它內部已經實作了讀寫鎖的功能,

讀鎖的偽代碼如下:

RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
RLock rLock = readWriteLock.readLock();
try {
    rLock.lock();
    //業務操作
} catch (Exception e) {
    log.error(e);
} finally {
    rLock.unlock();
}

寫鎖的偽代碼如下:

RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
RLock rLock = readWriteLock.writeLock();
try {
    rLock.lock();
    //業務操作
} catch (InterruptedException e) {
   log.error(e);
} finally {
    rLock.unlock();
}

將讀鎖和寫鎖分開,最大的好處是提升讀操作的性能,因為讀和讀之間是共享的,不存在互斥性,而我們的實際業務場景中,絕大多數資料操作都是讀操作,所以,如果提升了讀操作的性能,也就會提升整個鎖的性能,

下面總結一個讀寫鎖的特點:

  • 讀與讀是共享的,不互斥
  • 讀與寫互斥
  • 寫與寫互斥

6.2 鎖分段

此外,為了減小鎖的粒度,比較常見的做法是將大鎖:分段

在java中ConcurrentHashMap,就是將資料分為16段,每一段都有單獨的鎖,并且處于不同鎖段的資料互不干擾,以此來提升鎖的性能,

放在實際業務場景中,我們可以這樣做:

比如在秒殺扣庫存的場景中,現在的庫存中有2000個商品,用戶可以秒殺,為了防止出現超賣的情況,通常情況下,可以對庫存加鎖,如果有1W的用戶競爭同一把鎖,顯然系統吞吐量會非常低,

為了提升系統性能,我們可以將庫存分段,比如:分為100段,這樣每段就有20個商品可以參與秒殺,

在秒殺的程序中,先把用戶id獲取hash值,然后除以100取模,模為1的用戶訪問第1段庫存,模為2的用戶訪問第2段庫存,模為3的用戶訪問第3段庫存,后面以此類推,到最后模為100的用戶訪問第100段庫存,


如此一來,在多執行緒環境中,可以大大的減少鎖的沖突,以前多個執行緒只能同時競爭1把鎖,尤其在秒殺的場景中,競爭太激烈了,簡直可以用慘絕人寰來形容,其后果是導致絕大數執行緒在鎖等待,現在多個執行緒同時競爭100把鎖,等待的執行緒變少了,從而系統吞吐量也就提升了,

需要注意的地方是:將鎖分段雖說可以提升系統的性能,但它也會讓系統的復雜度提升不少,因為它需要引入額外的路由演算法,跨段統計等功能,我們在實際業務場景中,需要綜合考慮,不是說一定要將鎖分段,

7 鎖超時問題

前面提到過,如果執行緒A加鎖成功了,但是由于業務功能耗時時間很長,超過了設定的超時時間,這時候redis會自動釋放執行緒A加的鎖,

有些朋友可能會說:到了超時時間,鎖被釋放了就釋放了唄,對功能又沒啥影響,

答:錯,錯,錯,對功能其實有影響,

通常我們加鎖的目的是:為了防止訪問臨界資源時,出現資料例外的情況,比如:執行緒A在修改資料C的值,執行緒B也在修改資料C的值,如果不做控制,在并發情況下,資料C的值會出問題,

為了保證某個方法,或者段代碼的互斥性,即如果執行緒A執行了某段代碼,是不允許其他執行緒在某一時刻同時執行的,我們可以用synchronized關鍵字加鎖,

但這種鎖有很大的局限性,只能保證單個節點的互斥性,如果需要在多個節點中保持互斥性,就需要用redis分布式鎖,

做了這么多鋪墊,現在回到正題,

假設執行緒A加redis分布式鎖的代碼,包含代碼1和代碼2兩段代碼,

由于該執行緒要執行的業務操作非常耗時,程式在執行完代碼1的時,已經到了設定的超時時間,redis自動釋放了鎖,而代碼2還沒來得及執行,

此時,代碼2相當于裸奔的狀態,無法保證互斥性,假如它里面訪問了臨界資源,并且其他執行緒也訪問了該資源,可能就會出現資料例外的情況,(PS:我說的訪問臨界資源,不單單指讀取,還包含寫入)

那么,如何解決這個問題呢?

答:如果達到了超時時間,但業務代碼還沒執行完,需要給鎖自動續期,

我們可以使用TimerTask類,來實作自動續期的功能:

Timer timer = new Timer(); 
timer.schedule(new TimerTask() {
    @Override
    public void run(Timeout timeout) throws Exception {
      //自動續期邏輯
    }
}, 10000, TimeUnit.MILLISECONDS);
        

獲取鎖之后,自動開啟一個定時任務,每隔10秒鐘,自動重繪一次過期時間,這種機制在redisson框架中,有個比較霸氣的名字:watch dog,即傳說中的看門狗

當然自動續期功能,我們還是優先推薦使用lua腳本實作,比如:

if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
   redis.call('pexpire', KEYS[1], ARGV[1]);
  return 1; 
end;
return 0;

需要注意的地方是:在實作自動續期功能時,還需要設定一個總的過期時間,可以跟redisson保持一致,設定成30秒,如果業務代碼到了這個總的過期時間,還沒有執行完,就不再自動續期了,

自動續期的功能是獲取鎖之后開啟一個定時任務,每隔10秒判斷一下鎖是否存在,如果存在,則重繪過期時間,如果續期3次,也就是30秒之后,業務方法還是沒有執行完,就不再續期了,

8 主從復制的問題

上面花了這么多篇幅介紹的內容,對單個redis實體是沒有問題的,

but,如果redis存在多個實體,比如:做了主從,或者使用了哨兵模式,基于redis的分布式鎖的功能,就會出現問題,

具體是什么問題?

假設redis現在用的主從模式,1個master節點,3個slave節點,master節點負責寫資料,slave節點負責讀資料,

本來是和諧共處,相安無事的,redis加鎖操作,都在master上進行,加鎖成功后,再異步同步給所有的slave,

突然有一天,master節點由于某些不可逆的原因,掛掉了,

這樣需要找一個slave升級為新的master節點,假如slave1被選舉出來了,


如果有個鎖A比較悲催,剛加鎖成功master就掛了,還沒來得及同步到slave1,

這樣會導致新master節點中的鎖A丟失了,后面,如果有新的執行緒,使用鎖A加鎖,依然可以成功,分布式鎖失效了,

那么,如果解決這個問題呢?

答:redisson框架為了解決這個問題,提供了一個專門的類:RedissonRedLock,使用了Redlock演算法,

RedissonRedLock解決問題的思路如下:

  1. 需要搭建幾套相互獨立的redis環境,假如我們在這里搭建了3套,
  2. 每套環境都有一個redisson node節點,
  3. 多個redisson node節點組成了RedissonRedLock,
  4. 環境包含:單機、主從、哨兵和集群模式,可以是一種或者多種混合,

在這里我們以主從為例,架構圖如下:

RedissonRedLock加鎖程序如下:

  1. 回圈向所有的redisson node節點加鎖,假設節點數為N,例子中N等于5,
  2. 如果在N個節點當中,有N/2 + 1個節點加鎖成功了,那么整個RedissonRedLock加鎖是成功的,
  3. 如果在N個節點當中,小于N/2 + 1個節點加鎖成功,那么整個RedissonRedLock加鎖是失敗的,
  4. 如果中途發現各個節點加鎖的總耗時,大于等于設定的最大等待時間,則直接回傳失敗,

從上面可以看出,使用Redlock演算法,確實能解決多實體場景中,假如master節點掛了,導致分布式鎖失效的問題,

但也引出了一些新問題,比如:

  1. 需要額外搭建多套環境,申請更多的資源,需要評估一下,經費是否充足,
  2. 如果有N個redisson node節點,需要加鎖N次,最少也需要加鎖N/2+1次,才知道redlock加鎖是否成功,顯然,增加了額外的時間成本,有點得不償失,

由此可見,在實際業務場景,尤其是高并發業務中,RedissonRedLock其實使用的并不多,

最近無意間獲得一份BAT大廠大佬寫的刷題筆記,一下子打通了我的任督二脈,越來越覺得演算法沒有想象中那么難了,

BAT大佬寫的刷題筆記,讓我offer拿到手軟

在分布式環境中,CAP是繞不過去的,

CAP指的是在一個分布式系統中:

  • 一致性(Consistency)
  • 可用性(Availability)
  • 磁區容錯性(Partition tolerance)

這三個要素最多只能同時實作兩點,不可能三者兼顧,

如果你的實際業務場景,更需要的是保證資料一致性,那么請使用CP型別的分布式鎖,比如:zookeeper,它是基于磁盤的,性能可能沒那么好,但資料一般不會丟,

如果你的實際業務場景,更需要的是保證資料高可用性,那么請使用AP型別的分布式鎖,比如:redis,它是基于記憶體的,性能比較好,但有丟失資料的風險,

其實,在我們絕大多數分布式業務場景中,使用redis分布式鎖就夠了,真的別太較真,因為資料不一致問題,可以通過最終一致性方案解決,但如果系統不可用了,對用戶來說是暴擊,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/303029.html

標籤:java

上一篇:Java多型,抽象類和介面還不熟悉?comparable和comparator,Cloneable使用

下一篇:2021秋招最新JAVA面試題|JVM剖析與常用的調優總結

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more