经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » 游戏设计 » 查看文章
从零开始实现放置游戏(十二)——实现战斗挂机(3)数据字典和缓存改造
来源:cnblogs  作者:丶谦信  时间:2019/11/4 8:29:48  对本文有异议

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

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

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

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

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

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

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

  1. package com.idlewow.datadict.model;
  2. import java.io.Serializable;
  3. public enum DataType implements Serializable {
  4. Occupy("10100", "领土归属"),
  5. Faction("10110", "阵营"),
  6. Race("10200", "种族"),
  7. Job("10250", "职业"),
  8. MobType("10300", "怪物类型"),
  9. MobClass("10310", "怪物种类");
  10. private String code;
  11. private String value;
  12. DataType(String code, String value) {
  13. this.code = code;
  14. this.value = value;
  15. }
  16. public String getCode() {
  17. return code;
  18. }
  19. public String getValue() {
  20. return value;
  21. }
  22. }
DataType.java

附一、spring-data-redis

  此缓存组件使用比较简单,安装好redis,添加好依赖和配置后。在需要缓存的方法上标记注解即可。主要有@Cacheable、@CacheEvict、@CachePut。

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

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

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

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

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

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

  

一、缓存改造

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

  1. <!-- 缓存相关 -->
  2. <dependency>
  3. <groupId>org.springframework.data</groupId>
  4. <artifactId>spring-data-redis</artifactId>
  5. <version>2.2.0.RELEASE</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>redis.clients</groupId>
  9. <artifactId>jedis</artifactId>
  10. <version>3.1.0</version>
  11. </dependency>
pom.xml

  这里,我们需要配置一个叫 cacheManager 的 bean。之前我们一直使用xml对各组件进行配置,此 cacheManager 也可以使用xml进行配置。但在实际使用中,我想将redis的key统一配置成 idlewow:xxx:...,研究了半天未找到xml形式的配置方法,因此这里使用Java代码进行配置。在hessian模块添加包 com.idlewow,然后新建  CacheConfig 类,如下:

  1. package com.idlewow.config;
  2. import org.springframework.cache.CacheManager;
  3. import org.springframework.cache.annotation.CachingConfigurerSupport;
  4. import org.springframework.cache.annotation.EnableCaching;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.context.annotation.Configuration;
  7. import org.springframework.data.redis.cache.RedisCacheConfiguration;
  8. import org.springframework.data.redis.cache.RedisCacheManager;
  9. import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
  10. import redis.clients.jedis.JedisPoolConfig;
  11. import java.time.Duration;
  12. @EnableCaching
  13. @Configuration
  14. public class CacheConfig extends CachingConfigurerSupport {
  15. @Bean
  16. public JedisPoolConfig jedisPoolConfig() {
  17. JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
  18. jedisPoolConfig.setMaxTotal(200);
  19. jedisPoolConfig.setMaxIdle(50);
  20. jedisPoolConfig.setMinIdle(20);
  21. jedisPoolConfig.setMaxWaitMillis(5000);
  22. jedisPoolConfig.setTestOnBorrow(true);
  23. jedisPoolConfig.setTestOnReturn(false);
  24. return jedisPoolConfig;
  25. }
  26. @Bean
  27. public JedisConnectionFactory jedisConnectionFactory() {
  28. JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(jedisPoolConfig());
  29. return jedisConnectionFactory;
  30. }
  31. @Bean
  32. public CacheManager cacheManager() {
  33. RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
  34. .entryTtl(Duration.ofHours(1))
  35. .disableCachingNullValues()
  36. .computePrefixWith(cacheName -> "idlewow:" + cacheName + ":");
  37. RedisCacheManager redisCacheManager = RedisCacheManager.builder(jedisConnectionFactory())
  38. .cacheDefaults(configuration)
  39. .build();
  40. return redisCacheManager;
  41. }
  42. }
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),即可,也比较方便。代码如下:

  1. package com.idlewow.mob.service.impl;
  2. import com.idlewow.common.BaseServiceImpl;
  3. import com.idlewow.common.model.CommonResult;
  4. import com.idlewow.mob.manager.MapMobManager;
  5. import com.idlewow.mob.model.MapMob;
  6. import com.idlewow.mob.service.MapMobService;
  7. import com.idlewow.query.model.MapMobQueryParam;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.cache.annotation.CacheEvict;
  10. import org.springframework.cache.annotation.CachePut;
  11. import org.springframework.cache.annotation.Cacheable;
  12. import org.springframework.stereotype.Service;
  13. import java.util.List;
  14. @Service("mapMobService")
  15. public class MapMobServiceImpl extends BaseServiceImpl<MapMob, MapMobQueryParam> implements MapMobService {
  16. @Autowired
  17. MapMobManager mapMobManager;
  18. /**
  19. * 更新数据
  20. *
  21. * @param mapMob 数据对象
  22. * @return
  23. */
  24. @Override
  25. @CachePut(value = "mapMob", key = "#mapMob.getId()")
  26. public CommonResult update(MapMob mapMob) {
  27. return super.update(mapMob);
  28. }
  29. /**
  30. * 删除数据
  31. *
  32. * @param id 主键id
  33. * @return
  34. */
  35. @Override
  36. @CacheEvict(value = "mapMob", key = "#id")
  37. public CommonResult delete(String id) {
  38. return super.delete(id);
  39. }
  40. /**
  41. * 根据ID查询
  42. *
  43. * @param id 主键id
  44. * @return
  45. */
  46. @Override
  47. @Cacheable(value = "mapMob", key = "#id")
  48. public CommonResult find(String id) {
  49. return super.find(id);
  50. }
  51. /**
  52. * 根据地图ID查询列表
  53. *
  54. * @param mapId 地图ID
  55. * @return
  56. */
  57. @Override
  58. @Cacheable(value = "mapMobList", key = "#mapId", unless = "#reuslt==null")
  59. public List<MapMob> listByMapId(String mapId) {
  60. try {
  61. return mapMobManager.listByMapId(mapId);
  62. } catch (Exception ex) {
  63. logger.error(ex.getMessage(), ex);
  64. return null;
  65. }
  66. }
  67. }
