阅读本篇博文前,建议先查阅本篇博文的姊妹篇
https://blog.csdn.net/shenxiaomo1688/article/details/155241107?spm=1001.2014.3001.5501
当用户进行答题,消耗一定的能量,每隔30分钟恢复一个能量值,很多时候我们会想到使用定时任务,每隔一段时间扫描有哪些用户能量低于最大能量值,然后进行恢复。但不同用户恢复能量的时间点不一样。那么我们应该如何更好地实现用户能量恢复呢。答案是使用Redis的ZSet。
实现思路:
1.当用户进行答题扣除能量后,为该用户进行订阅,列入能量恢复的用户体系中;
2.然后在定时任务中为用户进行能量恢复;
3.如果用户开通会员,或者浏览广告恢复能量后,当该用户从能量恢复的用户体系中移除出去。
这样做的好处是避免用户能量表全表扫描。下面是实现的示例代码:
java
//用户扣除能量后,将该用户注册到用户能量恢复的订阅机制中
public void registerFullEnergyNotify(int uid, UserEnergyDTO entity, long now) {
// 每30分钟恢复1个能量值
public static final long RESTORE_INTERVAL_MILLIS = 30L * 60 * 1000;
//能量值满时通知key
public static final String ENERGY_NOTIFY_KEY = "userEnergy:full:notify";
int energy = entity.getEnergy();
int maxEnergy = entity.getMaxEnergy();
if (energy >= maxEnergy) {
// 已满,不需要提醒
redisTemplate.opsForZSet().remove(ENERGY_NOTIFY_KEY, String.valueOf(uid));
return;
}
long intervalMillis = ListeningConstant.RESTORE_INTERVAL_MILLIS;
long needPoints = maxEnergy - energy;
long fullRecoverAt = now + needPoints * intervalMillis;
redisTemplate.opsForZSet().add(
ENERGY_NOTIFY_KEY,
String.valueOf(uid),
fullRecoverAt
);
}
然后在定时任务中进行能量恢复
java
//每5分钟检查一次
@Scheduled(fixedDelay = 5 * 60 * 1000)
public void energyFullNotifyTask() {
long now = System.currentTimeMillis();
Set<Object> uids = redisTemplate.opsForZSet().rangeByScore(ENERGY_NOTIFY_KEY, 0, now);
if (uids == null || uids.isEmpty()) {
//log.info("当前没有能量回满需要通知的用户");
return;
}
for (Object obj : uids) {
String uidStr = String.valueOf(obj);
int uid = Integer.parseInt(uidStr);
try {
handleEnergyFull(uid);
} catch (Exception e) {
log.error("能量回满恢复失败 uid={}", uid, e);
continue;
}
// 成功后该用户移除出能量回满的任务队列
redisTemplate.opsForZSet().remove(ENERGY_NOTIFY_KEY, uidStr);
}
}
public void handleEnergyFull(int uid) {
//调用getEnergyInfo方法进行能量恢复
UserEnergyDTO dto = userEnergyService.getEnergyInfo(uid);
//在推送前再次确认能量是否已满
if (dto.getEnergy() < dto.getMaxEnergy()) {
log.error("能量未满但触发推送 uid={}", uid);
return;
}
//能量回满后可发送推送提醒用户
}
前面博文使用了redis hash存储用户能量信息,本篇使用zset管理用户能量恢复。
为什么能量设计一定会用到两种结构?
典型能量系统会同时关心这 3 类问题:
-
当前状态
-
当前能量值
-
最大能量
-
上次变化时间
-
-
时间驱动恢复
-
每 N 分钟恢复 1 点
-
什么时候恢复下一点
-
-
批量 / 定时处理
-
扫描哪些用户该恢复了
-
防止每次请求都实时算
-
Hash 擅长存"状态"
ZSet 擅长存"时间点 + 排序"
Hash vs ZSet 的明确分工表
| 维度 | Hash | ZSet |
|---|---|---|
| 存什么 | 当前状态 | 时间 / 排序 |
| 是否排序 | ❌ | ✅ |
| 是否范围查询 | ❌ | ✅ |
| 单用户读写 | ⭐⭐⭐⭐ | ⭐ |
| 批量扫描 | ❌ | ⭐⭐⭐⭐ |
| 是否适合能量值 | ✅ | ❌ |