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

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

    起风了,努力生存
  • 相关阅读:
    升讯威周报与工时统计系统 V3
    浅谈互联网时代的一万小时定律:方向与格局更重要
    GitHub开源:SQLite 增强组件 Sheng.SQLite.Plus
    GitHub开源:升讯威 SQLite 增强组件 Sheng.SQLite.Plus
    centos7 配置IPV6
    Vertica节点故障后的恢复经过
    windows下杀掉指定端口的应用
    解决QT Fault tolerant heap shim问题
    vertica生成查询计划失败:Request size too big
    IDEA配置aliyun的maven源
  • 原文地址:https://www.cnblogs.com/StivenYang/p/15560380.html
Copyright © 2020-2023  润新知