使用redis 的key缓存淘汰监听机制实现游戏的自动开始、结束


废弃原因:使用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   到达:设置游戏状态为已结束

另外需要在:游戏取消、游戏结束(手动)、游戏下所有人员都已完成游戏  之后取消相对应的缓存

起风了,努力生存
原文地址:https://www.cnblogs.com/StivenYang/p/15560380.html