1. 程式人生 > 其它 >on duplicate key update批量更新_關於批量插入時的資料重複問題

on duplicate key update批量更新_關於批量插入時的資料重複問題

技術標籤:on duplicate key update批量更新oracle批量insert多條springboot 主鍵重複導致資料重複usb插入自動啟動activity不能在物件中插入重複鍵使用spring時插入資料插入兩條

9621fafd4234e9026d2394921fe331c3.gif

在上一篇文章中我們談到了資料庫批量插入,如果你已經親自實踐過了的話你會發現有個問題,就是我們在第一次執行完測試之後,再次執行的話會報錯,除非你刪掉資料庫中的資料再執行。

那是因為 activity_stats 表中 activity_id 是表的主鍵,第二次執行的話就會因為主鍵重複而報錯了,當然你也可以選擇去掉這個主鍵的限制來進行測試。

但真正在專案中可就不能這麼做了,專案中我們可能就是想要 activity_id 欄位作為主鍵,不允許插入重複資料。

雖然說一般情況下不會有重複的,但畢竟是一般情況下,對於特殊情況我們也是需要考慮的。比如說 excel 中就是因為運營人員粗心弄了多條相同的資料,或者再次將同樣的 excel 檔案拿來上傳,這時候就會有問題了。

所以對於這種情況我們需要做到相容,儘量給使用者好的體驗,不然說因為幾條重複的資料就讓使用者去修改再次上傳。對於這種普通的資料,可能就需要系統自動去重了,當然如果是涉及到金錢方面的資料那還是需要人工再次確認過的。

下面我們就討論下 key 重複的幾種不同處理方式:
1.忽略重複 key 的資料,由於主鍵或者唯一鍵重複的記錄,資料庫自動忽略,在 insert 後面加上 ignore 關鍵字。

1insertignoreintoactivity_stats(activity_id,times_viewed,works_count,user_count)
2values(1,100,50,10);

上面的 SQL 執行時,如果主鍵重複或者唯一鍵重複,會忽略掉新插入的資料,受影響行數為0,表資料不變,這是比較簡單暴力的方式,但對於大部分情況下都是適用的,具體看業務情況而定。

2.使用最新資料,
對於 key 重複的資料,我們希望插入最新的資料到資料庫中。要做到這個有兩種方式: 一種是對重複資料進行更新,另一種是先刪掉舊資料,然後再插入新資料。

先看第一種: ON DUPLICATE KEY UPDATE

1insertintoactivity_stats(activity_id,times_viewed,works_count,user_count)
2values(1,100,50,10)
3ONDUPLICATEKEY
4UPDATEtimes_viewed=values(times_viewed),works_count=values(works_count),user_count=values(user_count);

這種方式就是對於有重複的資料採取更新操作,要更新的欄位跟在 UPDATE 關鍵字後面。

這裡要注意一個受影響的行數的問題:

  • 沒有重複 key 資料,直接插入新資料,受影響的行數為 1。

  • 有重複 key 資料並且要更新的新資料和已經存在的舊資料不一樣,受影響的行數為 2(???),這裡可能有點不好理解為什麼是 2,這裡我們先暫時記下是 2,待會我會詳細說明。

  • 有重複 key 資料並且要更新的新資料和已經存在的舊資料一致,受影響的行數為 1(???),這個可能也不是很好理解,同樣暫時先記下,馬上為你娓娓道來。

好了,現在看上面有兩個地方打上了問號,這兩個地方都是和我們正常所想的有點不一樣。先看第一個打了問號的地方,也就是上面的第二點,按我們所想,key 重複,然後進行資料更新,這裡只是進行了更新操作,受影響的行數應該是 1 才對,剛開始我對這個也是不太理解,後來通過查資料發現在 MySQL 的文件中關於 ON DUPLICATE KEY UPDATE 的使用中有下面這樣一句話:
文件連結:
https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html

1WithONDUPLICATEKEYUPDATE,theaffected-rowsvalueperrowis1iftherowisinsertedasanewrow,2ifanexistingrowisupdated,and0ifanexistingrowissettoitscurrentvalues.IfyouspecifytheCLIENT_FOUND_ROWSflagtothemysql_real_connect()CAPIfunctionwhenconnectingtomysqld,theaffected-rowsvalueis1(not0)ifanexistingrowissettoitscurrentvalues.

上面這句話前半句就能為我解答為什麼我們在第二點中提到的疑惑,前半句的大概意思就是:
對於 ON DUPLICATE KEY UPDATE 的使用,如果沒有 key 重複的新資料插入,每行受影響的行數是 1,如果有 key 重複的新資料更新,返回受影響的行數是 2,如果有 key 重複的資料並且存在的舊資料和要更新的資料一致,受影響的行數是 0。

看到這裡我們應該就能明白為什麼受影響的行數是 2 了,我們可以理解為是 MySQL 為了在 ON DUPLICATE KEY UPDATE 的使用中區分插入和更新,所以對於插入返回 1,更新返回 2。

但是不是說 "如果是有 key 重複的資料並且存在的舊資料和要更新的資料一致,受影響的行數是 0" 嗎?可是我們在上面第三點中 MySQL 返回的受影響的行數是 1,而這也正是我們第三點所疑惑的地方,我們再去看上面那句話的後半句,大概意思是:
在連線 mysqld 的時候如果你為 mysql_real_connect() API 函式指定了 CLIENT_FOUND_ROWS 標誌,那麼對於已經存在的舊資料和要更新的資料一致時,受影響的數值為 1 而不是 0。

