MySql的MVCC機制

来源:https://www.cnblogs.com/qisi/archive/2023/06/13/mvcc.html
-Advertisement-
Play Games

事務隔離級別遺留問題: 在讀已提交的級別下,事務B可以讀到事務A持有寫鎖的的記錄,且讀到的是未更新前的,為何寫讀沒有衝突? 可重覆讀級別,事務B可以更新事務A理論上應該已經獲取讀鎖的記錄,且更新後,事務A依然可以讀到數據,為何讀-寫-讀沒有衝突? 在可重覆讀級別,幻讀沒有產生 其中,前兩個問題就是因 ...


事務隔離級別遺留問題:

  1. 在讀已提交的級別下,事務B可以讀到事務A持有寫鎖的的記錄,且讀到的是未更新前的,為何寫讀沒有衝突?

  2. 可重覆讀級別,事務B可以更新事務A理論上應該已經獲取讀鎖的記錄,且更新後,事務A依然可以讀到數據,為何讀-寫-讀沒有衝突?

  3.  在可重覆讀級別,幻讀沒有產生

  其中,前兩個問題就是因為mvcc機制(讀鎖的一種優化機制),通過不加讀鎖,避免讀寫衝突,進而提高了性能。

為什麼要有MVCC機制?

  1. 在讀已提交的級別下,由於是給讀加鎖來保證讀已提交, 如果事務A持有寫鎖,為了保證讀已提交,事務B必須等待事務A提交之後才可以讀;其他的讀事務也是這樣的情況,效率太低

  2. 在可重覆讀級別,為了保證可重覆讀,如果事務A持有讀鎖,為了第二次讀到的一樣,其他所有寫事務必須等待讀完才可以,同樣效率低

那麼很自然的想到,無論讀事務是先產生還是後產生,如果這個時候還存在寫事務沒有執行,或者需要執行;那麼就應該讓讀事務讀到目前最新的值,且寫事務可以更新;只不過讀事務在寫事務提交更新後,依據隔離級別是否可見最新更新即可。這就是MVCC機制的核心能力,將讀鎖幹掉。

MVCC機制核心組件

MVCC機制由版本鏈、undolog、readview三大核心構成

版本鏈

猜測很多人第一次看到MVCC的版本都是和我一樣在各種各樣的博客文章上,或者可能是在一些課程專欄或者《高性能mysql》這本書的mvcc部分看到的,那麼在你的理解中,版本的底層是什麼樣子呢? innodb引擎資料庫中的每一條記錄上,我們都可以認為上面有3個隱藏欄位,分別是DB_ROW_ID(不在此次討論範圍),DB_TRX_ID和DB_ROLL_PTR,如下圖一樣

 

在我的理解中, DB_TRX_ID就是插入或者更新時,當前事務的trx_id,由全局事務管理器分配的遞增的一個id; DB_ROLL_PTR存儲的undolog中當前記錄上一個版本的指針,先姑且記住這是一個指針。 當插入一條記錄時 在這條記錄的DB_TRX_ID填入當前事務的id,由於沒有歷史版本,所以DB_ROLL_PTR為空 當更新一條記錄時 由於這個時候存在歷史版本,所以需要將老版本的數據寫到undolog里,然後構建指針,將DB_TRX_ID更新為當前事務的id,將DB_ROLL_PTR更新為剛纔構建的指針,以及更新需要更新的欄位。 當刪除一條記錄時(這個不太確定,主觀猜測) 猜測是將老記錄寫到undolog,然後構建指針,新記錄DB_TRX_ID更新為當前事務的id,將DB_ROLL_PTR更新為剛纔構建的指針,但是沒有需要更新的欄位。而且mysql不會立即刪除,記錄上有一個info_bits欄位,會標記上刪除標識(REC_INFO_DELETED_FLAG),後續由purge線程(不瞭解,姑且認為是個scheduleTask吧)刪除 這樣,當多次更新之後,新記錄存儲的永遠都是最新操作的事務id,並通過指針指向了老版本,老版本還指向了更老的版本...等等,最終構成了一個版本鏈

