从零开始实现放置游戏——实现战斗挂机数据字典和缓存改造
Posted lyosaki88
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始实现放置游戏——实现战斗挂机数据字典和缓存改造相关的知识,希望对你有一定的参考价值。
上一章,我们添加了游戏的主界面和注册登录功能。由于距离上上篇间隔较长,可能有些内容想些的后来就忘了。同时,逻辑也不复杂,所以描述比较粗略。
现在随着模块的增加,整个架构也暴露出一些问题。本章我们将对整个系统进行大规模重构。
比如,之前为了快速开发,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; } }
附一、spring-data-redis
此缓存组件使用比较简单,安装好redis,添加好依赖和配置后。在需要缓存的方法上标记注解即可。主要有@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>
这里,我们需要配置一个叫 cacheManager 的 bean。之前我们一直使用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; } }
这里我只简单的配置了下,缓存的有效期为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; } } }
OK, hessian模块的缓存已改造完毕。可以尝试调用一下,redis里应该已经可以写入数据。
另外:这里我还添加了一个listByMapId方法,后面game模块会调用。这里没有再统一返回CommonResult类型。因为我在实际写代码过程中,发现每次调接口都去做判断实在太繁琐了,对内调用一般无需这么麻烦。一般在跨部门、公司之间的接口对接,或者对容错要求比较高时,可以将异常全部捕获处理。因此,后面对内的即接口都直接返回需要的数据类型。
二、RMS系统对应改造
hessian既然已经改成了redis缓存。RMS系统需要做对应的改造。game模块中读取了缓存,如果rms模块修改了数据,却没有更新redis缓存,会造成最终的数据不一致。
因此,我们将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()); } } }
另外,因为添加了数据字典。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()); } }
在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();
注意,这里使用了jQuery的ajax请求,必须在引用之前引用jquery。
小结
内容有些许遗漏,下周再补充些。
源码下载地址:https://545c.com/file/14960372-405053633
以上是关于从零开始实现放置游戏——实现战斗挂机数据字典和缓存改造的主要内容,如果未能解决你的问题,请参考以下文章