對於第二個疑問,我們也大概明白為什麼受影響的行數是 1 了,是因為我們使用的 MySQL 指定了 CLIENT_FOUND_ROWS 標誌,而 mysql_real_connect 函式指定 CLIENT_FOUND_ROWS 標誌的意思就是:

1CLIENT_FOUND_ROWS:Returnthenumberoffound(matched)rows,notthenumberofchangedrows.

這段文字在上面文件連結中可以點選 mysql_real_connect 跳轉到對 mysql_real_connect 函式的描述頁面,裡面有 CLIENT_FOUND_ROWS 相應介紹。感興趣的可以去研究研究。


再回到使用最新資料方式上,上面已經描述了對 key 重複的資料進行更新的方式,接下來我們看第二種:
對於 key 重複,先刪掉舊資料,然後再插入新資料: REPLACE INTO

1REPLACEINTOactivity_stats(activity_id,times_viewed,works_count,user_count)
2values(1,100,50,10);

這種方式就是如果沒有重複 key,直接插入,效果和 insert into 一樣,但如果有重複 key,它會先刪除舊資料,再插入新資料。同樣這種方式受影響的行數也有以下幾種情況:

  • 沒有重複 key 資料,直接插入新資料,受影響的行數為 1。

  • 有重複 key 資料並且要更新的新資料和已經存在的舊資料不一樣,受影響的行數為 2。這個很好理解,先刪除舊資料,然後再插入新資料,所以受影響的行數為 2。

  • 有重複 key 資料並且要更新的新資料和已經存在的舊資料一致,受影響的行數為 1。這裡應該是資料沒有發生變化,至於返回 1,我的理解是和上面一樣的,返回匹配到的行數,並不是發生改變的行數(暫時沒找到有關文件描述)。

關於重複 key 資料的處理方式主要有上面提到的三種,忽略重複資料的 INSERT IGNORE,使用更新的方式插入最新資料的 ON DUPLICATE KEY UPDATE,還有先刪除再插入最新資料的 REPLACE INTO。

在我們使用這三種方式來避免插入重複資料的過程中,我們還需要注意下面幾個問題:
首先,使用這三種方式的前提都是表中需要存在主鍵衝突或唯一鍵衝突,不然這三種都跟直接 INSERT INTO 沒什麼區別。

第二,要注意存在多個鍵衝突(插入的資料既可能存在主鍵衝突,也可能存在其他唯一鍵衝突或者存在多個唯一鍵衝突)的情況,在這種情況下使用可能會出現其他結果(可以自己測試看看),所以這種情況下不太建議使用。

第三,這點比較重要,上面我的 activity_stats 表是有一個 activity_id 欄位作為主鍵,這是存在主鍵衝突的情況。但如果我把 activity_id 欄位改成唯一鍵並且新增一個欄位 id 作為主鍵自增長,插入語句不變,這裡因為 id 是自增長的,不存在衝突,所以這裡是滿足單一鍵衝突的情況,但是這裡有個坑要注意,這種情況下使用上面三種方式都會產生主鍵 id 自增長不連續的問題。

目前的 SQL 語句是這樣的:

1insertignoreintoactivity_stat(id,activity_id,times_viewed,works_count,user_count)
2values(null,1,100,50,10);
3或者

對於 INSERT IGNORE 和 ON DUPLICATE KEY UPDATE,在存在 unique key 重複時,前者雖然重複的記錄被忽略了沒有執行插入操作,但是 id 還是會 +1。而後者是在 key 重複時執行更新操作,但 id 同樣也會 +1,也就是說在存在 unique key 重複的情況下這兩種方式不管有沒有插入資料,自增長 id 都會增加。

這樣的話在主從庫的場景下就會出現主從的 auto_increment 不一致,因為 slave 並不會同步 master 的 auto_increment,一旦當前 master 掛了,任意一個 slave 被選舉為 master,再次執行 insert 語句就可能導致主鍵衝突。

至於這裡為什麼會出現沒有插入記錄 auto_increment 卻依然 +1 的情況,和 innodb_autoinc_lock_mode 的值有關。可以通過下面的 SQL 語句檢視:

1select@@innodb_autoinc_lock_mode;

它的值可能是 0,1,2 其中一種,預設是 1,具體各個值的特徵可以參看官方文件中的描述,下面是文件地址:
https://dev.mysql.com/doc/refman/5.7/en/innodb-auto-increment-handling.html#innodb-auto-increment-lock-modes

而對於 REPLACE INTO,那是因為在存在 unique key 重複時,會先刪除再插入,執行了插入操作,id 肯定會增加。問題就在於重新插入了一條資料 id 發生了變化。如果別的表關聯了這個表的主鍵 id 的話就會出現資料查詢不到的情況。

總之就是對於這三種避免插入重複資料的方式,如果是由於主鍵衝突的情況,基本上沒什麼問題,但如果是唯一鍵衝突的情況,都會導致 master 和 slave 的 auto_increment 不一致的問題,一旦主從的 auto_increment 不一致(這裡是 master 的 auto_increment 要大於 slave),只要 master 出現問題,任意一個 slave 升級成 master,由於當前新的 master 的 auto_increment 是小於資料中的 id 的值,所以再進行插入時就可能發生主鍵衝突。

推薦閱讀JdbcTemplate 實現批量插入

勵志成為一名菜鳥碼農,共勉!

290acbaca1ff2a711617d04073e9e89a.png