Readview

理論:

在周志明老師的鳳凰架構(或者極客時間的‘周志明的軟體架構課’)中對mvcc簡單介紹到
隔離級別是可重覆讀:總是讀取 CREATE_VERSION 小於或等於當前事務 ID 的記錄,在這個前提下,如果數據仍有多個版本,則取最新(事務 ID 最大)的。 隔離級別是讀已提交:總是取最新的版本即可,即最近被 Commit 的那個版本的數據記錄。
在mysql官網中是這麼描述的
If the transaction isolation level is REPEATABLE READ (the default level), all consistent reads within the same transaction read the snapshot established by the first such read in that transaction. You can get a fresher snapshot for your queries by committing the current transaction and after that issuing new queries. With READ COMMITTED isolation level, each consistent read within a transaction sets and reads its own fresh snapshot.
翻譯: 隔離級別是可重覆讀:在同一個事務中,一致性讀總是去讀在該事務第一次讀取時生成的快照。 隔離級別是讀已提交:事務中的每次讀取都取自己新生成的快照。
相比之下,周老師形容的更貼近隔離級別的概念上,官方的描述則是底層的具體實現邏輯。 兩者結合一下就是 可重覆讀:通過在每個事物只讀取第一次select時生成的快照和undolog比較,根據一個可見性規則判斷,是否可以讀當前版本的記錄,可以就返回,不行就繼續比較再上一個版本,直到最老的版本; 讀已提交:除了每次讀取都會使用最新的快照,後面的都和可重覆讀的邏輯一樣。 為什麼我這裡說的是可見性規則呢? 是因為周老師描述里“總是讀取 CREATE_VERSION 小於或等於當前事務 ID 的記錄” 很容易錯誤的理解為當前版本記錄里的trx_id<=快照創建時的事務id(create_trx_id)就都可見,真正的判斷邏輯並不只是一個create_trx_id就能搞定的。 但這裡先不展開講,自己想一下為什麼不行,下麵的圖可能會給你一點靈感,接下來我們先去讀一下“可見性規則”的底層源碼。

可見性規則底層實現

ReadView類

storage/innobase/include/read0types.h:47
//ReadView類
class ReadView {
...
private:
  /** trx id of creating transaction, set to TRX_ID_MAX for free
  views. */
  //創建快照的時候,快照對應的事務id,只有含有寫操作的才會分配真正的事務id
  trx_id_t m_creator_trx_id;
/** Set of RW transactions that was active when this snapshot
  was taken */
  //活躍的讀寫事務id列表,從trx_sys->rw_trx_ids抄過來的
  ids_t m_ids;
/** The read should not see any transaction with trx id >= this
  value. In other words, this is the "high water mark". */
  //賦值是即將分配的下一個事務id,所以大於等於這個id的記錄對當前事務來說都是不可見的
  trx_id_t m_low_limit_id;

/** The read should see all trx ids which are strictly
  smaller (<) than this value.  In other words, this is the
  low water mark". */
  //m_ids不為空就是ids.get(0),為空則是m_low_limit_id,所以小於這個事務id的就代表著快照建立的時候
  //已經不是活躍事務了,即已經提交了,所以一定可以看到這些事務的改動記錄
  trx_id_t m_up_limit_id;
....
  }

初始化賦值的時候

//read0read.cc
//row_search_mvcc -> trx_assign_read_view -> MVCC::view_open ->

