解決方案:如何防止數據重複插入?

摘要: 原創出處 https://www.bysocket.com 「公衆號:泥瓦匠BYSocket 」歡迎關注和轉載,保留摘要,謝謝!
目錄java

  1. 爲啥要解決數據重複插入?
  2. 解決方案實戰
  3. 可落地小總結

1、爲啥要解決數據重複插入?

問題起源,微信小程序抽風 wx.request() 重複請求服務器提交數據。後端服務也很簡單,僞代碼以下:spring

class SignLogService {
    public void saveSignLog(SignLogDO log) {
        // 簡單插入作記錄
        SignLogDAO.insert(log);
    }
}

發現數據庫會存在重複數據行,提交時間如出一轍。但業務需求是不能有多餘的 log 出現,這明顯是個問題。數據庫

問題是,重複請求致使的數據重複插入。這問題形成的後果很明顯:小程序

  • 數據冗餘,可能不僅僅多一條
  • 有些業務需求不能有多餘數據,形成服務問題

問題如圖所示:後端

image.png

解決方式:如何將 同請求 A,不執行插入,而是讀取前一個請求插入的數據並返回。解決後流程應該以下:
image.png微信小程序

2、解決方案實戰

1.單庫單表解決方案

  • 惟一索引 + 惟一字段
  • 冪等

上面說的那種業務場景:sign_log 表會有 user_id、sign_id、sign_time 等。那麼每次簽到,每一個人天天只有一條簽到記錄。springboot

數據庫層採起惟一索引的形式,保證數據記錄惟一性。即 UNIQUE 約束,UNIQUE 約束惟一標識數據庫表中的每條記錄。另外,user_id,sign_id,sign_time 三個組合適惟一字段。創表的僞代碼以下:服務器

CREATE TABLE sign_log
(
id int NOT NULL,
user_id int NOT NULL,
sign_id int,
sign_time int,
CONSTRAINT unique_sign_log UNIQUE (user_id,sign_id,sign_time)
)

重點是 CONSTRAINT unique_sign_log UNIQUE (user_id,sign_id,sign_time)。有個小問題,數據量大的時候,每條記錄都會有對應的惟一索引,比較耗資源。那麼這樣就好了嗎?微信

答案是不行,服務不夠健壯。第一個請求插入成功,第二個請求直接報錯,Java 服務會拋出 DuplicateKeyException併發

簡單的冪等寫法操做便可,僞代碼以下:

class SignLogService {
    public SingLogDO saveSignLog(SignLogDO log) {
        // 冪等處理
        SignLogDO insertLog = null;
        try {
            insertLog = signLogDAO.insert(log);
        } catch (DuplicateKeyException e) {
            insertLog = selectByUniqueKeys(userId,signId,signTime);
        }
        
        return insertLog;
    }
}

的確,流量不是很大,也不算很高併發。重複寫問題,這樣處理便可。那大流量、高併發場景咋搞

2.分庫分表解決方案

流量大了後,單庫單表會演變成分庫分表。那麼基於單表的惟一索引形式,在碰到分表就沒法保證呢,插入的地方多是兩個分表 A1 和 A2。

解決思路:將數據的惟一性條件放到其餘存儲,並進行鎖控制

仍是上面的例子,天天,每次簽到,每一個人只有一條簽到記錄。那麼使用分佈式鎖 Redis 的解決方案。大體僞代碼以下:

a.加鎖

// 加鎖
jedis.set(lockKey, requestId, "NX", "PX", expireTime);
  • lockKey 最簡單的是 user_id + sign_id + sign_time
  • expireTime 設置爲一天

b.解鎖

// 解鎖
jedis.eval(script, lockKey,requestId);

c.冪等代碼增強

class SignLogService {
    public SingLogDO saveSignLog(SignLogDO log) {
        
        // 冪等校驗
        SignLogDO existLog = selectByUniqueKeys(userId,signId,signTime);
        if(Objects.nonNull(existLog)) {
            return existLog;
        }
    
        // 加鎖
        jedis.set
        
        SignLogDO insertLog = signLogDAO.insert(log);
        
        // 解鎖
        jedis.eval

        return insertLog;
    }
}

這個方案仍是不是很成熟,你們參考下便可。

3、可落地小總結

解決方案實戰中,瞭解具體術。概括以下:

  • 冪等:保證屢次贊成請求後結果一致
  • 併發控制:單表惟一索引、分佈式多表分佈式鎖
  • 降級兜底方案:分佈式鎖鎖失效 - 考慮樂觀鎖兜底

參考資料

如下專題教程也許您會有興趣

image.png

相關文章
相關標籤/搜索