你的位置:首页 > 数据库

[数据库]Redis小实战分享


看了很久博客园的博客,今天有点小冲动,写一个小小分享,欢迎吐槽,O(∩_∩)O哈哈~

  • 背景:

微信活动小游戏开发,游戏中需要不断的恢复体力值,体力相关数据都存储在redis中。

  • 需求:

1.当日首次登录,增加全部体力值;

2.周期性增加定额体力值;

  • 实现方案1(全量更新):

出于用户量小、简单的考虑,对整个系统用户执行全量更新。

相关表:

 

 

体力

 

表名strength,前缀strength:uid:*,体力表,哈希存储

列名

备注

surplus

当前体力余量

 

total

体力总量

 



 

定时任务每5分钟(可配置,用户量大时间太短会导致任务叠加)扫描strgtimeline,当前时间和score(上一次恢复体力时间)对比,超过则更新时间,并且更新strength表。

 

表名strgupdate,字符串存储,体力更新表

列名

备注

value

定时任务执行前,该值设置为1;

定时任务执行完成,该值设置为0;

定时任务启动时,并检查该值为0时,再执行恢复体力操作,防止任务叠加,否则结束本次定时任务


 

以下是一个体力值增加方法:

 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,有序集合存储,体力时间表

列名

备注

value

uid用户ID

 

score

用户恢复体力的时间戳

只是在定时任务恢复体力时才更新,其他方式增加体力值时不更新


 

用户首次登录初始化

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,感觉还可以,类似这种业务处理的都可以这么做,提升效率。