上一章, 我们添加了游戏的主界面和注册登录功能. 由于距离上上篇间隔较长, 可能有些内容想些的后来就忘了. 同时, 逻辑也不复杂, 所以描述比较粗略.
现在随着模块的增加, 整个架构也暴露出一些问题. 本章我们将对整个系统进行大规模重构.
比如, 之前为了快速开发, 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-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>
pom.xml
这里, 我们需要配置一个叫 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;
- }
- }
- 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 系统需要做对应的改造. 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());
- }
- }
- }
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
来源: https://www.cnblogs.com/lyosaki88/p/idlewow_12.html