1,基本思路是:基於Redis實現分布式鎖+冪等性;
2,具體實現邏輯:
使用redis的decr (對key對應的數字做減1操作。如果key不存在,那么在操作之前,這個key對應的值會被置為0。如果key有一個錯誤類型的value或者是一個不能表示成數字的字符串,就返回錯誤。這個操作最大支持在64位有符號的整型數字。)可以實現原子性的遞增遞減操作控制優惠碼不超送,然后給每個用戶維護一個userid+優惠碼活動的key保證冪等性,只要redis存在這種key,那就代表已經領取了,具體的優惠碼分發可以異步執行。為了避免競爭(同一個用戶,多個設備同時領取)使用Redis.setNX() 實現分布式鎖(重復數據插入可用其來實現排他鎖);
3,相關核心代碼邏輯
ApplicationContext context = new AnnotationConfigApplicationContext(RedisCacheConfiguration.class); JedisPool jedisPool = (JedisPool)context.getBean("redisPoolFactory"); /** * 優惠劵發放主邏輯 * @param user 用戶數據 * @param eventsPromoCodeMark 本次活動唯一標記 */ public void redisAcquireLockLock(User user, String eventsPromoCodeMark){ //活動上線前設置優惠碼總量一百萬:jedisResources.set(promoCodeNumberKey, "1000000"); Jedis jedisResources = jedisPool.getResource(); String lock = user.getId() + eventsPromoCodeMark;//設置本次活動優惠碼鎖(保證冪等性):userId + 優惠碼活動的key String promoCodeNumberKey = eventsPromoCodeMark + "promoCodeNumber"; //設置優惠碼總量key String userKey = lock + "receiveCoupon";//設置用戶優惠碼領取key(自定義,只要保證一個用戶在一個活動的唯一性即可) //優惠碼領取庫存校驗 boolean promoCode = jedisResources.exists(promoCodeNumberKey) && StringUtils.isNotBlank(jedisResources.get(promoCodeNumberKey)) && 0L < Long.valueOf(jedisResources.get(promoCodeNumberKey)); if(!promoCode){ System.out.println("優惠碼已經領取完了"); jedisResources.close(); return; } //為了避免競爭(同一個用戶,多個設備同時領取).使用Redis.setNX() 實現分布式鎖(重復數據插入可用其來實現排他鎖) boolean flag = acquireLock(lock); if(flag){ //1,獲取鎖成功,進行用戶已領取標記,查詢用戶是否已經領取過 Boolean isExists = jedisResources.exists(userKey); if(!isExists){ //2,先使用redis的decr可以實現原子性的遞增遞減操作控制優惠碼不超送, Long success = jedisResources.decr(promoCodeNumberKey); // 先減庫存后發碼(減庫存后返回的現有庫存數量大於等於0說明本次搶碼成功,再進行發送優惠碼,否則庫存已經空了就不進行發送優惠碼) if(success >= 0L){ //3,再進行優惠碼分發(可異步執行通過MQ進行發放,如果減庫存成功,發放失敗,進行發放補償性操作) jedisResources.set(userKey, "received"); System.out.println("領取成功"); long oldValue = Long.valueOf(jedisResources.get(lock));//獲取鎖的舊值 if (oldValue > System.currentTimeMillis()) {//檢查處理時間是否小於超時時間,釋放鎖 jedisResources.delete(lock); } } } else { System.out.println("已經領取過了"); } } else { System.out.println("請重試"); } jedisResources.close(); } /** * 獲取鎖方法 * @param lock 鎖的k * @return boolean 本次獲鎖是否成功 */ public boolean acquireLock(String lock) { boolean success;//設置獲鎖成功標記 Jedis jedis = jedisPool.getResource(); int expired = 60 * 1000;//強制過期時間:60 秒 long value = System.currentTimeMillis() + expired; long acquired = jedis.setnx(lock, String.valueOf(value)); //通過SETNX試圖獲取一個lock if (1 == acquired) {//setnx 返回值為1則表示設置成功,則成功獲取一個鎖 success = true; } else {//setnx 失敗,說明鎖仍然被其他對象保持,檢查其是否已經超時 long oldValue = Long.valueOf(jedis.get(lock));//獲取鎖的舊值 if (oldValue < System.currentTimeMillis()) {//檢查是否超時 String getOldValue = jedis.getSet(lock, String.valueOf(value));//超時,進行鎖覆蓋,並獲取覆蓋值之后的獲取到的舊值 // 進行最終獲取鎖是否成功原子性校驗(覆蓋舊值之前獲取到的舊值 和 覆蓋值之后的獲取到的舊值 相同 則本次獲鎖成功,否則 失敗已經被別的對象獲取到) if (Long.valueOf(getOldValue) == oldValue) { success = true; } else {// 已被其他進程捷足先登了 success = false; } } else {//未超時,則直接返回獲取鎖失敗 success = false; } } jedisPool.close(); return success; }
獲鎖具體的使用步驟如下:
1. setnx(lockkey, 當前時間+過期超時時間) ,如果返回1,則獲取鎖成功;如果返回0則沒有獲取到鎖,轉向2。
2. get(lockkey)獲取值oldExpireTime ,並將這個value值與當前的系統時間進行比較,如果小於當前系統時間,則認為這個鎖已經超時,可以允許別的請求重新獲取,轉向3。
3. 計算newExpireTime=當前時間+過期超時時間,然后getset(lockkey, newExpireTime) 會返回當前lockkey的值currentExpireTime。
4. 判斷currentExpireTime與oldExpireTime 是否相等,如果相等,說明當前getset設置成功,獲取到了鎖。如果不相等,說明這個鎖又被別的請求獲取走了,那么當前請求可以直接返回失敗,或者繼續重試。
5. 在獲取到鎖之后,當前線程可以開始自己的業務處理,當處理完畢后,比較自己的處理時間和對於鎖設置的超時時間,如果小於鎖設置的超時時間,則直接執行delete釋放鎖;如果大於鎖設置的超時時間,則不需要再鎖進行處理。