void ReadView::prepare(trx_id_t id) {
  ut_ad(trx_sys_mutex_own());

  m_creator_trx_id = id;

  m_low_limit_no = trx_get_serialisation_min_trx_no();

  m_low_limit_id = trx_sys_get_next_trx_id_or_no();

  ut_a(m_low_limit_no <= m_low_limit_id);

  if (!trx_sys->rw_trx_ids.empty()) {
    copy_trx_ids(trx_sys->rw_trx_ids);
  } else {
    m_ids.clear();
  }

/* The first active transaction has the smallest id. */
  m_up_limit_id = !m_ids.empty() ? m_ids.front() : m_low_limit_id;

  ut_a(m_up_limit_id <= m_low_limit_id);

  ut_d(m_view_low_limit_no = m_low_limit_no);
  m_closed = false;
}

判斷某個版本的記錄是否可見?

//read0types.h
 bool changes_visible(trx_id_t id,
                                   const table_name_t &name) const {
  ut_ad(id > 0);
//如果當前版本記錄上的事務id(DB_TRX_ID)小於低水位或者等於當前事務,
//那麼要麼就是自己更改的,要麼就是歷史上已經提交了的,所以可以讀到
  if (id < m_up_limit_id || id == m_creator_trx_id) {
    return (true);
  }

  check_trx_id_sanity(id, name);
//如果當前版本記錄上的事務id(DB_TRX_ID)大於高水位,那麼就是在當前快照生成後生成的事務,一律看不到
  if (id >= m_low_limit_id) {
    return (false);
  //這一步我沒有理解,
  } else if (m_ids.empty()) {
    return (true);
  }

  const ids_t::value_type *p = m_ids.data();
//二分查找,如果活躍的事務裡面沒有,那麼就返回true
//這裡我是這麼理解的,[低水位,高水位]包含活水和死水,即活躍的事務和已經提交的事務
//假如存在事務1是活躍的,事物2是已提交的,事務3是活躍的,我們在事務4的時候開啟快照,很明顯我們只能讀到事務2或者事務4的變更
//假如正在判斷的是事務2,因為已經經過了上面的校驗,
//所以我們知道當前版本記錄的事務m_low_limit_id(高水位)>id>=m_up_limit_id(低水位),且不是當前事務;
//所以就需要判斷事務只要不是活躍的,那麼就一定是已經提交的事務,那麼就可讀
  return (!std::binary_search(p, p + m_ids.size(), id));
}

在瞭解可見性規則之後我們知道,快照建立的時候會初始化幾個屬性,在查詢的時候會通過changes_visible方法來判斷是否可見,而調用這個方法的上層就是下麵這兩段邏輯,和我先前描述的類似,判斷是否可以讀當前版本的記錄,可以就返回,不行就繼續比較再上一個版本,直到最老的版本
//row0sel.cc#row_search_mvcc
if (srv_force_recovery < 5 &&
    !lock_clust_rec_cons_read_sees(rec, index, offsets,
                                   trx_get_read_view(trx))) {
  rec_t *old_vers;
  /* The following call returns 'offsets' associated with 'old_vers' */
  err = row_sel_build_prev_vers_for_mysql(
      trx->read_view, clust_index, prebuilt, rec, &offsets, &heap,
      &old_vers, need_vrow ? &vrow : nullptr, &mtr,
      prebuilt->get_lob_undo());

  if (err != DB_SUCCESS) {
    goto lock_wait_or_error;
  }

  if (old_vers == nullptr) {
    /* The row did not exist yet in
    the read view */

    goto next_rec;
  }

  rec = old_vers;
  prev_rec = rec;
  ut_d(prev_rec_debug = row_search_debug_copy_rec_order_prefix(
           pcur, index, prev_rec, &prev_rec_debug_n_fields,
           &prev_rec_debug_buf, &prev_rec_debug_buf_size));
}
//lock0lock.cc#lock_clust_rec_cons_read_sees
bool lock_clust_rec_cons_read_sees(
    const rec_t *rec,     /*!< in: user record which should be read or
                          passed over by a read cursor */
    dict_index_t *index,  /*!< in: clustered index */
    const ulint *offsets, /*!< in: rec_get_offsets(rec, index) */
    ReadView *view)       /*!< in: consistent read view */
{
  ut_ad(index->is_clustered());
  ut_ad(page_rec_is_user_rec(rec));
  ut_ad(rec_offs_validate(rec, index, offsets));

  /* Temp-tables are not shared across connections and multiple
  transactions from different connections cannot simultaneously
  operate on same temp-table and so read of temp-table is
  always consistent read. */
  if (srv_read_only_mode || index->table->is_temporary()) {
    ut_ad(view == nullptr || index->table->is_temporary());
    return (true);
  }

  /* NOTE that we call this function while holding the search
  system latch. */

  trx_id_t trx_id = row_get_rec_trx_id(rec, index, offsets);

  return (view->changes_visible(trx_id, index->table->name));
}

