一、業務需求:
當操作積分用戶表時,如果accountId在表中沒有數據,那么我們新增一條數據,設置用戶積分。如果accountId在表中有數據,我們需要更新用戶積分。
這個操作簡單來說就是:
在單線程下 我們先查詢后處理當然沒有問題,但是在並發下問題就顯而易見了,系統里可能同時插入兩條一樣的accountId數據。
二、問題解決:
解決方式一: ON DUPLICATE KEY UPDATE
數據庫中account_id設置唯一索引,當發現account__id已經存在時,會執行update操作,不存在時會執行insert操作。
一行sql語句就能完成兩種操作,保證了原子性。
sql語句如下:

添加單元測試,查看耗時以及查驗數據庫在並發下數據是否正確。
代碼隱去業務代碼,如下:

查看打印的日志,共耗時:22690ms
數據庫數據能夠保持正確性
解決方式二: 使用分布式鎖
這個耗時比第一種方式差很多,所以沒有測試完就放棄了。
因為高並發的情況下 鎖的搶占很激烈,這里很多時間都耗費在鎖的搶占上,沒有搶占到鎖的線程需要重試而不能失敗,類似於CAS操作,所以這種方式不適合當前業務。
解決方式三: INSERT INTO SELECT
此種方式也是最優的,耗時:20010ms
sql語句如下:

查詢accountId不存在時結果:

查詢accountId存在時結果:

這里需要注意的是,此sql語句在Mapper.xml中是insert語句:

三、原理分析
1、ON DUPLICATE KEY UPDATE
mysql "ON DUPLICATE KEY UPDATE" 語法:
如果在INSERT語句末尾指定了ON DUPLICATE KEY UPDATE,並且插入行后會導致在一個UNIQUE索引或PRIMARY KEY中出現重復值,則在出現重復值的行執行UPDATE;如果不會導致唯一值列重復的問題,則插入新行。
2、 INSERT INTO SELECT
INSERT INTO SELECT 語句從一個表復制數據,然后把數據插入到一個已存在的表中。目標表中任何已存在的行都不會受影響。
其中使用到了dual虛擬表, 根據mysql的官方定義:
DUAL is purely for the convenience of people who require that all SELECT statements should have FROM and possibly other clauses. MySQL may ignore the clauses. MySQL does not require FROM DUAL if no tables are referenced.
官方的解釋說:純粹是為了滿足select … from…這一習慣問題,mysql會忽略對該表的引用。所以上面的語句from dual可以去掉。
簡言之,from dual完全是一個可有可無的東西。只是為了方便使用select 語句中喜歡帶上from的開發者。
例如我們使用select 1 查詢等價於select 1 from dual
四、總結
到了這里就分析完了,如果大家有更好的解決方案也可以拿出來學習下,文中如有問題懇請大家指正一下。
第一種方式會有一個id不是連續自增的問題,具體可以參考文章:
https://segmentfault.com/a/1190000017268633