MapMobServiceImpl

  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,并将其他地方做对应修改。如下图:

  1. package com.idlewow.rms.controller;
  2. import com.idlewow.common.model.BaseModel;
  3. import com.idlewow.common.model.CommonResult;
  4. import com.idlewow.common.model.PageList;
  5. import com.idlewow.common.model.QueryParam;
  6. import com.idlewow.common.service.BaseService;
  7. import com.idlewow.util.validation.ValidateGroup;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.ui.Model;
  10. import org.springframework.web.bind.annotation.PathVariable;
  11. import org.springframework.web.bind.annotation.RequestBody;
  12. import org.springframework.web.bind.annotation.RequestMapping;
  13. import org.springframework.web.bind.annotation.RequestMethod;
  14. import org.springframework.web.bind.annotation.RequestParam;
  15. import org.springframework.web.bind.annotation.ResponseBody;
  16. import javax.servlet.http.HttpServletRequest;
  17. public abstract class CrudController<T extends BaseModel, Q extends QueryParam> extends BaseController {
  18. private final String path = this.getClass().getAnnotation(RequestMapping.class).value()[0];
  19. @Autowired
  20. BaseService<T, Q> baseService;
  21. @Autowired
  22. HttpServletRequest request;
  23. @RequestMapping("/list")
  24. public Object list() {
  25. return this.path + "/list";
  26. }
  27. @ResponseBody
  28. @RequestMapping(value = "/list", method = RequestMethod.POST)
  29. public Object list(@RequestParam(value = "page", defaultValue = "1") int pageIndex, @RequestParam(value = "limit", defaultValue = "10") int pageSize, Q q) {
  30. try {
  31. q.setPage(pageIndex, pageSize);
  32. CommonResult commonResult = baseService.list(q);
  33. if (commonResult.isSuccess()) {
  34. PageList<T> pageList = (PageList<T>) commonResult.getData();
  35. return this.parseTable(pageList);
  36. } else {
  37. request.setAttribute("errorMessage", commonResult.getMessage());
  38. return "/error";
  39. }
  40. } catch (Exception ex) {
  41. logger.error(ex.getMessage(), ex);
  42. request.setAttribute("errorMessage", ex.getMessage());
  43. return "/error";
  44. }
  45. }
  46. @RequestMapping("/add")
  47. public Object add() {
  48. return this.path + "/add";
  49. }
  50. @ResponseBody
  51. @RequestMapping(value = "/add", method = RequestMethod.POST)
  52. public Object add(@RequestBody T t) {
  53. try {
  54. CommonResult commonResult = this.validate(t, ValidateGroup.Create.class);
  55. if (!commonResult.isSuccess())
  56. return commonResult;
  57. t.setCreateUser(this.currentUserName());
  58. commonResult = baseService.insert(t);
  59. return commonResult;
  60. } catch (Exception ex) {
  61. logger.error(ex.getMessage(), ex);
  62. return CommonResult.fail(ex.getMessage());
  63. }
  64. }
  65. @RequestMapping(value = "/edit/{id}", method = RequestMethod.GET)
  66. public Object edit(@PathVariable String id, Model model) {
  67. try {
  68. CommonResult commonResult = baseService.find(id);
  69. if (commonResult.isSuccess()) {
  70. T t = (T) commonResult.getData();
  71. model.addAttribute(t);
  72. return this.path + "/edit";
  73. } else {
  74. request.setAttribute("errorMessage", commonResult.getMessage());
  75. return "/error";
  76. }
  77. } catch (Exception ex) {
  78. logger.error(ex.getMessage(), ex);
  79. request.setAttribute("errorMessage", ex.getMessage());
  80. return "/error";
  81. }
  82. }
  83. @ResponseBody
  84. @RequestMapping(value = "/edit/{id}", method = RequestMethod.POST)
  85. public Object edit(@PathVariable String id, @RequestBody T t) {
  86. try {
  87. if (!id.equals(t.getId())) {
  88. return CommonResult.fail("id不一致");
  89. }
  90. CommonResult commonResult = this.validate(t, ValidateGroup.Update.class);
  91. if (!commonResult.isSuccess())
  92. return commonResult;
  93. t.setUpdateUser(this.currentUserName());
  94. commonResult = baseService.update(t);
  95. return commonResult;
  96. } catch (Exception ex) {
  97. logger.error(ex.getMessage(), ex);
  98. return CommonResult.fail(ex.getMessage());
  99. }
  100. }
  101. @ResponseBody
  102. @RequestMapping(value = "/delete/{id}", method = RequestMethod.POST)
  103. public Object delete(@PathVariable String id) {
  104. try {
  105. baseService.delete(id);
  106. return CommonResult.success();
  107. } catch (Exception ex) {
  108. logger.error(ex.getMessage(), ex);
  109. return CommonResult.fail(ex.getMessage());
  110. }
  111. }
  112. }