事務的trx_id

在我還沒開始看mysql源碼,只是跟著博客學習寫用例測試的時候,我發現,開啟事務進行了第一次查詢之後,確實有生成事務id,但後面我執行了一條更新語句之後,原來的事務id變了;就像下麵這個圖一樣,最開始只有查詢的時候是比較長的這個id,但執行了一條update語句後,事務id變成了一個短的。

這個時候我就產生了很多疑問?同一個事務里,事務id怎麼還能變呢?變的話changes_visible裡面的比較怎麼算?搜索了一下之後瞭解到,只讀事務是不會生成事務id的,是假的!於是我又疑惑,那這個假id怎麼參與changes_visible呢?也就是這個時候,我才下定決心去看源碼,也藉此理解了高低水位的設計,並認識到自己之前的理解是錯誤的。 先上結論 只讀事務不會分配真正的事務id,他的值是0; 只讀事務參與change_visable的時候,create_trx_id也確實是0,是通過m_up_limit_id(低水位)來判斷是否可見的,只有在變成讀寫事務時,create_trx_id才會起效並應用; 因為值是0,所以在通過下麵sql查詢的時候,那串id只是展示的時候特殊處理的
select * from information_schema.INNODB_TRX; 
//trx0trx.cc#trx_start_low
 //這裡可以看到只有讀寫事務才真正分配了id
 else {
trx->id = 0;

if (!trx_is_autocommit_non_locking(trx)) {
    /* If this is a read-only transaction that is writing
    to a temporary table then it needs a transaction id
    to write to the temporary table. */

    if (read_write) {
      trx_sys_mutex_enter();

      ut_ad(!srv_read_only_mode);

      trx->state.store(TRX_STATE_ACTIVE, std::memory_order_relaxed);

      trx->id = trx_sys_allocate_trx_id();

      trx_sys->rw_trx_ids.push_back(trx->id);

      trx_sys_mutex_exit();

      trx_sys_rw_trx_add(trx);

    } else {
      trx->state.store(TRX_STATE_ACTIVE, std::memory_order_relaxed);
    }
  } else {
    ut_ad(!read_write);
    trx->state.store(TRX_STATE_ACTIVE, std::memory_order_relaxed);
  }
}
//trx0trx.ic
//這裡是在展示的時候對只讀事務的id做了處理
/** Retreieves the transaction ID.
In a given point in time it is guaranteed that IDs of the running
transactions are unique. The values returned by this function for readonly
transactions may be reused, so a subsequent RO transaction may get the same ID
as a RO transaction that existed in the past. The values returned by this
function should be used for printing purposes only.
@param[in]      trx     transaction whose id to retrieve
@return transaction id */
static inline trx_id_t trx_get_id_for_print(const trx_t *trx) {
  /* Readonly and transactions whose intentions are unknown (whether
  they will eventually do a WRITE) don't have trx_t::id assigned (it is
  0 for those transactions). Transaction IDs in
  information_schema.innodb_trx.trx_id,
  performance_schema.data_locks.engine_transaction_id,
  performance_schema.data_lock_waits.requesting_engine_transaction_id,
  performance_schema.data_lock_waits.blocking_engine_transaction_id
  should match because those tables
  could be used in an SQL JOIN on those columns. Also trx_t::id is
  printed by SHOW ENGINE INNODB STATUS, and in logs, so we must have the
  same value printed everywhere consistently. */

  /* DATA_TRX_ID_LEN is the storage size in bytes. */
  static const trx_id_t max_trx_id = (1ULL << (DATA_TRX_ID_LEN * CHAR_BIT)) - 1;

  ut_ad(trx->id <= max_trx_id);

  /* on some 32bit architectures casting trx_t* (4 bytes) directly to
  trx_id_t (8 bytes unsigned) does sign extension and the resulting value
  has highest 32 bits set to 1, so the number is unnecessarily huge.
  Also there is no guarantee that we will obtain the same integer each time.
  Casting to uintptr_t first, and then extending to 64 bits keeps the highest
  bits clean. */

  return (trx->id != 0
              ? trx->id
              : trx_id_t{reinterpret_cast<uintptr_t>(trx)} | (max_trx_id + 1));
}

