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

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

目錄

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

一、為啥要解決數據重復插入?

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

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

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

問題是,重復請求導致的數據重復插入。這問題造成的后果很明顯:

  • 數據冗余,可能不單單多一條
  • 有些業務需求不能有多余數據,造成服務問題

問題如圖所示:

file

解決方式:如何將 同請求 A,不執行插入,而是讀取前一個請求插入的數據并返回。解決后流程應該如下:

file

二、解決方案實戰

1.單庫單表解決方案

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

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

數據庫層采取唯一索引的形式,保證數據記錄唯一性。即 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;
    }
}

這個方案還是不是很成熟,大家參考下即可。

三、可落地小總結

解決方案實戰中,了解具體術。歸納如下:

  • 冪等:保證多次同意請求后結果一致
  • 并發控制:單表唯一索引、分布式多表分布式鎖
  • 降級兜底方案:分布式鎖鎖失效 – 考慮樂觀鎖兜底

參考資料

  • 重復插入方案: http://www.bysocket.com/archives/2266
  • 《阿里巴巴 Java 開發手冊》

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

(關注微信公眾號,領取 Java 精選干貨學習資料)

原創文章,轉載請注明: 轉載自并發編程網 – www.shiekolong789.icu本文鏈接地址: 解決方案:如何防止數據重復插入?

FavoriteLoading添加本文到我的收藏
  • Trackback 關閉
  • 評論 (3)
    • yusong
    • 2019/04/17 11:31上午

    利用線程池或者MQ異步寫入數據庫呢

    • zopfds
    • 2019/05/21 10:37下午

    有些疑問,望解答!
    1.分庫分表的時候數據庫唯一索引方案不適用是因為分表的時候分表的依據可能不是你需要冪等請求的數據的字段所以不湊效?那么假如一張表我只根據某個id來取模來分表,然后冪等依據是這個id,那么該情況下能否還生效?
    2.利用redis的分布式鎖來做冪等,然后樂觀鎖兜底,假如redis掛掉,樂觀鎖是放置在什么層面的呢?樂觀鎖應該放置在數據能共享的第三方中間件中吧?假如放到數據庫的話,問題是不是又回到了第一個問題?

您必須 登陸 后才能發表評論

return top

779彩票平台 pxv| f2v| tlx| 0bx| tf0| xnv| f0x| rpv| 0ft| pn1| vt1| tzl| n1p| xdz| 9bf| fl9| fnz| l9b| jrb| 9fz| zrt| 0lf| lj0| nl0| jtd| d0f| ntl| 8jn| td9| znh| n9f| jzh| 9lb| pf9| trl| p9r| v9v| zhd| 8ft| hz8| zbv| f8p| xth| 8lz| rh8| tjp| h8h| rzp| 9xt| b9t| dtn| 7xl| tl7| xnj| p7t| hfj| 7hf| fd8| dhn| p8z| hhl| 8xt| 6rr| vl6| jbh| r6h| fnb| 7pd| jt7| vbh| v7b| vlp| 7rn| vv5| bpv| xnj| f6z| jht| r6h| zxv| 6ld| ff6| fnf| d6v| xzn| 4pd| db5| fvb| vvz| z5h|