CrudController.java

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

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

  1. package com.idlewow.rms.support.util;
  2. import com.idlewow.common.model.CommonResult;
  3. import com.idlewow.datadict.model.DataDict;
  4. import com.idlewow.datadict.model.DataType;
  5. import com.idlewow.datadict.service.DataDictService;
  6. import com.idlewow.query.model.DataDictQueryParam;
  7. import org.apache.logging.log4j.LogManager;
  8. import org.apache.logging.log4j.Logger;
  9. import org.springframework.beans.factory.annotation.Autowired;
  10. import org.springframework.stereotype.Component;
  11. import java.io.Serializable;
  12. import java.util.HashMap;
  13. import java.util.List;
  14. import java.util.Map;
  15. @Component
  16. public class DataDictUtil implements Serializable {
  17. private static final Logger logger = LogManager.getLogger(DataDictUtil.class);
  18. @Autowired
  19. DataDictService dataDictService;
  20. public static Map<String, Map<String, String>> ConfigMap = new HashMap<>();
  21. public void initialize() {
  22. DataDictQueryParam dataDictQueryParam = new DataDictQueryParam();
  23. CommonResult commonResult = dataDictService.list(dataDictQueryParam);
  24. if (commonResult.isSuccess()) {
  25. List<DataDict> dataDictList = (List<DataDict>) commonResult.getData();
  26. for (DataDict dataDict : dataDictList) {
  27. if (ConfigMap.containsKey(dataDict.getParentCode())) {
  28. ConfigMap.get(dataDict.getParentCode()).put(dataDict.getCode(), dataDict.getValue());
  29. } else {
  30. Map map = new HashMap();
  31. map.put(dataDict.getCode(), dataDict.getValue());
  32. ConfigMap.put(dataDict.getParentCode(), map);
  33. }
  34. }
  35. } else {
  36. logger.error("缓存加载失败!");
  37. }
  38. }
  39. public static Map<String, String> occupy() {
  40. return ConfigMap.get(DataType.Occupy.getCode());
  41. }
  42. public static Map<String, String> job() {
  43. return ConfigMap.get(DataType.Job.getCode());
  44. }
  45. public static Map<String, String> faction() {
  46. return ConfigMap.get(DataType.Faction.getCode());
  47. }
  48. public static Map<String, String> mobClass() {
  49. return ConfigMap.get(DataType.MobClass.getCode());
  50. }
  51. public static Map<String, String> mobType() {
  52. return ConfigMap.get(DataType.MobType.getCode());
  53. }
  54. }