生成快照時機(不太確定)

可重覆讀:只生成一次,後面繼續使用
ReadView *trx_assign_read_view(trx_t *trx) /*!< in/out: active transaction */
{
  ut_ad(trx_can_be_handled_by_current_thread_or_is_hp_victim(trx));
  ut_ad(trx->state.load(std::memory_order_relaxed) == TRX_STATE_ACTIVE);

  if (srv_read_only_mode) {
    ut_ad(trx->read_view == nullptr);
    return (nullptr);

  } else if (!MVCC::is_view_active(trx->read_view)) {
    trx_sys->mvcc->view_open(trx->read_view, trx);
  }

  return (trx->read_view);
}
讀已提交:用完就關,所以每次再獲取就得新開,但是這裡的關有兩個地方調
ha_innodb.cc#store_lock 和ha_innodb.cc#external_lock
if (lock_type != TL_IGNORE && trx->n_mysql_tables_in_use == 0) {
  trx->isolation_level =
      innobase_trx_map_isolation_level(thd_get_trx_isolation(thd));

  if (trx->isolation_level <= TRX_ISO_READ_COMMITTED &&
      MVCC::is_view_active(trx->read_view)) {
    /* At low transaction isolation levels we let
    each consistent read set its own snapshot */

    mutex_enter(&trx_sys->mutex);

    trx_sys->mvcc->view_close(trx->read_view, true);

    mutex_exit(&trx_sys->mutex);
  }
}

在學習瞭解MVCC機制中遇到的問題:

  1. 為什麼更新操作必須使用當前讀?
更新操作後的如果不回滾那沒有事,如果要回滾,應該回滾到最新一次的提交,所以undo log里必須是最新的視圖
  1. 只讀事務突然更新的話,因為更新必須使用當前讀,那是否需要重新生成事務id?
不算做重新,是只有在觸發鎖操作時會分配真正的事務id,只讀事務分配的id其實就是0
  1. 只讀事務分配的事務id是什麼東西?如何參與運作?
沒有作為判斷條件,作為判斷條件的是up_limit記錄的是最小活躍id,小於他的才能讀,正規的事務id分配的在readview結構里其實是creator_trxid,用來判斷當前是否能被當前事務看見
  1. readview的範圍
有這個疑問還是因為最開始對change_visable掌握的不夠清晰。 之前想的場景是在事務A里,如果存在兩條不同記錄甚至不同表的查詢,而事務B在第事務A兩條查詢中間的時候對第二條查詢的記錄做了更改並提交,那應該查到的是新的還是舊的。 其實應該是舊的,因為就算是第二條查詢,最新版本的改動的事務id會大於等於事務A的高水位,因此只能查詢到更老的undolog里的記錄
  1. 知道了mvcc底層是undolog和readview後,怎麼理解“版本”這個概念
