猿們好,我是honery,今天來給你們嘮一嘮如何避免數據庫報惟一性約束的錯誤。redis
##1、問題的引出 首先拋出一個問題,如何保證數據庫表中的某列的值都不同呢?相信你們很容易想到給該列加上惟一性約束
,這樣就能保證業務邏輯的正確性了。實際的使用中,尤爲高併發場景下,很容易出現插入同一條記錄的狀況,該狀況下數據庫會報違反惟一性約束的錯誤。總不能讓數據庫一直拋這個錯誤吧。因而咱們想到能夠在業務代碼中加上該列值是否爲空的判斷,判斷爲空時再行插入,因而問題就解決了。數據庫
問題真的解決了嗎?說是,你就too young too simple了。有沒考慮過高併發場景
呢?若是多個線程同時在某次插入前去判空,顯然判斷的結果都是空,那麼第一次插入成功後,後續的插入動做都會報違反數據庫惟一性約束的錯誤。總不能讓日誌一直報錯吧,該如何解決呢?性能優化
##2、問題的解決方案 這個問題實際上是個典型的問題,能夠有不少種解決方案,小編這裏就簡單提供三種解決策略。方案很簡單,猿們跟上思路~~ ###2.1 經過鎖機制,將查詢和插入原子化 相信不少小夥伴很容易就能想到這個方案,經過鎖機制(如內置鎖,synchronized
)將記錄是否存在的查詢動做和插入新記錄的動做放在一個同步鎖中,實現的關鍵代碼以下:併發
@Transactional public synchronized void insertWhenIdIsEmpty(Qingmj qingmj) { log.info("進入時間:"+System.currentTimeMillis()); log.info(qingmj.toString()); Qingmj qingmj_old = qingmjMapper.getQingmjById(qingmj.getId()); try { //等待10s,製造併發場景 Thread.sleep(15000); } catch (InterruptedException e) { log.warn("睡眠等待過程異常",e); } if(qingmj_old==null) { log.info("數據準備插入中... ..."); try { qingmjMapper.insert(qingmj); log.info("數據插入成功!"); }catch(Exception e) { log.info("數據插入失敗",e); } }else { log.info("約束鍵已存在,再也不插入"); } log.info("結束時間:"+System.currentTimeMillis()); }
<font color=#0099ff size=4 face="黑體">[執行結果]</font>: app
<font color=#0099ff size=4 face="黑體">[結果分析]</font>:高併發
<font color=#ff0000 size=3 face="黑體"> 從執行結果可知,經過鎖機制將查詢和插入原子化後,完全的避免了插入重複數據的問題。可是帶來了一個新問題,同步鎖將兩個業務動做鎖在一塊兒,強制執行過程串行化,會致使系統運行的性能較差,該如何優化呢?</font>性能
###2.2 經過雙重檢查鎖機制,優化串行化帶來的性能問題 所謂的雙重檢查鎖機制,從名字不難看出其邏輯。它主要有兩個技術點:1、將方法鎖細化成代碼塊鎖
,儘可能減小鎖住的執行邏輯;2、執行兩次判空邏輯,即在同步代碼塊中再加入一次判空檢查。具體的代碼實現以下:優化
//@Transactional public void insertWhenIdIsEmpty(Qingmj qingmj) { log.info("進入時間:"+System.currentTimeMillis()); log.info(qingmj.toString()); Qingmj qingmj_old = qingmjMapper.getQingmjById(qingmj.getId()); try { //等待10s,製造併發場景 Thread.sleep(15000); } catch (InterruptedException e) { log.warn("睡眠等待過程異常",e); } if(qingmj_old==null) { synchronized (this) { Qingmj qingmj_old_2 = qingmjMapper.getQingmjById(qingmj.getId()); if(qingmj_old_2==null) { log.info("數據準備插入中... ..."); try { qingmjMapper.insert(qingmj); log.info("數據插入成功!"); }catch(Exception e) { log.info("數據插入失敗",e); } }else { log.info("約束鍵發現變爲存在狀態,再也不插入"); } } }else { log.info("約束鍵已存在,再也不插入"); } log.info("結束時間:"+System.currentTimeMillis()); }
<font color=#0099ff size=4 face="黑體">[執行結果]</font>: this
<font color=#0099ff size=4 face="黑體">[結果分析]</font>:線程
<font color=#ff0000 size=3 face="黑體"> 引入雙重檢查鎖機制,有效優化了同步方法鎖性能較差的問題,是一種較爲推薦的方法。這也是單例模式中懶漢模式的一種經典實現。代碼中的事務註解不能在此處方法上加上,不然會拔苗助長,這也是鎖與事務做用範圍的一個角逐。好了,回到正題,解決違反數據庫惟一性約束問題,能不能不加鎖呢?答案是確定的。</font>
###2.3 經過巧用Redis的setnx特性,避免重複數據的插入 咱們要解決的問題是業務流程正常,但數據庫會持續的報違反惟一性約束的問題。如何保留數據庫的惟一性約束,維持業務流程正常,但同時不讓數據庫報上述異常呢?順着這個思路咱們能夠想到可否在數據進入數據庫以前就識別出重複數據的衝突呢?這樣即可以解決上述問題了。有不少第三方的中間件能夠幫咱們作到這點,其中Redis就是一個例子。
你們都用過Redis,知道Redis的setnx
方法有個特色,當第一次插入某個key
及其value
值時會返回「1」;當後續繼續插入該key
的其餘value
值時會返回「0」,並插入失敗。因而乎,咱們在插入數據庫以前,先將數據存入Redis當中,若Redis反饋爲第一次插入則落地數據庫中;若Redis反饋非第一次插入則不落地數據庫。如此便巧妙解決了數據庫報惟一性約束的問題了,並且!沒有用到鎖!具體實現以下:
@Transactional public void insertWhenIdIsEmpty(Qingmj qingmj) { log.info("進入時間:"+System.currentTimeMillis()); log.info(Thread.currentThread().getName() + " parameters:" + qingmj.toString()); Qingmj qingmj_old = qingmjMapper.getQingmjById(qingmj.getId()); try { //等待10s,製造併發場景 Thread.sleep(15000); } catch (InterruptedException e) { log.warn("睡眠等待過程異常",e); } if(qingmj_old==null){ if(redisUtil_test.setnx("constraint_id_"+qingmj.getId(), qingmj.getId()) == 1){//若是數據存在則返回0,不存在返回1 log.info("數據準備插入中... ..."); try { qingmjMapper.insert(qingmj); log.info("數據插入成功!"); redisUtil_test.expire("constraint_id_"+qingmj.getId(), 1000);//設失效時間3秒 }catch(Exception e) { redisUtil_test.del("constraint_id_"+qingmj.getId());//插入出異常則刪除 log.info("數據插入失敗",e); } }else{ log.info("併發情形,約束鍵已存在,再也不插入"); } }else{ log.info("約束鍵已存在,再也不插入"); } log.info("結束時間:"+System.currentTimeMillis()); }
<font color=#0099ff size=4 face="黑體">[執行結果]</font>:
<font color=#0099ff size=4 face="黑體">[結果分析]</font>:
<font color=#ff0000 size=3 face="黑體"> 經過巧用Redis的setnx特性有效解決了數據庫持續報違反惟一性約束的錯誤。同時沒有使用同步鎖,大大提高了系統的性能。這種方法甚至在表字段未加惟一性約束的狀況下,也能保證業務邏輯的正確性,很是巧妙且靈活。固然系統也必需要支持Redis才能採用這種方案啦。</font>
##3、總結
數據庫違反惟一性約束問題,只是一個典型的小問題,解決方案特別多,你們均可以去借鑑。可是小編文中陳述的三種方案匯聚瞭解決這類問題的兩個思路。其一是加鎖,鎖性能優化;其二是不加鎖,在落地數據庫以前提早引起數據衝突。方案雖然說簡單,可是值得回味的。