从零开始实现放置游戏(十二)——实现战斗挂机(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-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

时间: 2024-10-07 23:48:28

从零开始实现放置游戏(十二)——实现战斗挂机(3)数据字典和缓存改造的相关文章

从零开始实现放置游戏(六)——实现挂机战斗(4)导入Excel数值配置

前面我们已经实现了在后台管理系统中,对配置数据的增删查改.但每次添加只能添加一条数据,实际生产中,大量数据通过手工一条一条添加不太现实.本章我们就实现通过Excel导入配置数据的功能.这里我们还是以地图数据为例,其他配置项可参照此例. 涉及的功能点主要有对office文档的编程.文件上传功能.流程图大致如下: 一.添加依赖项 解析office文档推荐使用免费的开源组件POI,已经可以满足80%的功能需求.上传文件需要依赖commons-fileupload包.我们在pom中添加下列代码: <!-

从零开始实现放置游戏(八)——实现挂机战斗(6)代码重构

前几张,我们主要实现了升级经验.人物等级属性.地图.地图怪物,这四种配置的增删查改以及Excel导入功能.我们主要以地图怪物为例,因此在文章末尾提供的源代码中只实现了地图怪物这部分的逻辑功能. 如果你照猫画虎,把4种配置功能的逻辑全部实现的话,就会发现,增删查改的代码基本相同,除了SQL语句和模型对象不同,其他地方变化不大. 本章我们利用泛型模板,对整个系统就行重构.在重构结束后,你就会发现写代码简直就是TMD艺术! 后端重构 idlewow-core 我们从最底层开始,首先重构位于core模块

从零开始学习html(十二)CSS布局模型——下

五.什么是层模型? 什么是层布局模型?层布局模型就像是图像软件PhotoShop中非常流行的图层编辑功能一样, 每个图层能够精确定位操作,但在网页设计领域,由于网页大小的活动性,层布局没能受到热捧. 但是在网页上局部使用层布局还是有其方便之处的.下面我们来学习一下html中的层布局. 如何让html元素在网页中精确定位,就像图像软件PhotoShop中的图层一样可以对每个图层能够精确定位操作. CSS定义了一组定位(positioning)属性来支持层布局模型. 层模型有三种形式: 1.绝对定位

从零开始学安全(四十二)●利用Wireshark分析ARP协议数据包

wireshark:是一个网络封包分析软件.网络封包分析软件的功能是撷取网络封包,并尽可能显示出最为详细的网络封包资料.Wireshark使用WinPCAP作为接口,直接与网卡进行数据报文交换,是目前全世界最广泛的网络封包分析软件 什么是ARP协议    协议分析篇第一个要研究的就是ARP协议.ARP(Address Resolution Protocol,地址解析协议)用于将IP地址解析为物理地址(MAC地址).这里之所以需要使用MAC地址,是因为网络中用于连接各个设备的交换机使用了内容可寻址

从零开始实现放置游戏(九)——实现后台管理系统(7)地图选择控件

前面做了地图怪物的添加,删除,查询等功能.但添加怪物的时候,需要选择怪物所在地图.前几张的源代码中,我忘了把这部分改回去,所以如果想要成功添加,需要自己改一下html界面,手动填写怪物所在地图的ID.然而,我们配置的时候,地图ID并不是固定的,而是数据库自增的.所以这里最好做成一个弹窗,点击后弹出一个地图列表,让我们手动选择怪物所在地图. 本章我们就实现这样一个弹窗控件,实现对地图的选择.后面如果有选择怪物,选择装备等需求,都可照猫画虎.整个过程的流程大致如下: 实现步骤 首先,我们给弹出的地图

JavaWeb 后端 &lt;十二&gt; 之 过滤器 filter 乱码、不缓存、脏话、标记、自动登录、全站压缩过滤器

一.过滤器是什么?有什么? 1.过滤器属于Servlet规范,从2.3版本就开始有了. 2.过滤器就是对访问的内容进行筛选(拦截).利用过滤器对请求和响应进行过滤 二.编写步骤和执行过程 1.编码步骤: a.编写一个类:实现javax.servlet.Filter接口 public class FilterDemo1 implements Filter { public FilterDemo1(){ System.out.println("调用了默认的构造方法"); } //用户每次访

从零开始学习PYTHON3讲义(十二)画一颗心送给你

(内容需要,本讲使用了大量在线公式,如果因为转帖网站不支持公式无法显示的情况,欢迎访问原始博客.) <从零开始PYTHON3>第十二讲 上一节课我们主要讲解了数值计算和符号计算.数值计算的结果,很常用的目的之一就是用于绘制图像,从图像中寻找公式的更多内在规律. Python科学绘图 科学绘图是计算机图形学的一个重要分支.同其它绘图方式相比,更简单易用,能让使用者把工作的主要精力集注在公式和算法上而不是绘图本身.此外科学绘图的工具包普遍精度更高,数据.图的对应关系准确,从而保证基于图的研究工作顺

【从零开始学NGUI 】 (十二)UIGrid

[从零开始学NGUI ] (十二)UIGrid 在很多情况下,我们都会用到可以变化的列表,背包,公告,活动,等等,这个时候通常我们都会用到UIGrid 创建UIGrid UIGrid一般都会与UIScrollView组合使用 首先打开NGUI Prefab Toolbar NGUI-> Open ->  Prefab Toolbar 拖拽一个background 到Hierarchy面板中 创建一个Sprite 作为scrollview 的背景 创建一个Grid放在scrollview的下面

从零开始学ios开发(十二):Table Views(中)UITableViewCell定制

我们继续学习Table View的内容,这次主要是针对UITableViewCell,在前一篇的例子中我们已经使用过UITableViewCell,一个默认的UITableViewCell包含imageView.textLabel.detailTextLabel等属性,但是很多时候这些默认的属性并不能满足需要,其实更多的时候我们想自己制定UITableViewCell的内容,这篇学习的就是制定自己的UITableViewCell. UITableViewCell继承自UIView,因此它可以加载