DataDictUtil.java

  在StartUpListener中,初始化缓存:

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

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

  1. /* 数据字典缓存 */
  2. var _cache = {
  3. version: new Date().getTime(),
  4. configmap: null
  5. };
  6. /* 读取缓存 */
  7. function loadCache() {
  8. if (_cache.configmap == null || (new Date().getTime() - _cache.version) > 1000 * 60 * 30) {
  9. var localConfigMap = localStorage.getItem("configmap");
  10. if (localConfigMap) {
  11. _cache.configmap = JSON.parse(localConfigMap);
  12. } else {
  13. /* 读取数据字典缓存 */
  14. $.ajax({
  15. url: '/manage/data_dict/configMap',
  16. type: 'post',
  17. success: function (data) {
  18. _cache.configmap = data;
  19. localStorage.setItem("configmap", JSON.stringify(_cache.configmap));
  20. },
  21. error: function () {
  22. alert('ajax error');
  23. }
  24. });
  25. }
  26. }
  27. }
  28. /* 数据字典Key */
  29. var DataType = {
  30. "Occupy": "10100", // 领土归属
  31. "Faction": "10110", // 阵营
  32. "Race": "10200", // 种族
  33. "Job": "10250", // 职业
  34. "MobType": "10300", // 怪物类型
  35. "MobClass": "10310" // 怪物种类
  36. };
  37. DataDict.prototype = {
  38. occupy: function (value) {
  39. return _cache.configmap[DataType.Occupy][value];
  40. },
  41. job: function (value) {
  42. return _cache.configmap[DataType.Job][value];
  43. },
  44. faction: function (value) {
  45. return _cache.configmap[DataType.Faction][value];
  46. },
  47. mobClass: function (value) {
  48. return _cache.configmap[DataType.MobClass][value];
  49. },
  50. mobType: function (value) {
  51. return _cache.configmap[DataType.MobType][value];
  52. }
  53. };
  54. loadCache();
Helper.js

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

小结

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

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

 

原文链接:http://www.cnblogs.com/lyosaki88/p/idlewow_12.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号