废弃原因:使用redis缓存失效监听会有一定的延时,dev环境下延时已经达到90s左右,线上可能更甚,所以必须更换方案。
(基本上,expired
事件是在Redis服务器删除键的时候生成的,而不是在理论上生存时间达到零值时生成的。)
可参考文章:
https://blog.csdn.net/a13935302660/article/details/121285975
http://www.redis.cn/topics/notifications.html
技术选型
1.定时任务。-》因为游戏的开始时间和结束时间不确定,所以定时任务不可以用。
2. 消息中间件。-》公司目前使用的消息中间件是rocketmq,在rocketmq官网找到rocketmq目前仅支持指定时间片轮转。所以也不能使用mq实现定时功能。
3. redis缓存失效监听。-》利用redis提供的特性,key失效之后可以通知客户端对应的失效key值,将对应的信息放入key,对key进行过滤,实现自动开始\结束游戏。(已废弃)
流程图
代码
redis监听过期key的配置如下:
@Configuration
public class RedisListenerConfig {
@Bean
RedisMessageListenerContainer container(@Qualifier("getJedisConnectionFactory") RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
|
对应redis监听key过期的处理器逻辑如下:
/**
* redis key过期监听器
*
* @author yangjh
**/
@Slf4j
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
private final LockService lockService;
private final GmGameService gameService;
private final GameCommonService gameCommonService;
public static final String SEPARATOR_CHARS = ":";
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer, LockService lockService,
GmGameService gameService, GameCommonService gameCommonService) {
super(listenerContainer);
this.lockService = lockService;
this.gameService = gameService;
this.gameCommonService = gameCommonService;
}
/**
* 针对 redis 数据失效事件,进行数据处理
* @param message 失效的key
* @param pattern 过滤的表达式
*/
@Override
public void onMessage(Message message, byte[] pattern) {
// 获取到失效的 key,进行取消订单业务处理
String expiredKey = message.toString();
if (expiredKey.startsWith(GAME_START_PREFIX)) {
//到达游戏开始时间了,更新一波游戏状态
String[] keyArr = StringUtils.split(expiredKey, SEPARATOR_CHARS);
long gameId = Long.parseLong(keyArr[keyArr.length-1]);
log.debug("game {} is starting.", gameId);
try (RLockSupport locker = RLockSupport.ofLocker(lockService)){
locker.acquire(String.format(GAME_START_LOCK, gameId), INT_5, TimeUnit.SECONDS);
GmGame game = gameService.getById(gameId);
game.setUpdateTime(LocalDateTime.now());
game.setUpdator(StringUtils.EMPTY);
game.setGameStatus(GameStatusEnum.STARTED.getStatus());
gameService.updateById(game);
locker.unlock();
}
log.debug("game {} is started.", gameId);
} else if (expiredKey.startsWith(GAME_FINISH_PREFIX)) {
//到达游戏开始时间了,更新一波游戏状态
String[] keyArr = StringUtils.split(expiredKey, SEPARATOR_CHARS);
long gameId = Long.parseLong(keyArr[keyArr.length-1]);
log.debug("game {} is finishing.", gameId);
try (RLockSupport locker = RLockSupport.ofLocker(lockService)){
locker.acquire(String.format(GAME_AUTO_FINISH_LOCK, gameId), INT_5, TimeUnit.SECONDS);
GmGame game = gameService.getById(gameId);
game.setUpdateTime(LocalDateTime.now());
game.setUpdator(StringUtils.EMPTY);
game.setGameStatus(GameStatusEnum.FINISHED.getStatus());
gameService.updateById(game);
//游戏结束之后,给外包发送一个结束消息
gameCommonService.sendFinishToClient(game);
locker.unlock();
}
log.debug("game {} is finished.", gameId);
}
}
}
|
代码如下:
@Transactional(rollbackFor = Exception.class)
public Long publishGame(Long gameId) {
GmGame game = getById(gameId);
//只有未发布状态的游戏才可以发布
gameCommonService.checkGamePublishStatus(game);
//判断游戏下面是否有题目以及成员,如果没有题目和成员,也不可以发布
Integer userCount = gameUserMapper.selectCount(
new QueryWrapper<GmGameUser>().lambda().eq(GmGameUser::getDeleted, NOT_DELETED)
.eq(GmGameUser::getGameId, gameId));
Validate.isTrue(userCount > INT_0, GmcenterExceptionKeys.APIS_GAME_PUBLISH_USER_NOT_EMPTY);
Integer subjectCount = gameSubjectMapper.selectCount(
new QueryWrapper<GmGameSubject>().lambda().eq(GmGameSubject::getDeleted, NOT_DELETED)
.eq(GmGameSubject::getGameId, gameId));
Validate.isTrue(subjectCount > INT_0, GmcenterExceptionKeys.APIS_GAME_PUBLISH_SUBJECT_NOT_EMPTY);
getAndSetStatus(game);
gameCommonService.setUpdateField(game);
updateById(game);
setRedisClock(game);
return game.getId();
}
/**
* 设置redis定时开始/结束游戏
*
* @param game 游戏
*/
private void setRedisClock(GmGame game) {
Long gameId = game.getId();
int gameStatus = game.getGameStatus();
LocalDateTime endTime = game.getEndTime();
LocalDateTime startTime = game.getStartTime();
LocalDateTime now = LocalDateTime.now();
long startSecond = LocalDateTimeUtil.between(now, startTime, ChronoUnit.SECONDS);
long endSecond = LocalDateTimeUtil.between(now, endTime, ChronoUnit.SECONDS);
if (UN_START.getStatus().equals(gameStatus)) {
// case1:未开始状态->设置开始时间redis->判断endTime是否设置->设置结束redis
redissonClient.getBucket(GAME_START_PREFIX + gameId)
.set(gameId.toString(), Math.abs(startSecond), TimeUnit.SECONDS);
if (!DateUtil.isDefaultDateTime(endTime)) {
redissonClient.getBucket(GAME_FINISH_PREFIX + gameId)
.set(gameId.toString(), Math.abs(endSecond), TimeUnit.SECONDS);
}
} else if (STARTED.getStatus().equals(gameStatus) && !DateUtil.isDefaultDateTime(endTime)) {
// case2:开始状态->那就判断结束时间是否设置了->如果设置了就设置redis,没设置,就什么都不用设置了
redissonClient.getBucket(GAME_FINISH_PREFIX + gameId)
.set(gameId.toString(), Math.abs(endSecond), TimeUnit.SECONDS);
}
// case3:已结束状态->nothing(发布即结束)
}
/**
* 获取当前游戏状态,并根据开始结束时间设置发布之后的游戏状态
*
* @param game db中的游戏
*/
private void getAndSetStatus(GmGame game) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime startTime = game.getStartTime();
LocalDateTime endTime = game.getEndTime();
//now < startTime
if (now.compareTo(startTime) < INT_0) {
//当前时间小于开始时间,说明游戏还没开始
game.setGameStatus(UN_START.getStatus());
return;
}
//当前时间>=开始时间,标识游戏已经开始了,直接修改游戏状态为已开始
game.setGameStatus(STARTED.getStatus());
//当前时间>=开始时间 && 当前时间>=结束时间,直接修改游戏状态为已结束
if (!DateUtil.isDefaultDateTime(endTime) && now.compareTo(endTime) >= INT_0) {
game.setGameStatus(FINISHED.getStatus());
}
}
|
简单逻辑是:
游戏发布的时候,会判断是否到达开始时间和结束时间了:
switch 开始时间:
case 未到达:设置ttl为当前时间到开始时间的缓存
case 到达:设置游戏状态为已开始,并判断是否到达结束时间
switch 结束时间:
case 未到达:设置ttl为当前时间到开始时间的缓存
case 到达:设置游戏状态为已结束
另外需要在:游戏取消、游戏结束(手动)、游戏下所有人员都已完成游戏 之后取消相对应的缓存