zoukankan      html  css  js  c++  java
  • 从零开始实现放置游戏(十二)——实现战斗挂机(3)数据字典和缓存改造

      上一章,我们添加了游戏的主界面和注册登录功能。由于距离上上篇间隔较长,可能有些内容想些的后来就忘了。同时,逻辑也不复杂,所以描述比较粗略。

      现在随着模块的增加,整个架构也暴露出一些问题。本章我们将对整个系统进行大规模重构。

      比如,之前为了快速开发,rms模块,我们采用了直接访问数据库的方式,对于rms模块本身来说,没有什么问题。

      但是,在game模块中,对于频繁访问的、不经常改变的数据或接口,希望采用缓存的方式,将数据缓存起来,减少后端压力,同时加快响应速度,从而提升体验。

      之前rms模块中尝试使用了EhCache,作为内存缓存。但现在增加了game模块,内存缓存无法在两个进程中共享。因此,我们引入redis,把缓存数据统一存到redis中。这里我们先使用spring-data-redis来进行缓存。通过在Service的方法上标记注解,来将方法返回结果进行缓存。这样一个粗粒度的缓存,目前能满足大部分需求。后面有需要时,我们再手动操作redis,进行细粒度的缓存。

      除了缓存改造,发现一些枚举值,比如:种族、职业、阵营等,目前以静态类、枚举类的形式,在各个模块定义,这样每当我修改时,需要同时修改几个地方。因此,我添加了数据字典表,将这类数据统一配置到数据库中,同时由于不常修改,各个模块可以直接将其读到缓存中。数据字典的UML类图如下。

      这样,我只需要一个静态类,枚举出父级配置即可,以后只会增加,一般情况下都不会修改。代码如下:

    package com.idlewow.datadict.model;
    
    import java.io.Serializable;
    
    public enum DataType implements Serializable {
        Occupy("10100", "领土归属"),
        Faction("10110", "阵营"),
        Race("10200", "种族"),
        Job("10250", "职业"),
        MobType("10300", "怪物类型"),
        MobClass("10310", "怪物种类");
    
        private String code;
        private String value;
    
        DataType(String code, String value) {
            this.code = code;
            this.value = value;
        }
    
        public String getCode() {
            return code;
        }
    
        public String getValue() {
            return value;
        }
    }
    DataType.java

    附一、spring缓存

      spring-context包下,有关于缓存的注解类,可以直接使用。在需要缓存的方法上标记注解即可。主要有@Cacheable、@CacheEvict、@CachePut。

    例一:下面的注解,代表此方法执行成功后,将返回结果缓存到redis中, key为 mapMob:#{id},当结果为NULL时,不缓存。

    @Cacheable(value = "mapMob", key = "#id", unless = "#result == null")
    public CommonResult find(String id) {
        return super.find(id);
    }    
    

    例二:下面的注解,代表此方法执行成功后,将缓存 dataDict: 中的键全部清除

    @CacheEvict(value = "dataDict", allEntries = true)
    public CommonResult update(DataDict dataDict) {
        return super.update(dataDict);
    }
    

    例三:下面的注解,代表方法执行成功后,将key为 levelExp:#{id} 的缓存更新为方法返回的结果

    @CachePut(value = "levelExp", key = "#levelExp.getId()")
    public CommonResult update(LevelExp levelExp) {
        return super.update(levelExp);
    }

    一、缓存改造

      因为是在hessian的方法上进行缓存,这里我们在hessian模块的pom.xml中添加依赖如下:

            <!-- 缓存相关 -->
            <dependency>
                <groupId>org.springframework.data</groupId>
                <artifactId>spring-data-redis</artifactId>
                <version>2.2.0.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>redis.clients</groupId>
                <artifactId>jedis</artifactId>
                <version>3.1.0</version>
            </dependency>
    pom.xml

      这里,我们需要配置一个叫 cacheManager 的 bean,通过引用spring-data-redis的包,进行配置,来把加了注解的缓存存进redis。

      之前我们一直使用xml对各组件进行配置,此 cacheManager 也可以使用xml进行配置。但在实际使用中,我想将redis的key统一配置成 idlewow:xxx:...,研究了半天未找到xml形式的配置方法,因此这里使用Java代码进行配置。

      在hessian模块添加包 com.idlewow,然后新建  CacheConfig 类,如下:

    package com.idlewow.config;
    
    import org.springframework.cache.CacheManager;
    import org.springframework.cache.annotation.CachingConfigurerSupport;
    import org.springframework.cache.annotation.EnableCaching;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.cache.RedisCacheConfiguration;
    import org.springframework.data.redis.cache.RedisCacheManager;
    import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
    import redis.clients.jedis.JedisPoolConfig;
    
    import java.time.Duration;
    
    @EnableCaching
    @Configuration
    public class CacheConfig extends CachingConfigurerSupport {
        @Bean
        public JedisPoolConfig jedisPoolConfig() {
            JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
            jedisPoolConfig.setMaxTotal(200);
            jedisPoolConfig.setMaxIdle(50);
            jedisPoolConfig.setMinIdle(20);
            jedisPoolConfig.setMaxWaitMillis(5000);
            jedisPoolConfig.setTestOnBorrow(true);
            jedisPoolConfig.setTestOnReturn(false);
            return jedisPoolConfig;
        }
    
        @Bean
        public JedisConnectionFactory jedisConnectionFactory() {
            JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(jedisPoolConfig());
            return jedisConnectionFactory;
        }
    
        @Bean
        public CacheManager cacheManager() {
            RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofHours(1))
                    .disableCachingNullValues()
                    .computePrefixWith(cacheName -> "idlewow:" + cacheName + ":");
    
            RedisCacheManager redisCacheManager = RedisCacheManager.builder(jedisConnectionFactory())
                    .cacheDefaults(configuration)
                    .build();
    
            return redisCacheManager;
        }
    }
    CacheConfig

        这里我只简单的配置了下,缓存的有效期为1小时,当结果为NULL时不缓存,key前缀为 idlewow:。 有兴趣的话可以研究下到底能否用xml配置key前缀,注意这里用的是spring-data-redis 2.x版本,和 1.x 版本配置区别较大。

        添加好依赖后,我们需要在服务的方法上打上标记即可。服务的实现类,在core模块下。

      比如,我们这里以 MapMobServiceImpl 为例,此服务的方法update、delete、find执行成功后,我们均需要更新缓存。因为我们不缓存NULL值,因此add执行后,无需更新缓存。这里的方法已经在BaseServiceImpl里实现过来,但需要打注解,不能直接在父类里标记,因此各个子类重写一下方法签名,内容直接 super.find(id),即可,也比较方便。代码如下:

    package com.idlewow.mob.service.impl;
    
    import com.idlewow.common.BaseServiceImpl;
    import com.idlewow.common.model.CommonResult;
    import com.idlewow.mob.manager.MapMobManager;
    import com.idlewow.mob.model.MapMob;
    import com.idlewow.mob.service.MapMobService;
    import com.idlewow.query.model.MapMobQueryParam;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cache.annotation.CacheEvict;
    import org.springframework.cache.annotation.CachePut;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    
    @Service("mapMobService")
    public class MapMobServiceImpl extends BaseServiceImpl<MapMob, MapMobQueryParam> implements MapMobService {
        @Autowired
        MapMobManager mapMobManager;
    
        /**
         * 更新数据
         *
         * @param mapMob 数据对象
         * @return
         */
        @Override
        @CachePut(value = "mapMob", key = "#mapMob.getId()")
        public CommonResult update(MapMob mapMob) {
            return super.update(mapMob);
        }
    
        /**
         * 删除数据
         *
         * @param id 主键id
         * @return
         */
        @Override
        @CacheEvict(value = "mapMob", key = "#id")
        public CommonResult delete(String id) {
            return super.delete(id);
        }
    
        /**
         * 根据ID查询
         *
         * @param id 主键id
         * @return
         */
        @Override
        @Cacheable(value = "mapMob", key = "#id")
        public CommonResult find(String id) {
            return super.find(id);
        }
    
        /**
         * 根据地图ID查询列表
         *
         * @param mapId 地图ID
         * @return
         */
        @Override
        @Cacheable(value = "mapMobList", key = "#mapId", unless = "#reuslt==null")
        public List<MapMob> listByMapId(String mapId) {
            try {
                return mapMobManager.listByMapId(mapId);
            } catch (Exception ex) {
                logger.error(ex.getMessage(), ex);
                return null;
            }
        }
    }
    MapMobServiceImpl

      OK, hessian模块的缓存已改造完毕。可以尝试调用一下,redis里应该已经可以写入数据。

      另外:这里我还添加了一个listByMapId方法,后面game模块会调用。这里没有再统一返回CommonResult类型。因为我在实际写代码过程中,发现每次调接口都去做判断实在太繁琐了,对内调用一般无需这么麻烦。一般在跨部门、公司之间的接口对接,或者对容错要求比较高时,可以将异常全部捕获处理。因此,后面对内的即接口都直接返回需要的数据类型。

    二、RMS系统对应改造

      hessian既然已经改成了redis缓存,如果rms模块系统逻辑不变,修改了数据,却没有更新redis缓存,那game模块在调用hessian时,如果读取了缓存,就会造成数据的不一致。

      因此,我们将rms模块改造为通过访问hessian服务来读写数据,这样调用hessian方法时就能触发缓存,不再直接访问数据库。

      这里把EhCache、数据库相关的代码、配置、依赖都删掉。并在pom中添加对hessian的引用,并像game模块一样,配置hessian-client.xml并在applicationContext.xml中引入。

      在代码中,我们将CrudController中的BaseManager替换成BaseService,并将其他地方做对应修改。如下图:

    package com.idlewow.rms.controller;
    
    import com.idlewow.common.model.BaseModel;
    import com.idlewow.common.model.CommonResult;
    import com.idlewow.common.model.PageList;
    import com.idlewow.common.model.QueryParam;
    import com.idlewow.common.service.BaseService;
    import com.idlewow.util.validation.ValidateGroup;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import javax.servlet.http.HttpServletRequest;
    
    public abstract class CrudController<T extends BaseModel, Q extends QueryParam> extends BaseController {
        private final String path = this.getClass().getAnnotation(RequestMapping.class).value()[0];
    
        @Autowired
        BaseService<T, Q> baseService;
        @Autowired
        HttpServletRequest request;
    
        @RequestMapping("/list")
        public Object list() {
            return this.path + "/list";
        }
    
        @ResponseBody
        @RequestMapping(value = "/list", method = RequestMethod.POST)
        public Object list(@RequestParam(value = "page", defaultValue = "1") int pageIndex, @RequestParam(value = "limit", defaultValue = "10") int pageSize, Q q) {
            try {
                q.setPage(pageIndex, pageSize);
                CommonResult commonResult = baseService.list(q);
                if (commonResult.isSuccess()) {
                    PageList<T> pageList = (PageList<T>) commonResult.getData();
                    return this.parseTable(pageList);
                } else {
                    request.setAttribute("errorMessage", commonResult.getMessage());
                    return "/error";
                }
            } catch (Exception ex) {
                logger.error(ex.getMessage(), ex);
                request.setAttribute("errorMessage", ex.getMessage());
                return "/error";
            }
        }
    
        @RequestMapping("/add")
        public Object add() {
            return this.path + "/add";
        }
    
        @ResponseBody
        @RequestMapping(value = "/add", method = RequestMethod.POST)
        public Object add(@RequestBody T t) {
            try {
                CommonResult commonResult = this.validate(t, ValidateGroup.Create.class);
                if (!commonResult.isSuccess())
                    return commonResult;
    
                t.setCreateUser(this.currentUserName());
                commonResult = baseService.insert(t);
                return commonResult;
            } catch (Exception ex) {
                logger.error(ex.getMessage(), ex);
                return CommonResult.fail(ex.getMessage());
            }
        }
    
        @RequestMapping(value = "/edit/{id}", method = RequestMethod.GET)
        public Object edit(@PathVariable String id, Model model) {
            try {
                CommonResult commonResult = baseService.find(id);
                if (commonResult.isSuccess()) {
                    T t = (T) commonResult.getData();
                    model.addAttribute(t);
                    return this.path + "/edit";
                } else {
                    request.setAttribute("errorMessage", commonResult.getMessage());
                    return "/error";
                }
            } catch (Exception ex) {
                logger.error(ex.getMessage(), ex);
                request.setAttribute("errorMessage", ex.getMessage());
                return "/error";
            }
        }
    
        @ResponseBody
        @RequestMapping(value = "/edit/{id}", method = RequestMethod.POST)
        public Object edit(@PathVariable String id, @RequestBody T t) {
            try {
                if (!id.equals(t.getId())) {
                    return CommonResult.fail("id不一致");
                }
    
                CommonResult commonResult = this.validate(t, ValidateGroup.Update.class);
                if (!commonResult.isSuccess())
                    return commonResult;
    
                t.setUpdateUser(this.currentUserName());
                commonResult = baseService.update(t);
                return commonResult;
            } catch (Exception ex) {
                logger.error(ex.getMessage(), ex);
                return CommonResult.fail(ex.getMessage());
            }
        }
    
        @ResponseBody
        @RequestMapping(value = "/delete/{id}", method = RequestMethod.POST)
        public Object delete(@PathVariable String id) {
            try {
                baseService.delete(id);
                return CommonResult.success();
            } catch (Exception ex) {
                logger.error(ex.getMessage(), ex);
                return CommonResult.fail(ex.getMessage());
            }
        }
    }
    CrudController.java

      另外,因为添加了数据字典。rms模块需要添加对应的contoller和页面。这里不一一赘述。既然已经有了数据字典,之前写死的枚举,EnumUtil都可以废除了。直接从hessian读取数据字典配置到缓存。

      在com.idlewow.rms.support.util包下添加DataDictUtil类,代码如下:

    package com.idlewow.rms.support.util;
    
    import com.idlewow.common.model.CommonResult;
    import com.idlewow.datadict.model.DataDict;
    import com.idlewow.datadict.model.DataType;
    import com.idlewow.datadict.service.DataDictService;
    import com.idlewow.query.model.DataDictQueryParam;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.io.Serializable;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    @Component
    public class DataDictUtil implements Serializable {
        private static final Logger logger = LogManager.getLogger(DataDictUtil.class);
    
        @Autowired
        DataDictService dataDictService;
    
        public static Map<String, Map<String, String>> ConfigMap = new HashMap<>();
    
        public void initialize() {
            DataDictQueryParam dataDictQueryParam = new DataDictQueryParam();
            CommonResult commonResult = dataDictService.list(dataDictQueryParam);
            if (commonResult.isSuccess()) {
                List<DataDict> dataDictList = (List<DataDict>) commonResult.getData();
                for (DataDict dataDict : dataDictList) {
                    if (ConfigMap.containsKey(dataDict.getParentCode())) {
                        ConfigMap.get(dataDict.getParentCode()).put(dataDict.getCode(), dataDict.getValue());
                    } else {
                        Map map = new HashMap();
                        map.put(dataDict.getCode(), dataDict.getValue());
                        ConfigMap.put(dataDict.getParentCode(), map);
                    }
                }
            } else {
                logger.error("缓存加载失败!");
            }
        }
    
        public static Map<String, String> occupy() {
            return ConfigMap.get(DataType.Occupy.getCode());
        }
    
        public static Map<String, String> job() {
            return ConfigMap.get(DataType.Job.getCode());
        }
    
        public static Map<String, String> faction() {
            return ConfigMap.get(DataType.Faction.getCode());
        }
    
        public static Map<String, String> mobClass() {
            return ConfigMap.get(DataType.MobClass.getCode());
        }
    
        public static Map<String, String> mobType() {
            return ConfigMap.get(DataType.MobType.getCode());
        }
    }
    DataDictUtil.java

      在StartUpListener中,初始化缓存:

        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            logger.info("缓存初始化。。。");
            dataDictUtil.initialize();
            logger.info("缓存初始化完毕。。。");
        }
    

      后端缓存有了,同样的,前端写死的枚举也不需要了。可以使用localStorage进行缓存。代码如下:

    /* 数据字典缓存 */
    var _cache = {
        version: new Date().getTime(),
        configmap: null
    };
    
    /* 读取缓存 */
    function loadCache() {
        if (_cache.configmap == null || (new Date().getTime() - _cache.version) > 1000 * 60 * 30) {
            var localConfigMap = localStorage.getItem("configmap");
            if (localConfigMap) {
                _cache.configmap = JSON.parse(localConfigMap);
            } else {
                /* 读取数据字典缓存 */
                $.ajax({
                    url: '/manage/data_dict/configMap',
                    type: 'post',
                    success: function (data) {
                        _cache.configmap = data;
                        localStorage.setItem("configmap", JSON.stringify(_cache.configmap));
                    },
                    error: function () {
                        alert('ajax error');
                    }
                });
            }
        }
    }
    
    /* 数据字典Key */
    var DataType = {
        "Occupy": "10100",  // 领土归属
        "Faction": "10110", // 阵营
        "Race": "10200",    // 种族
        "Job": "10250",     // 职业
        "MobType": "10300", // 怪物类型
        "MobClass": "10310" // 怪物种类
    };
    
    DataDict.prototype = {
        occupy: function (value) {
            return _cache.configmap[DataType.Occupy][value];
        },
        job: function (value) {
            return _cache.configmap[DataType.Job][value];
        },
        faction: function (value) {
            return _cache.configmap[DataType.Faction][value];
        },
        mobClass: function (value) {
            return _cache.configmap[DataType.MobClass][value];
        },
        mobType: function (value) {
            return _cache.configmap[DataType.MobType][value];
        }
    };
    
    loadCache();
    Helper.js

      注意,这里使用了jQuery的ajax请求,必须在引用之前引用jquery。

    小结

           内容有些许遗漏,下周再补充些。

      源码下载地址:https://545c.com/file/14960372-405053633

       项目交流群:329989095

  • 相关阅读:
    linux常用命令整理
    总结五大常用算法!
    python数组(列表、元组及字典)
    网络编程---笔记1
    python3与python2的区别 记录一波
    Python学习week3-python数据结构介绍与列表
    Python学习week2-python介绍与pyenv安装
    Python学习week1-linux文件系统与IO重定向
    Python学习week1-计算机基础
    css3文本多行省略
  • 原文地址:https://www.cnblogs.com/lyosaki88/p/idlewow_12.html
Copyright © 2011-2022 走看看