上一章,我們添加了游戲的主界面和注冊登錄功能,由于距離上上篇間隔較長,可能有些內容想些的后來就忘了,同時,邏輯也不復雜,所以描述比較粗略,
現在隨著模塊的增加,整個架構也暴露出一些問題,本章我們將對整個系統進行大規模重構,
比如,之前為了快速開發,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 =https://www.cnblogs.com/lyosaki88/p/ 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 = "https://www.cnblogs.com/lyosaki88/p/mapMob", key = "#id", unless = "#result == null")
public CommonResult find(String id) {
return super.find(id);
}
例二:下面的注解,代表此方法執行成功后,將快取 dataDict: 中的鍵全部清除
@CacheEvict(value = "https://www.cnblogs.com/lyosaki88/p/dataDict", allEntries = true)
public CommonResult update(DataDict dataDict) {
return super.update(dataDict);
}
例三:下面的注解,代表方法執行成功后,將key為 levelExp:#{id} 的快取更新為方法回傳的結果
@CachePut(value = "https://www.cnblogs.com/lyosaki88/p/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 = "https://www.cnblogs.com/lyosaki88/p/page", defaultValue = "https://www.cnblogs.com/lyosaki88/p/1") int pageIndex, @RequestParam(value = "https://www.cnblogs.com/lyosaki88/p/limit", defaultValue = "https://www.cnblogs.com/lyosaki88/p/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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/10679.html
標籤:其他
