Redis緩存雙寫一致性之更新策略

文章目錄

    • 1. 經典面試題
    • 2. 雙寫一致性
    • 3. 更新策略
    • 4. canal簡介
    • 5. Redis與Mysql數據雙寫一致性工程落地案例

1. 經典面試題

在這里插入圖片描述

  1. 上面的業務邏輯你用java代碼如何實現?
  2. 你只要用緩存,就可能會涉及到redis緩存與數據庫雙存儲雙寫,你只要是雙寫,就一定會有數據一致性問題,那么你如何解決數據一致性問題?
  3. 雙寫一致性,你先動redis緩存還是數據庫mysql中的哪一個?為什么?
  4. 延時雙刪你做過嗎?會有哪些問題?
  5. 有這個一種情況,微服務查詢redis無mysql有,為保證數據雙寫一致性回寫redis你需要注意什么?雙檢加鎖策略你了解過嗎?如何盡量避免緩存擊穿問題?
  6. redis和mysql雙寫100%會出紕漏,做不到強一致性,你如何保證最終一致性?

2. 雙寫一致性

首先,如果redis中有數據,就需要和數據庫中的值相同,如果redis中沒有數據,數據庫中的值要是最新值,且準備回寫redis。redis按照操作來分,分為兩種,即只讀緩存,實際環境中只能讀(實際業務中也是讀的操作居多),另一種是讀寫緩存,redis此時不僅可以讀數據,還可以寫數據(充當redis的門神),而這種緩存,redis與mysql數據同步有兩種策略:

  • 同步直寫策略

寫數據庫后也同步寫redis,緩存和數據庫中的數據一致,對于讀寫緩存來說,要想保證緩存和數據庫中的數據一致,就要采用同步直寫策略。

  • 異步緩寫策略

正常業務當中,mysql數據變動了,但是可以在業務上容許出現一定時間后才作用于redis,比如倉庫、物流系統等。如果出現異常情況,不得不將失敗的動作重新修補,有可能需要借助kafka或者RabbitMQ等消息中間件,實現重試重寫

有這個一種情況,微服務查詢redis無mysql有,為保證數據雙寫一致性回寫redis你需要注意什么?雙檢加鎖策略你了解過嗎?如何盡量避免緩存擊穿問題?

考慮一個場景,在QPS很高的一個項目中,有幾百萬個線程要訪問redis,發現redis中沒有這幾個數據,那么這些線程又要去訪問Mysql,Mysql此時在此海量的訪問下可能會奔潰,此時,mysql查詢完數據后要回寫redis,可能會出現redis數據覆蓋的問題(例如一個線程A已經回寫了a這條數據到redis中,在回寫的過程中另一個線程B正在讀mysql,B也是讀的a這條數據,然后回寫又覆蓋了A回寫到redis的數據)。總結下來,上面的場景揭露了兩個核心問題:

  1. Redis沒有數據,海量訪問可能會訪問mysql,導致mysql奔潰
  2. 海量線程回寫數據到redis,可能出現同一條數據多次回寫,出現數據覆蓋的問題

出現上面問題的本質原因就是訪問mysql數據庫和回寫數據的redis,這兩個操作并不是原子的。所以這時候就出現了雙檢加鎖策略

多個線程同時區去查詢數據庫的這條數據,那么我們可以在第一個查詢數據的請求上使用一個互斥鎖鎖住它。其他線程走到這一步拿不到鎖就等著,等第一個線程查詢到了數據,然后做緩存,后面的線程進來發現已經有緩存了,就直接走緩存。