創建版本肯定就是欄位里那個隱藏欄位,刪除版本應該是回滾指針
  1. 在只讀視圖能查到其他事務已經刪除並且提交的記錄嗎?
經測試是可以的

怎麼解決的幻讀?

在只讀事務下,如上文所說的事務1讀不到事務2的更新是因為事務2的版本號要大於當前快照的高水位,那對於新增的記錄來說,其版本號也是同樣的道理,因此事務1讀不到比當前快照里的高水位高的,也就避免了幻讀這種情況。  

參考資料:

MySQL 8.0 MVCC 源碼解析 - 掘金 https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html MySQL事務ID的分配時機_mysql事務id什麼時候分配_哲學長的博客-CSDN博客 MYSQL innodb中的只讀事物以及事物id的分配方式_ITPUB博客 Mysql如何實現隔離級別 - 可重覆讀和讀提交 源碼分析_mysql 可重覆度源碼_擇維士的博客-CSDN博客  

本文來自博客園,作者:起司啊,轉載請註明原文鏈接:https://www.cnblogs.com/qisi/p/mvcc.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # Windows下安裝MySQL5.7.37 [TOC] [MySQL5.7.37下載地址](https://downloads.mysql.com/archives/community/?version=5.7.37&osva=Windows+%28x86%2C+64-bit%29) ##下載M ...
  • 目錄 一、入侵檢測系統 二、防火牆 三、防水牆 四、tcpdump抓包 五、實驗演示 1.SNAT 一、入侵檢測系統 特點:是不阻斷任何網路訪問,量化、定位來自內外網路的威脅情況, 主要以提供報警和事後監督為主,提供有針對性的指導措施和安全決策依據,類似於監控系統 二、防火牆 1.特點:隔離功能,工 ...
  • ## Core Dump 是什麼? Core Dump 是指進程異常退出時,操作系統將進程的記憶體狀態保存到文件中,這個文件就是 Core Dump 文件,中文一般翻譯為“核心轉儲”,哈,看起來還不如不翻譯。 我們可以認為 Core Dump 是“記憶體快照”,但實際上,除了記憶體信息之外,還有些關鍵的程 ...
  • CS5366內部集成了PD3.0及DSC decoder,應用Type-C轉HDMI2.0的顯示協議轉換晶元, 簡介: CS5366系列提供了USB Type-C(DisplayPort Alternate Mode)到HDMI轉換器的單晶元解決方案,帶有電源傳輸。CS5366系列支持一個USB T ...
  • 參考帖子:https://blog.csdn.net/freedompoi/article/details/122350866 目前想要實現STM32F4自帶的DMA雙緩衝區,嘗試過一版,結果不能預期,就使用了RxHalfCplt和RxCplt去實現DMA雙緩衝區的效果。 現在有時間了,又重新實現S ...
  • ## 介紹 windows系統自帶的遠程桌面連接相對市面上其他的遠程軟體而言有他的優點: 1. 免費 2. 連接穩定 3. 操作流暢,幾乎沒有遠程辦公的卡滯感 但是,唯一的缺點是連接的電腦之間必須使用一個區域網,也就是說比較適合校園、企業用戶。以校園為例進行講解。 ## 使用前提 1. 連接及被連接 ...
  • # 文件系統 > 文件是面向OS和麵向使用者而言的,對於人來說,音樂,圖片,文檔,游戲,軟體,郵件,等記錄信息的載體都被操作系統統稱為文件,而存儲在HDD(機械硬碟)和SSD(固態硬碟)里.因此文件是一種實體的抽象,而之所以文件需要文件名,是因為不同的文件需要進行相對應的區分,也就是文件名,而其中的 ...
  • # region Region是HBase數據管理的基本單位,region有一點像關係型數據的分區。 Region中存儲這用戶的真實數據,而為了管理這些數據,HBase使用了RegionSever來管理region。 ## region的分配 一個表中可以包含一個或多個Region。 每個Regio ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...