看了很久博客園的博客,今天有點小衝動,寫一個小小分享,歡迎吐槽,O(∩_∩)O哈哈~ 背景: 微信活動小游戲開發,游戲中需要不斷的恢復體力值,體力相關數據都存儲在redis中。 需求: 1.當日首次登錄,增加全部體力值; 2.周期性增加定額體力值; 實現方案1(全量更新): 出於用戶量小、簡單的考慮 ...
看了很久博客園的博客,今天有點小衝動,寫一個小小分享,歡迎吐槽,O(∩_∩)O哈哈~
- 背景:
微信活動小游戲開發,游戲中需要不斷的恢復體力值,體力相關數據都存儲在redis中。
- 需求:
1.當日首次登錄,增加全部體力值;
2.周期性增加定額體力值;
- 實現方案1(全量更新):
出於用戶量小、簡單的考慮,對整個系統用戶執行全量更新。
相關表:
體力
表名strength,首碼strength:uid:*,體力表,哈希存儲 |
|||||||||
|
定時任務每5分鐘(可配置,用戶量大時間太短會導致任務疊加)掃描strgtimeline,當前時間和score(上一次恢復體力時間)對比,超過則更新時間,並且更新strength表。
表名strgupdate,字元串存儲,體力更新表 |
||||||
|
以下是一個體力值增加方法:
1 /** 2 * 添加體力值,返回true:標識添加成功 3 * @param uid 用戶ID 4 * @param nums 增加數量 5 * @return 6 */ 7 @Override 8 public boolean incrStrength(Long uid, int nums) { 9 boolean ret = false; 10 Jedis jedis = null; 11 // 用戶體力表 12 String strgKey = "strength:uid:" + uid; 13 try { 14 jedis = YlcqUtil.getRedis().getReadPool().getResource(); 15 jedis.watch(strgKey); 16 Transaction trans = jedis.multi(); 17 18 //獲取當前體力 19 List<String> strgInfo = jedis.hmget(strgKey, "surplus", "total"); 20 long surplus = Long.parseLong(strgInfo.get(0)); 21 long total = Long.parseLong(strgInfo.get(1)); 22 if(surplus == total){ 23 logger.info("體力值不能在增加!" + strgKey); 24 }else{ 25 surplus = surplus + nums; 26 if(surplus > total){ 27 surplus = total; 28 } 29 //更新體力 30 jedis.hset(strgKey, "surplus", surplus + ""); 31 List<Object> result = trans.exec(); 32 33 if (result == null || result.isEmpty()) { 34 logger.debug("增加體力值失敗!增加前:" + strgInfo + ";增加:" + nums); 35 } else { 36 ret = true; 37 } 38 } 39 } catch (Exception e) { 40 e.printStackTrace(); 41 } finally { 42 YlcqUtil.getRedis().getReadPool().returnResource(jedis); 43 } 44 return ret; 45 }
定時任務業務代碼:
1 /** 2 * 周期性恢復-執行任務 3 */ 4 public void cycleRecovery() { 5 logger.info("開始執行周期恢復體力任務!"); 6 String updateKey = "strgupdate"; 7 // 更新標識 8 String strgupdate = this.redisService.get(updateKey); 9 if (null != strgupdate && !TASK_STATUS_FINISHED.equals(strgupdate)) { 10 logger.info("恢復體力任務正在執行,本次任務終止(當前是周期性恢復任務)!"); 11 return; 12 } 13 try { 14 this.redisService.set(updateKey, TASK_STATUS_RUNNING); 15 doCycleTask(); 16 } catch (Exception e) { 17 e.printStackTrace(); 18 logger.info("周期恢復體力任務執行異常!" + e.getMessage()); 19 } finally { 20 this.redisService.set(updateKey, TASK_STATUS_FINISHED); 21 } 22 logger.info("周期恢復體力任務執行完成!"); 23 } 24 25 /** 26 * 周期性恢復-首次登錄 27 */ 28 public void firstLoginRecovery() { 29 logger.info("開始執行首次登錄恢復體力任務!"); 30 String updateKey = "strgupdate"; 31 // 更新標識 32 String strgupdate = this.redisService.get(updateKey); 33 if (null != strgupdate && !TASK_STATUS_FINISHED.equals(strgupdate)) { 34 logger.info("恢復體力任務正在執行,本次任務終止(當前是首次登錄恢復任務)!"); 35 return; 36 } 37 try { 38 this.redisService.set(updateKey, TASK_STATUS_RUNNING); 39 doFirstLoginTask(); 40 } catch (Exception e) { 41 e.printStackTrace(); 42 logger.info("首次登錄恢復體力任務執行異常!" + e.getMessage()); 43 } finally { 44 this.redisService.set(updateKey, TASK_STATUS_FINISHED); 45 } 46 logger.info("首次登錄恢復體力任務執行完成!"); 47 } 48 49 /** 50 * 當天首次登錄恢復體力 51 */ 52 private void doFirstLoginTask() { 53 String strgkey = "strength:uid:*"; 54 Set<String> allstrgs = this.redisService.hkeys(strgkey); 55 if (null == allstrgs || 0 == allstrgs.size()) { 56 logger.info("沒有需要恢復體力的用戶(首次登錄任務)!"); 57 return; 58 } 59 Iterator<String> it = allstrgs.iterator(); 60 while (it.hasNext()) { 61 String strength = it.next(); 62 List<String> strgInfo = this.redisService.hmget(strength, "surplus", "total"); 63 if (strgInfo.get(0).equals(strgInfo.get(1))) { 64 logger.info("體力值不需要恢復(首次登錄任務)!" + strength); 65 continue; 66 } 67 // 更新體力 68 Long setRlt = this.redisService.hset(strength, "surplus", strgInfo.get(1)); 69 if (null == setRlt || 0 == setRlt) { 70 logger.info("更新用戶體力表失敗(首次登錄任務)!" + strength); 71 } 72 } 73 } 74 75 /** 76 * 周期性恢復體力 77 */ 78 private void doCycleTask() { 79 String strgkey = "strength:uid:*"; 80 Set<String> allstrgs = this.redisService.hkeys(strgkey); 81 if (null == allstrgs || 0 == allstrgs.size()) { 82 logger.info("沒有需要恢復體力的用戶(周期恢復任務)!"); 83 return; 84 } 85 Iterator<String> it = allstrgs.iterator(); 86 while (it.hasNext()) { 87 String strength = it.next(); 88 List<String> strgInfo = this.redisService.hmget(strength, "surplus", "total"); 89 if (strgInfo.get(0).equals(strgInfo.get(1))) { 90 logger.info("體力值不需要恢復(周期恢復任務)!" + strength); 91 continue; 92 } 93 long surplus = Long.parseLong(strgInfo.get(0)); 94 long total = Long.parseLong(strgInfo.get(1)); 95 surplus = surplus + ActivityCommonService.STRENGTH_PLUS; 96 if (surplus > total) { 97 surplus = total; 98 } 99 // 更新體力 100 Long setRlt = this.redisService.hset(strength, "surplus", surplus + ""); 101 if (null == setRlt || 0 == setRlt) { 102 logger.info("更新用戶體力表失敗(周期恢復任務)!" + strength); 103 } 104 } 105 }
- 弊端分析:
每次任務都會把不需要恢復體力的用戶也都恢復一遍,而且這個還是周期性的任務,所以需要優化。
- 實現方案2(按需更新):
增加一個表,記錄更新時間。
用戶體力恢復的時候,同步維護一個體力時間有序集合表。
表名strgtimeline,有序集合存儲,體力時間表 |
|||||||||
用戶首次登錄初始化 jedis.zadd("strgtimeline", nowSecs, uid);
|
優化後代碼如下:
1 public boolean incrStrength(Long uid, int nums) { 2 boolean ret = false; 3 Jedis jedis = null; 4 // 用戶體力表 5 String strgKey = "strength:uid:" + uid; 6 //體力時間表 7 String strgtimeline = "strgtimeline"; 8 long nowSecs = TimeUtil.toSecs(System.currentTimeMillis()); 9 try { 10 jedis = YlcqUtil.getRedis().getReadPool().getResource(); 11 jedis.watch(strgKey); 12 Transaction trans = jedis.multi(); 13 14 //獲取當前體力 15 List<String> strgInfo = jedis.hmget(strgKey, "surplus", "total"); 16 long surplus = Long.parseLong(strgInfo.get(0)); 17 long total = Long.parseLong(strgInfo.get(1)); 18 if(surplus == total){ 19 logger.info("體力值不能在增加!" + strgKey); 20 }else{ 21 surplus = surplus + nums; 22 if(surplus > total){ 23 surplus = total; 24 } 25 //更新體力 26 jedis.hset(strgKey, "surplus", surplus + ""); 27 28 //插入體力時間 29 jedis.zadd(strgtimeline, nowSecs, uid.toString()); 30 List<Object> result = trans.exec(); 31 32 if (result == null || result.isEmpty()) { 33 logger.debug("增加體力值失敗!增加前:" + strgInfo + ";增加:" + nums); 34 } else { 35 ret = true; 36 } 37 } 38 } catch (Exception e) { 39 e.printStackTrace(); 40 } finally { 41 YlcqUtil.getRedis().getReadPool().returnResource(jedis); 42 } 43 return ret; 44 }
在體力值變更時,記錄一條時間戳值,作為下次更新的時重要依據。
定時任務也做簡單調整,使用redis的
zremrangeByScore方法,確保每次只更新最近兩個周期內的數據,也就是熱數據。
:
1 /** 2 * 周期性恢復-執行任務 3 */ 4 public void cycleRecovery() { 5 logger.info("開始執行周期恢復體力任務!"); 6 //更新標識 7 String strgupdate = this.redisService.get("strgupdate"); 8 if(null != strgupdate && !TASK_STATUS_FINISHED.equals(strgupdate)){ 9 logger.info("恢復體力任務正在執行,本次任務終止(當前是周期性恢復任務)!"); 10 return; 11 } 12 try { 13 this.redisService.set("strgupdate", TASK_STATUS_RUNNING); 14 doCycleTask(); 15 } catch (Exception e) { 16 e.printStackTrace(); 17 logger.info("周期恢復體力任務執行異常!" + e.getMessage()); 18 } finally { 19 this.redisService.set("strgupdate", TASK_STATUS_FINISHED); 20 } 21 logger.info("周期恢復體力任務執行完成!"); 22 } 23 24 /** 25 * 周期性恢復-首次登錄 26 */ 27 public void firstLoginRecovery() { 28 logger.info("開始執行首次登錄恢復體力任務!"); 29 //更新標識 30 String strgupdate = this.redisService.get("strgupdate"); 31 if(null != strgupdate && !TASK_STATUS_FINISHED.equals(strgupdate)){ 32 logger.info("恢復體力任務正在執行,本次任務終止(當前是首次登錄恢復任務)!"); 33 return; 34 } 35 try { 36 this.redisService.set("strgupdate", TASK_STATUS_RUNNING); 37 doFirstLoginTask(); 38 } catch (Exception e) { 39 e.printStackTrace(); 40 logger.info("首次登錄恢復體力任務執行異常!" + e.getMessage()); 41 } finally { 42 this.redisService.set("strgupdate", TASK_STATUS_FINISHED); 43 } 44 logger.info("首次登錄恢復體力任務執行完成!"); 45 } 46 47 /** 48 * 首次登錄恢復體力 49 */ 50 private void doFirstLoginTask() { 51 String strgkey = "strength:uid:*"; 52 Set<String> allstrgs = this.redisService.hkeys(strgkey); 53 if(null == allstrgs || 0 == allstrgs.size()){ 54 logger.info("沒有需要恢復體力的用戶(首次登錄任務)!"); 55 return; 56 } 57 String strgtimeline = "strgtimeline"; 58 long nowSecs = TimeUtil.toSecs(System.currentTimeMillis()); 59 Iterator<String> it = allstrgs.iterator(); 60 while(it.hasNext()){ 61 String strength = it.next(); 62 List<String> strgInfo = this.redisService.hmget(strength, "surplus", "total"); 63 if(strgInfo.get(0).equals(strgInfo.get(1))){ 64 logger.info("體力值不需要恢復(首次登錄任務)!" + strength); 65 continue; 66 } 67 //更新體力 68 Long setRlt = this.redisService.hset(strength, "surplus", strgInfo.get(1)); 69 if(null == setRlt || 0 == setRlt){ 70 logger.info("更新用戶體力表失敗(首次登錄任務)!" + strength); 71 }else{ 72 //插入體力時間 73 String uid = strength.split(":")[2]; 74 this.redisService.zadd(strgtimeline, nowSecs, uid); 75 } 76 } 77 } 78 79 /** 80 * 周期性恢復體力 81 */ 82 private void doCycleTask() { 83 //體力時間表 84 String strgtimeline = "strgtimeline"; 85 86 //刪除早於兩倍體力恢復周期之前是的數據,防止多次無用的恢復 87 long nowSecs = TimeUtil.toSecs(System.currentTimeMillis()); 88 long minSecs = nowSecs - ActivityCommonService.STRENGTH_PLUS_CYCLE_SECS * 2; 89 this.redisService.zremrangeByScore(strgtimeline, 0, minSecs - 1); 90 91 //獲取體力時間表 92 Set<Tuple> strgupdates = this.redisService.zrangeByScoreWithScores(strgtimeline, minSecs, nowSecs); 93 if(null == strgupdates || 0 == strgupdates.size()){ 94 logger.info("沒有需要恢復體力的用戶(周期恢復任務)!"); 95 return; 96 } 97 Iterator<Tuple> it = strgupdates.iterator(); 98 while(it.hasNext()){ 99 Tuple tuple = it.next(); 100 double lastRecoveryTime = tuple.getScore(); 101 if(nowSecs - lastRecoveryTime >= ActivityCommonService.STRENGTH_PLUS_CYCLE_SECS){ 102 String uid = tuple.getElement(); 103 String strength = "trength:uid:" + uid; 104 List<String> strgInfo = this.redisService.hmget(strength, "surplus", "total"); 105 long surplus = Long.parseLong(strgInfo.get(0)); 106 long total = Long.parseLong(strgInfo.get(1)); 107 if(surplus == total){ 108 logger.info("體力值不需要恢復(周期恢復任務)!" + strength); 109 continue; 110 } 111 surplus = surplus + ActivityCommonService.STRENGTH_PLUS; 112 if(surplus > total){ 113 surplus = total; 114 } 115 //更新體力 116 Long setRlt = this.redisService.hset(strength, "surplus", surplus + ""); 117 if(null == setRlt || 0 == setRlt){ 118 logger.info("更新用戶體力表失敗(周期恢復任務)!" + strength); 119 }else{ 120 //插入體力時間 121 this.redisService.zadd(strgtimeline, nowSecs, uid); 122 } 123 } 124 } 125 }
O(∩_∩)O~。
也是初試redis,感覺還可以,類似這種業務處理的都可以這麼做,提升效率。