問題背景
應公司產品要求,爲了方便觀察,編號需要由統一前綴+日期+連續增長序列構成。
方案
採用redis作爲中間件從而產生唯一增長序列後綴。
僞代碼大概如下:(線程不安全)
① if(exists(key)) {
② incr(key);
} else {
// 設置初始值爲1000,並設置過期時間爲凌晨0點加隨機整數
// 增加隨機數是爲了避免大量key同時過期,導致redis短暫不可用問題
③ SETEX key 過期時間 1000
}
發現問題
- 項目爲內部系統,平時使用並不集中,因此一直沒有出現過問題
- 由於每天凌晨需要定時獲取昨日的司機運費數據,統一彙總並存儲,項目在測試環境觀察數據時發現產生了多個一模一樣的
1000
這個id號
分析問題
- 線程到達①處代碼時,向redis詢問有無key,得到的結果爲false,同時將此結果存儲在
本地內存
中 - 當執行③處代碼時,線程被切換了,此線程休眠
- 第二個線程執行了③處代碼,向redis設置了初始值,並獲取到了編號1000
- 第一個線程被調度執行的時候又重複執行了③處代碼,再次向redis設置了初始值,並獲取到了編號1000,這時就出現了id重複問題了
解決方案
- 鎖—簡單粗暴,由於是SpringCloud項目,所以只能使用分佈式鎖。然而代價就是原本此處只有在每天初次使用時才需要加鎖,其它時間使用redis的
incr
命令已經可以保證原子性了,如果採用分佈式鎖的方案就會做n-1次無用功,代價太大了 - 使用Lua腳本,在redis中執行具有天然的原子性特性,推薦
在SpringBoot中實現
- Lua腳本
if redis.call('EXISTS', KEYS[1]) == 1
then
return redis.call('INCR', KEYS[1])
else
redis.call('SETEX', KEYS[1], ARGV[1], ARGV[2])
return tonumber(ARGV[2])
end
- 代碼調用
stringRedisTemplate.execute(new DefaultRedisScript<Long>(SCRIPT, Long.class), keys, args);
注意事項
returnType 參數不能使用Integer.class ,需要使用Long.class。