public String getData(String key) {// 先嘗試從 Redis 中獲取數據String data = redisClient.get(key);if (data == null) {// 如果 Redis 中沒有數據,則加鎖并從 MySQL 中獲取數據synchronized (lock) {// 再次檢查 Redis 中是否已經存在數據,因為在獲取鎖之前可能其他線程已經更新了緩存data = redisClient.get(key);if (data == null) {try {// 從 MySQL 中獲取數據PreparedStatement statement = mysqlConn.prepareStatement("SELECT data FROM table WHERE key = ?");statement.setString(1, key);ResultSet resultSet = statement.executeQuery();if (resultSet.next()) {data = resultSet.getString("data");// 將數據存入 Redis,并設置過期時間redisClient.setex(key, 3600, data);}statement.close();} catch (SQLException e) {e.printStackTrace();}}}}return data;}

在加鎖前進行一次檢查的目的是避免不必要的加鎖操作。如果在加鎖之前就發現緩存中已經存在數據,那么就不需要加鎖,直接返回緩存中的數據,從而減少了對鎖資源的競爭,提高了并發性能。加鎖后進行一次檢查是為了處理競態條件。即使在加鎖前檢查時發現緩存中沒有數據,但在獲取鎖之前可能其他線程已經更新了緩存。因此,在獲取鎖之后,需要再次檢查緩存中是否已經存在數據,以避免重復向緩存中寫入數據或者以過期數據為準備數據返回。

3. 更新策略

  1. 你只要用緩存,就可能會涉及到redis緩存與數據庫雙存儲雙寫,你只要是雙寫,就一定會有數據一致性問題,那么你如何解決數據一致性問題?
  2. 雙寫一致性,你先動redis緩存還是數據庫mysql中的哪一個?為什么?

上面兩個的問題的最終目的都是達到最終一致性。給緩存設置過期時間,定義清理緩存并回寫,是保證最終一致性的解決方案。我們可以對存入緩存的數據設置過期時間,所有的寫操作都以數據庫為準,對緩存操作只是盡最大努力即可。也就是說,如果數據庫寫成功,緩存更新失敗,那么只要達過期時間,則后面的請求自然會從數據庫中讀取到最新值然后回填到緩存,達到一致性,切記,要以Mysql數據庫寫入庫為準

如何解決數據一致性問題,如果機器可以停機,那么就可以直接停機維護了,但一般公司的業務是不能隨便停止的,所以這里我們主要探討4種更新策略:(這一部分十分重要)

  1. 先更新數據庫,再更新緩存
  2. 先更新緩存,再更新數據庫
  3. 先刪除緩存,再刪除數據庫
  4. 先更新數據庫,再刪除緩存
  • 方案一:先更新數據庫后更新緩存

這種方案存在兩個問題:

問題一
例如先更新mysql的某商品的庫存,當前商品的庫存是100,更新為99個。先更新mysql修改為99,然后更新redis。此時假設異常出現(例如redis掛了),更新redis失敗了,這導致mysql里面的庫存是99,而redis里面還是100。上面問題發生,會導致數據庫里面和緩存里面的數據不一致,客戶端會讀到redis的臟數據。

問題二
A、B兩個線程發起調用:

A update mysql 100
A update redis 100
B update mysql 80
B update redis 80

但在多線程環境下,上面代碼的執行順序不是固定的,可能出現很多情況,例如下面情況:

A update redis 100
A update mysql 100
B update redis 80
B update mysql 80

現在就出現了mysql數據為100,而redis的數據為80的情況,出現了數據不一致問題,說到底這個問題就是高并發導致的問題。

  • 方案二:先更新緩存再更新數據庫

這種方案是不太推薦的,因為業務上通常把Mysql作為底單數據庫,保證最后解釋。考慮下面這種情況

A update redis 100
A update mysql 100
B update redis 80
B update mysql 80

同樣是多線程緩存可能出現下面情況:

A update redis 100
B update redis 80
B update mysql 80
A update mysql 100

上面現象同樣導致了緩存不一致的情況

針對上面這種情況,有一些解決方案可以考慮:
使用樂觀鎖或版本控制:在更新數據時,使用樂觀鎖或版本控制來確保更新的原子性。例如,在更新數據庫時,可以使用版本號字段或者時間戳來判斷數據是否被其他線程修改過,從而避免覆蓋其他線程的更新。
引入分布式鎖:在更新緩存和數據庫時,可以引入分布式鎖來保證操作的原子性。這樣可以確保在更新緩存和數據庫時只有一個線程可以執行,從而避免并發更新導致的問題。
使用消息隊列:將更新緩存和更新數據庫的操作放入消息隊列中按順序執行,這樣可以保證更新的順序性。例如,先將更新緩存的消息發送到消息隊列中,等待更新完成后再將更新數據庫的消息發送到消息隊列中,這樣可以保證先更新緩存再更新數據庫。
細化業務邏輯:根據具體業務情況,設計更加細化的業務邏輯來處理緩存和數據庫的更新順序。例如,可以將緩存和數據庫的更新操作放在同一個事務中執行,以保證操作的原子性和一致性。

  • 方案三:先刪除緩存再更新數據庫

這種方案同樣有異常問題,例如:A線程先刪除了redis里面的數據,然后去更新mysql,此時Mysql正在更新中,還沒有結束(比如出現了網絡延遲),此時B突然出現要來讀取緩存。

在這里插入圖片描述

此時redis里面的數據是空的,B線程來讀,先去讀redis里面的數據(但數據被A給刪除了),此時就有兩個問題了:

  • B去讀Mysql但是獲取了舊值(A沒更新完)
  • B獲取舊值后寫回redis(剛被A刪除的數據又寫回redis了)

在這里插入圖片描述
此時A更新完Mysql了,發現redis里面的緩存是臟數據,給A線程干的CPU直接燒了。于是緩存中的數據還是老數據,導致緩存中的數據是臟的,而且還會一直臟下去。

在這里插入圖片描述

延時雙刪你做過嗎?會有哪些問題?

在這里插入圖片描述

在注釋暫停兩秒的地方,加上Thread.sleep(2),就是為了讓b線程先從數據看中讀取數據,再把缺失的數據寫入緩存,然后線程A再進行刪除,所以,線程A sleep的時間,就需要大雨線程B讀取數據的時間,這樣一來,其它線程讀取數據時,會發現緩存缺失,所以會從數據看中讀取最新的值,因為這個方案會在第一次刪除緩存值后,延遲一段時間再次進行刪除,所以我們也把它叫做延遲雙刪

針對延遲雙刪也會出現一些問題:

這個刪除該休眠多久?

確定這個時間有兩種方法,第一種是在業務程序運行的時候,統計程序讀數據和寫緩存的操作時間,自行評估自己的項目的讀數據業務邏輯的耗時。以此為基礎進行估算,然后寫數據的休眠時間則在讀數據業務邏輯的耗時基礎上加上100ms。這么做的目的就是為了確保請求結束,寫請求可以刪除讀請求造成的緩存臟數據。另一種方案就是新啟動一個后臺監控程序,比如要講解的WatchDog。

這種同步淘汰策略,吞吐量降低怎么辦?

由于A線程會睡眠阻塞后面的業務,這確實會導致業務吞吐量降低。此時可以考慮第二次刪的時候,再開一個線程來異步刪除。

在這里插入圖片描述

  • 方案四:先更新數據庫再刪除緩存

這種方案相比上面三種方案是最好的,但它也存在它的問題。
在這里插入圖片描述

上面緩存刪除失敗或者來不及刪除,導致請求再次訪問redis緩存命中的時候,讀取到的緩存是舊的值。

redis和mysql雙寫100%會出紕漏,做不到強一致性,你如何保證最終一致性?

在這里插入圖片描述

  1. 我們可以把要刪除的緩存值或者是要更新的值暫存在消息隊列中(Kafka/RabbitMQ)
  2. 當程序沒有能夠成功地刪除緩存值或者是更新數據庫值時,可以從消息隊列中重寫讀取這些值面,然后再次進行刪除或者更新
  3. 如果能夠成功刪除或更新,我們就要把這些值從消息隊列中去除,以免重復操作,此時,我們也可以保證數據庫和緩存的數據一致了,否則還需要重試
  4. 如果重試超過一定次數后還沒有成功,我們就需要向業務層發送報錯信息了,通知運維人員

業務指導思想

微軟云

在這里插入圖片描述

阿里巴巴的canal

  • 總結

在這里插入圖片描述

4. canal簡介

如何知道Mysql數據發生了變動?

Mysql在更新數據的時候所有的更新操作都會記錄到binlog中,如果我們拿到了這個binlog我們相應就知道了Mysql數據庫的變動情況。所以我們需要一個技術,能監聽到mysql變動,且能通知到redis,這個中間件就是canal。

Canal是什么?

Canal是阿里巴巴的Mysql Binlog增量訂閱和消費組件,主要用途是用于Mysql數據庫增量日志數據的訂閱、消費和解析,是阿里巴巴開發并開源的,采用java語言開發。Canal模擬MySQL的slave角色,向MySQL請求binlog,然后解析binlog成為DBChange對象。

Canal能干什么?

  • 數據庫景象
  • 數據庫實時備份
  • 索引構建和實時維護(拆分異構索引、倒排索引等)
  • 業務cache刷新
  • 帶業務邏輯的增量數據處理

Canal工作原理?

首先了解一個傳統的Mysql主從復制的工作原理:

在這里插入圖片描述
Mysql主從復制將經歷下面這些過程:

  1. 當master主服務器上的數據發生改變時,則將其改變寫入到二進制日志文件當中
  2. slave從服務器會在一定時間間隔內對master服務器上的二進制文件進行探測,探測其是否發生變化(offset偏移量),如果探測到master主服務器的二進制事件日志發生了改變,則開始一個I/O Thread請求master二進制事件日志
  3. 同時master主服務器為每個I/O thread啟動一個dump Thread,用于向其發送二進制事件文件

參考Mysql主從復制的原理,canal就出現了。

在這里插入圖片描述

canal模擬Mysql slave的交互協議,偽裝自己為mysql slave,向mysql master 發送dump協議,Mysql master受到dump請求,開始推送binary log給slave(即canal),canal解析binary log對象(原始流為byte)。

5. Redis與Mysql數據雙寫一致性工程落地案例

  • Mysql配置

查看Mysql版本

SELECT VERSION();

在這里插入圖片描述
查看當前主機的binlog

SHOW MASTER STATUS;

在這里插入圖片描述
查看當前binlog是否對外開放

SHOW VARIABLES LIKE 'log_bin'

在這里插入圖片描述

默認時關閉的,這里我打開了

開啟binlog的寫入功能

打開Mysql的my.ini配置文件,進行以下配置

log-bin=mysql-bin #開啟binlog
binlog-format=ROW #選擇row模式
server_id=1 #配置Mysql replaction需要定義,不要和canal的slaveid相同

Row模式除了記錄sql語句之外,還會記錄每個字段的變化情況,能夠清楚的記錄每行記錄的變化歷史,但會占用更多空間
STATEMENT模式只記錄sql語句,但是沒有記錄上下文信息,在進行數據恢復的時候可能會丟失數據
MIX模式比較靈活的記錄,理論上說當遇到了表結構發生變更的情況,就會時statement模式,當遇到數據更新或者刪除的情況就會變味row模式

授權canal連接mysql

DROP USER IF EXISTS 'canal'@'%';
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';
GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' IDENTIFIED BY 'canal';
FLUSH PRIVILEGES;

在這里插入圖片描述

  • canal配置

下載地址

在這里插入圖片描述
解壓

解壓后放在指定的一個目錄就行

在這里插入圖片描述

配置

修改conf/example路徑下的instance.properties文件

  • 換成自己的mysql主機的master地址
  • 換成自己的在mysql新建立的canal賬戶

在這里插入圖片描述
啟動canal

./startup.sh 

查看是否啟動成功

查看example.log和canal.log即可
在這里插入圖片描述

在這里插入圖片描述

  • java程序開發

引入jar包

        <dependency><groupId>com.alibaba.otter</groupId><artifactId>canal.client</artifactId><version>1.1.3</version></dependency>

添加配置

spring:datasource:master:url: jdbc:mysql://localhost:3306/atguigudb?characterEncoding=utf-8&useSSL=falsedriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: 123456

業務類

public class RedisUtils {public static  final  String REDIS_IP_ADDR="127.0.0.1";public static JedisPool jedispool;static {JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();jedisPoolConfig.setMaxTotal(20);jedisPoolConfig.setMaxIdle(20);jedispool=new JedisPool(jedisPoolConfig,REDIS_IP_ADDR,6379,10000);}public static Jedis getJedis() throws Exception {if(null!=jedispool){return jedispool.getResource();}throw  new Exception("Jedis poll is not ok");}
}  
package com.jack.mybatis_plus.biz;import java.net.InetSocketAddress;
import java.util.List;import com.alibaba.fastjson.JSONObject;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.common.utils.AddressUtils;
import com.alibaba.otter.canal.protocol.Message;
import com.alibaba.otter.canal.protocol.CanalEntry.Column;
import com.alibaba.otter.canal.protocol.CanalEntry.Entry;
import com.alibaba.otter.canal.protocol.CanalEntry.EntryType;
import com.alibaba.otter.canal.protocol.CanalEntry.EventType;
import com.alibaba.otter.canal.protocol.CanalEntry.RowChange;
import com.alibaba.otter.canal.protocol.CanalEntry.RowData;
import com.jack.mybatis_plus.utils.RedisUtils;
import redis.clients.jedis.Jedis;import java.util.List;
import java.util.UUID;public class SimpleCanalClientExample {public static final Integer _60SECONDS=60;public static final String REDIS_IP_ADDR="127.0.0.1";public static void main(String args[]) {// 創建鏈接CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(REDIS_IP_ADDR,11111), "example", "", "");int batchSize = 1000;int emptyCount = 0;try {connector.connect();//connector.subscribe(".*\\..*"); 訂閱全部庫全部表connector.subscribe("atguigudb.jobs");  //只訂閱atguigudb數據庫的jobs表connector.rollback();//這幾個就指定了監聽程序·10分鐘結束int totalEmptyCount = 10*_60SECONDS;while (emptyCount < totalEmptyCount) {System.out.println("我是canal,每一秒一次正在監聽:"+ UUID.randomUUID().toString());Message message = connector.getWithoutAck(batchSize); // 獲取指定數量的數據,一次處理1000條long batchId = message.getId();int size = message.getEntries().size();if (batchId == -1 || size == 0) {//表示當前數據庫沒有變動emptyCount++;try {Thread.sleep(1000);} catch (InterruptedException e) {}} else {//計數器置0emptyCount = 0;// System.out.printf("message[batchId=%s,size=%s] \n", batchId, size);printEntry(message.getEntries());}connector.ack(batchId); // 提交確認// connector.rollback(batchId); // 處理失敗, 回滾數據}System.out.println("已經監聽了"+totalEmptyCount+"秒,無任何消息,請重啟!");} finally {connector.disconnect();}}private static void printEntry(List<Entry> entrys) {for (Entry entry : entrys) {if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {continue;}RowChange rowChage = null;try {//獲取已經變更的數據rowChage = RowChange.parseFrom(entry.getStoreValue());} catch (Exception e) {throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),e);}EventType eventType = rowChage.getEventType();System.out.println(String.format("================&gt; binlog[%s:%s] , name[%s,%s] , eventType : %s",entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),eventType));for (RowData rowData : rowChage.getRowDatasList()) {if (eventType == EventType.DELETE) {redisDelete(rowData.getAfterColumnsList());} else if (eventType == EventType.INSERT) {redisInsert(rowData.getAfterColumnsList());} else {redisUpdate(rowData.getAfterColumnsList());}}}}private static void redisUpdate(List<Column> columns) {JSONObject jsonObject=new JSONObject();for (Column column : columns) {System.out.println(column.getName()+" : "+column.getValue()+" update="+column.getUpdated());jsonObject.put(column.getName(),column.getValue());}if(columns.size()>0){try(Jedis jedis= RedisUtils.getJedis()){jedis.set(columns.get(0).getValue(),jsonObject.toJSONString());System.out.println("--------update after: "+jedis.get(columns.get(0).getValue()));}catch (Exception e){e.printStackTrace();}}}private static void redisDelete(List<Column> columns) {JSONObject jsonObject=new JSONObject();for (Column column : columns) {jsonObject.put(column.getName(),column.getValue());}if(columns.size()>0){try(Jedis jedis= RedisUtils.getJedis()){jedis.del(columns.get(0).getValue());}catch (Exception e){e.printStackTrace();}}}private static void redisInsert(List<Column> columns)  {JSONObject jsonObject=new JSONObject();for (Column column : columns) {System.out.println(column.getName()+" : "+column.getValue()+" insert="+column.getUpdated());jsonObject.put(column.getName(),column.getValue());}if(columns.size()>0){try(Jedis jedis= RedisUtils.getJedis()){jedis.set(columns.get(0).getValue(),jsonObject.toJSONString());}catch (Exception e){e.printStackTrace();}}}
}

測試

修改數據
在這里插入圖片描述

在這里插入圖片描述

被正確同步到redis

在這里插入圖片描述

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

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

相關文章

嵌入式學習day29 指針復習

1.指針&#xff1a; 1.提供一種間接訪問數據的方法 2.空間沒有名字,只有一個地址編號 2.指針: 1.地址:區分不同內存空間的編號 2.指針:指針就是地址,地址就是指針 3.指針變量:存放指針的變量稱為指針變量,簡稱為指針 3.指針的定義: int *p NULL; …

MyBatis中 #{} 和 ${} 區別

Mybatis的Mapper映射文件中&#xff0c;有兩種方式可以引用形參變量進行取值: #{} 和 ${}。本文將簡述兩種方式的區別和適用場景 取值引用 #{} 方式 #{}: 解析為SQL時&#xff0c;會將形參變量的值取出&#xff0c;并自動給其添加引號。 例如&#xff1a;當實參username&quo…

AI 筆記助手,你的思路整理助手

大家好&#xff0c;今天給大家介紹一款非常實用的 AI 筆記助手——AI Note。這款助手就像是一個貼心的小助手&#xff0c;能幫助我們整理筆記&#xff0c;提高學習和工作效率。 &#x1f916; AI Note 可以智能總結筆記內容&#xff0c;準確標記重點&#xff0c;讓我們更快地獲…

final關鍵字有什么作用

final關鍵字在Java中用于聲明變量、方法和類&#xff0c;表示它們的值或行為不能被修改。 被 final 修飾的類不可以被繼承 被 final 修飾的方法不可以被重寫 被 final 修飾的變量不可變&#xff0c;被 final 修飾的變量必須被顯式第指定初始值&#xff0c;還得注意的是&#…

學習助手:借助AI大模型,學習更高效!

在當今的數字時代&#xff0c;人工智能&#xff08;AI&#xff09;的崛起已經徹底改變了我們獲取信息、處理數據以及學習新知識的方式。AI大模型&#xff0c;特別是如OpenAI開發的GPT-4這類先進的技術&#xff0c;已成為學習和教育領域的一大助力。本文旨在探索如何借助AI大模型…

了解 SYN Flood 攻擊

文章目錄&#xff1a; 什么是 SYN Flood 攻擊&#xff1f;對網絡的影響SYN Flood 發生的跡象如何解決&#xff1f; 什么是 SYN Flood 攻擊&#xff1f; SYN Flood&#xff08;SYN 洪水攻擊&#xff09;是一種常見的分布式拒絕服務&#xff08;DDoS - Distributed Denial of Se…

購買騰訊云服務器請先領取代金券,2024騰訊云優惠

騰訊云優惠代金券領取入口共三個渠道&#xff0c;騰訊云新用戶和老用戶均可領取8888元代金券&#xff0c;可用于云服務器等產品購買、續費和升級使用&#xff0c;阿騰云atengyun.com整理騰訊云優惠券&#xff08;代金券&#xff09;領取入口、代金券查詢、優惠券兌換碼使用方法…

FL Studio選購指南:新手小白應該選擇哪個版本FL Studio?

很多打算入手正版FL Studio的新手朋友都會糾結一個問題&#xff1a;哪個版本的FL Studio更適合我&#xff0c;到底應該入手哪一款FL Studio&#xff1f;本文會介紹每個版本之間的差異點&#xff0c;并帶大家選擇適合自己的FL Sudio版本。 FL Studio全版本 在選購前有一些小知識…

UE5常見問題處理筆記

一、C工程中的文件出現很多頭文件找不到&#xff0c;比如&#xff1a;#include CoreMinimal.h文件提示找不到。 解決方法&#xff1a;在UE編輯器中選擇菜單Tools -> Refresh Visual Studio Project。 二、莫名其妙的編譯錯誤。 解決方法&#xff0c;找到工程根目錄下的Bi…

GO流程控制

1. if else 在Go語言中&#xff0c;關鍵字if是用于測試某個條件&#xff08;布爾型或邏輯型&#xff09;的語句&#xff0c;如果該條件成立&#xff0c;則會執行 if 后由大括號{}括起來的代碼塊&#xff0c;否則就忽略該代碼塊繼續執行后續的代碼。 if condition {// 條件為真…

qwen.cpp

1、Release模式 git clone --recursive https://github.com/QwenLM/qwen.cpp && cd qwen.cpp git submodule update --init --recursive python3 qwen_cpp/convert.py -i /mnt/workspace/qwen.cpp/Qianwen/qwen/Qwen-7B-Chat -t q4_0 -o qwen7b-ggml.bincmake -B buil…

什么是MAC地址? win10電腦查看MAC地址的多種方法

您是否知道連接到家庭網絡的每件硬件都有自己的身份&#xff1f;正如每個設備都分配有自己的 IP 地址一樣&#xff0c;每個硬件都有一個唯一的網絡標識符。 該標識符稱為MAC 地址。MAC 代表媒體訪問控制。您可能需要 MAC 地址來解決網絡問題或配置新設備。在 Windows 中查找您…

three.js 點乘判斷平行向量方向異同

效果&#xff1a; 代碼&#xff1a; <template><div><el-container><el-main><div class"box-card-left"><div id"threejs"></div><div>判斷的前提是兩個向量平行<el-button click"judge"…

一文掌握大模型提示詞技巧:從戰略到戰術

作者&#xff1a;明明如月學長&#xff0c; CSDN 博客專家&#xff0c;大廠高級 Java 工程師&#xff0c;《性能優化方法論》作者、《解鎖大廠思維&#xff1a;剖析《阿里巴巴Java開發手冊》》、《再學經典&#xff1a;《Effective Java》獨家解析》專欄作者。 熱門文章推薦&am…

pytest 的 request fixture:實現個性化測試需求

在前文章中&#xff0c;我們看到pytest_repeat源碼中有這樣一段 pytest.fixture def __pytest_repeat_step_number(request):marker request.node.get_closest_marker("repeat")count marker and marker.args[0] or request.config.option.count......看到參數為r…

Stable Cascade又升級了,現在只需要兩個模型

Stable Cascade這個模型&#xff0c;大家如果還有印象的話&#xff0c;是需要下載三個模型的&#xff0c;分別是Stage_a,Stage_b和Stage_c,如果全都下載下來&#xff0c;需要20多個G&#xff0c;但是最近使用ComfyUI做嘗試的時候&#xff0c;發現官方的案例中已經沒有用到單獨的…

手擼AI-2: 設置腳本參數與設置隨機種子

一.設置腳本參數 1. 代碼示例 在完整的模型訓練代碼中,我們時常能看到通過python train.py --params 來訓練模型,這也是在無UI界面的服務器上訓練模型最主要的方法,因此使用腳本并設置腳本參數尤為重要. 我們通常會將腳本設置的代碼寫在config,py中,再用訓練主函數main.py進行…

P4715 【深基16.例1】淘汰賽題解

題目 有&#xff08;n≤7&#xff09;個國家參加世界杯決賽圈且進入淘汰賽環節。已經知道各個國家的能力值&#xff0c;且都不相等。能力值高的國家和能力值低的國家踢比賽時高者獲勝。1號國家和2號國家踢一場比賽&#xff0c;勝者晉級。3號國家和4號國家也踢一場&#xff0c;…

C++用臨時對象構造新對象

C用臨時對象構造新對象 //用臨時對象構造同類型的新對象&#xff0c;該臨時對象不產生&#xff1b; // 直接用生成臨時對象的方法構造新對象&#xff0c;這是編譯器對代碼的優化&#xff0c;效率更高 #include<iostream> using namespace std; class MyClass { public:…

Golang 調度器 GPM模型

Golang 調度器 GPM模型 1 多進程/線程時代有了調度器需求 在多進程/多線程的操作系統中&#xff0c;就解決了阻塞的問題&#xff0c;因為一個進程阻塞cpu可以立刻切換到其他進程中去執行&#xff0c;而且調度cpu的算法可以保證在運行的進程都可以被分配到cpu的運行時間片。這…