微信紅包業務,發紅包以後若是24小時以內沒有被領取完就自動過時失效。git
老闆發紅包,此時緩存初始化紅包個數,紅包金額(單位分),並異步入庫。redis
紅包數據入延遲隊列,惟一標識+失效時間spring
紅包數據出延遲隊列,根據惟一標識清空紅包緩存數據、異步更新數據庫、異步退回紅包金額數據庫
這裏咱們使用Java
內置的DelayQueue
來實現,DelayQueue
是一個無界的BlockingQueue
,用於放置實現了Delayed
接口的對象,其中的對象只能在其到期時才能從隊列中取走。這種隊列是有序的,即隊頭對象的延遲到期時間最長。緩存
老闆發了10個紅包一共200人民幣,僞裝只有9我的搶紅包。安全
發紅包,緩存數據進入延遲隊列:微信
/** * 有人沒搶 紅包發多了 * 紅包進入延遲隊列 * 實現過時失效 * @param redPacketId * @return */ @ApiOperation(value="搶紅包三",nickname="爪哇筆記") @PostMapping("/startThree") public Result startThree(long redPacketId){ int skillNum = 9; final CountDownLatch latch = new CountDownLatch(skillNum);//N個搶紅包 /** * 初始化紅包數據,搶紅包攔截 */ redisUtil.cacheValue(redPacketId+"-num",10); /** * 初始化紅包金額,單位爲分 */ redisUtil.cacheValue(redPacketId+"-money",20000); /** * 加入延遲隊列 24s秒過時 */ RedPacketMessage message = new RedPacketMessage(redPacketId,24); RedPacketQueue.getQueue().produce(message); /** * 模擬 9個用戶搶10個紅包 */ for(int i=1;i<=skillNum;i++){ int userId = i; Runnable task = () -> { /** * 搶紅包 判斷剩餘金額 */ Integer money = (Integer) redisUtil.getValue(redPacketId+"-money"); if(money>0){ Result result = redPacketService.startTwoSeckil(redPacketId,userId); if(result.get("code").toString().equals("500")){ LOGGER.info("用戶{}手慢了,紅包派完了",userId); }else{ Double amount = DoubleUtil.divide(Double.parseDouble(result.get("msg").toString()), (double) 100); LOGGER.info("用戶{}搶紅包成功,金額:{}", userId,amount); } } latch.countDown(); }; executor.execute(task); } try { latch.await(); Integer restMoney = Integer.parseInt(redisUtil.getValue(redPacketId+"-money").toString()); LOGGER.info("剩餘金額:{}",restMoney); } catch (InterruptedException e) { e.printStackTrace(); } return Result.ok(); }
紅包隊列消息:多線程
/** * 紅包隊列消息 */ public class RedPacketMessage implements Delayed { private static final DateTimeFormatter F = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); /** * 默認延遲3秒 */ private static final long DELAY_MS = 1000L * 3; /** * 紅包 ID */ private final long redPacketId; /** * 建立時間戳 */ private final long timestamp; /** * 過時時間 */ private final long expire; /** * 描述信息 */ private final String description; public RedPacketMessage(long redPacketId, long expireSeconds) { this.redPacketId = redPacketId; this.timestamp = System.currentTimeMillis(); this.expire = this.timestamp + expireSeconds * 1000L; this.description = String.format("紅包[%s]-建立時間爲:%s,超時時間爲:%s", redPacketId, LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()).format(F), LocalDateTime.ofInstant(Instant.ofEpochMilli(expire), ZoneId.systemDefault()).format(F)); } public RedPacketMessage(long redPacketId) { this.redPacketId = redPacketId; this.timestamp = System.currentTimeMillis(); this.expire = this.timestamp + DELAY_MS; this.description = String.format("紅包[%s]-建立時間爲:%s,超時時間爲:%s", redPacketId, LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()).format(F), LocalDateTime.ofInstant(Instant.ofEpochMilli(expire), ZoneId.systemDefault()).format(F)); } public long getRedPacketId() { return redPacketId; } public long getTimestamp() { return timestamp; } public long getExpire() { return expire; } public String getDescription() { return description; } @Override public long getDelay(TimeUnit unit) { return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS); } @Override public int compareTo(Delayed o) { return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); } }
紅包延遲隊列:架構
/** * 紅包延遲隊列 */ public class RedPacketQueue { /** 用於多線程間下單的隊列 */ private static DelayQueue<RedPacketMessage> queue = new DelayQueue<>(); /** * 私有的默認構造子,保證外界沒法直接實例化 */ private RedPacketQueue(){} /** * 類級的內部類,也就是靜態的成員式內部類,該內部類的實例與外部類的實例 * 沒有綁定關係,並且只有被調用到纔會裝載,從而實現了延遲加載 */ private static class SingletonHolder{ /** * 靜態初始化器,由JVM來保證線程安全 */ private static RedPacketQueue queue = new RedPacketQueue(); } //單例隊列 public static RedPacketQueue getQueue(){ return SingletonHolder.queue; } /** * 生產入隊 * 一、執行加鎖操做 * 二、把元素添加到優先級隊列中 * 三、查看元素是否爲隊首 * 四、若是是隊首的話,設置leader爲空,喚醒全部等待的隊列 * 五、釋放鎖 */ public Boolean produce(RedPacketMessage message){ return queue.add(message); } /** * 消費出隊 * 一、執行加鎖操做 * 二、取出優先級隊列元素q的隊首 * 三、若是元素q的隊首/隊列爲空,阻塞請求 * 四、若是元素q的隊首(first)不爲空,得到這個元素的delay時間值 * 五、若是first的延遲delay時間值爲0的話,說明該元素已經到了可使用的時間,調用poll方法彈出該元素,跳出方法 * 六、若是first的延遲delay時間值不爲0的話,釋放元素first的引用,避免內存泄露 * 七、判斷leader元素是否爲空,不爲空的話阻塞當前線程 * 八、若是leader元素爲空的話,把當前線程賦值給leader元素,而後阻塞delay的時間,即等待隊首到達能夠出隊的時間,在finally塊中釋放leader元素的引用 * 九、循環執行從1~8的步驟 * 十、若是leader爲空而且優先級隊列不爲空的狀況下(判斷還有沒有其餘後續節點),調用signal通知其餘的線程 * 十一、執行解鎖操做 */ public RedPacketMessage consume() throws InterruptedException { return queue.take(); } }
紅包延遲隊列過時消費,監放任務:app
/** * 紅包延遲隊列過時消費 */ @Component("redPacket") public class TaskRunner implements ApplicationRunner { private final static Logger LOGGER = LoggerFactory.getLogger(TaskRunner.class); @Autowired private RedisUtil redisUtil; ExecutorService executorService = Executors.newSingleThreadExecutor(r -> { Thread thread = new Thread(r); thread.setName("RedPacketDelayWorker"); thread.setDaemon(true); return thread; }); @Override public void run(ApplicationArguments var){ executorService.execute(() -> { while (true) { try { RedPacketMessage message = RedPacketQueue.getQueue().consume(); if(message!=null){ long redPacketId = message.getRedPacketId(); LOGGER.info("紅包{}過時了",redPacketId); /** * 獲取剩餘紅包個數以及金額 */ int num = (int) redisUtil.getValue(redPacketId+"-num"); int restMoney = (int) redisUtil.getValue(redPacketId+"-money"); LOGGER.info("剩餘紅包個數{},剩餘紅包金額{}",num,restMoney); /** * 清空紅包數據 */ redisUtil.removeValue(redPacketId+"-num"); redisUtil.removeValue(redPacketId+"-money"); /** * 異步更新數據庫、異步退回紅包金額 */ } } catch (InterruptedException e) { e.printStackTrace(); } } }); } }
淘寶訂單到期,下單成功後60s以後給用戶發送短信通知,限時支付、緩存系統等等。
在Application
中有接口演示說明,你能夠在搶紅包 Red Packet Controller
接口中輸入任何參數進行測試,也能夠配合數據庫稍加修改便可做爲生產環境的搶紅包功能模塊。
https://gitee.com/52itstyle/spring-boot-seckill