zoukankan      html  css  js  c++  java
  • SLG手游Java服务器的设计与开发——数据管理

    • 文章版权归腾讯GAD所有,禁止匿名转载;禁止商业使用;禁止个人使用。

    一、前言

    上文介绍了我们的SLG手游的服务器架构设计以及网络通信部分,本文介绍数据管理部分,在数据存储方面,我选择了Mysql、Memcache和Redis,利用这三种数据库各自的优势,各自发挥所长,在项目中有着不同的应用。

    二、游戏数据分析

    前文已经对游戏数据做了大概的分析,如下图所示:
    游戏数据
    这是我个人对游戏中的数据进行的一种划分。其中,游戏数据直接使用代码将表文件中的数据加载到内存,然后从内存中读取数据即可。玩家数据则分为了热数据和冷数据两部分分别进行存储,最大程度利用Mysql和Redis各自的优势。

    三、游戏静态数据

    在我们的这款游戏中,我们的静态数据配置在CSV表文件中,在服务器启动时,我读取所有的表文件,然后取出数据放到Map中,一般以一个标志性列(如ID)为key,一个JavaBean作为value放入到Map中。
    读取CSV表文件步骤如下:
    1.读取CSV文件
    2.按一定格式对文件内容进行解析
    3.将内容封装到JavaBean
    4.存放数据到Map中
    读取完了之后,在代码中如果需要,只需通过key在Map中来读取即可。
    在项目中,我将整个过程进行了封装,封装成了dataConfig.xml文件、*.csv文件、JavaBean、CsvLoader.java、CsvParser.java和TempletService.java,添加一个CSV文件的步骤如下:
    1.在dataConfig.xml中添加csv文件路径
    2.创建一个和CSV文件中结构一模一样的JavaBean
    3.服务器启动时调用CsvLoader的load()方法来加载所有的CSV文件
    4.调用TempletService.listAll()方法,并传入Javabean的simpleName来加载CSV文件内容到List中
    5.将List中的内容按一定结构存储(我一般都存为Map结构)

    1.dataConfig.xml

    dataConfig.xml中存储所有CSV表的路径,在CsvLoader.java中直接对这个xml表中的路径下的CSV文件进行读取加载。

    <?xml version="1.0" encoding="UTF-8"?>
    <config>
        <file name="Card.csv" />
        <file name="Equip.csv" />
        <!--省略更多CSV表-->
    </config>
    

    2.CSV文件

    CSV文件中存储具体的游戏数据,这个数据表一般是由数值策划来进行配置。CSV表的本质就是按逗号进行分割的数据,如下是卡牌表的前两行数据。

    卡牌ID,卡牌名称,英雄动画名称,卡牌兵种name,卡牌兵种类型,卡牌兵种美术资源ID,品质编号,所属势力,英雄等级,英雄升星等级,技能id,统,勇,智,初始兵力,初始攻击力,兵力,攻击力,克制系数,被克制系数,移动速度,爆击率,爆击倍数,普攻伤害加深,普攻伤害减免,技能伤害加深,技能伤害减免,普攻伤害点数,普攻免伤点数,技能伤害点数,技能减伤点数,KPI,技能名称,技能描述:目标 目标个数(范围)效果数值buff描述,英雄定位
    CardId,CardName,CardFlashName,CardSoldName,CardSoldID,CardSoldFlashID,RMBID,CountryID,CardLv,CardStar,SkillID,Tong,Yong,Zhi,HpStar,AttackStar,Hp,Attack,Kezhi,Beikezhi,Speed,Crit,Double,AttackAddbaifen,Attackreducebaifen,SkillAddbaifen,Skillreducebaifen,AttackNum,AttackreduceNum,SkillAddNum,SkillreduceNum,KPI,SkillName,SkillDes,HeroLocal
    200001,吕布,lvbu,骑兵,2,4,8,4,1,1,200001,105,110,90,344,70,2.22,2.26,0.17,0.43,385,0.16,1.44,0.176,0.144,0.176,0.144,0,0,0,0,1000,猛将无双,对敌方所有目标造成 1倍伤害并且全体晕眩3秒
    200002,赵云,zhaoyun,步兵,3,1,8,2,1,1,200002,103,106,100,381,62,2.464,2.008,0.31,0.29,300,0.16,1.44,0.128,0.208,0.128,0.208,0,0,0,0,1000,枪出如龙,对敌方目标造成3次0.5倍伤害并提升自身50%爆击率持续4秒
    

    3.JavaBean

    添加了CSV文件之后,我们需要创建一个和CSV表结构一模一样的JavaBean,如下是卡牌表对应的JavaBean。

    package com.hjc._36.template;
    
    public class Card {
        private int CardId;
        private String CardName;
        private String CardFlashName;
        private String CardSoldName;
        private int CardSoldID;
        private int CardSoldFlashID;
        private int RMBID;
        private int CountryID;
        private int CardLv;
        private int CardStar;
        private int SkillID;
        private int Tong;
        private int Yong;
        private int Zhi;
        private int HpStar;
        private int AttackStar;
        private float Hp;
        private float Attack;
        private float Kezhi;
        private float Beikezhi;
        private int Speed;
        private float Crit;
        private float Double;
        private float AttackAddbaifen;
        private float Attackreducebaifen;
        private float SkillAddbaifen;
        private float Skillreducebaifen;
        private float AttackreduceNum;
        private float AttackNum;
        private float SkillAddNum;
        private float SkillreduceNum;
        private int KPI;
    
       // getter/setter
    }
    

    4.CsvDataLoader.java

    CsvDataLoader封装了对CSV数据的载入,包括使用SAXReader对dataConfig.xml文件的读取,以及对其中的CSV文件的内容的读取,代码如下:

    package com.hjc._36.util.csv;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.lang.reflect.Field;
    import java.lang.reflect.Modifier;
    import java.sql.Timestamp;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.LinkedList;
    import java.util.List;
    import java.util.Map;
    
    import org.dom4j.Document;
    import org.dom4j.DocumentException;
    import org.dom4j.Element;
    import org.dom4j.io.SAXReader;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import com.hjc._36.core.GameInit;
    
    public class CsvDataLoader {
        public static Logger logger = LoggerFactory.getLogger(CsvDataLoader.class);
        private String packageName;
        private String config; // 每一行就是一个配置文件名字
        public static int ActivityMaxValue;
        private static CsvDataLoader inst;
    
        public CsvDataLoader(String packageName, String config) {
            this.packageName = packageName;
            this.config = config;
        }
    
        public static CsvDataLoader getInstance(String packageName, String config) {
            if (inst == null) {
                inst = new CsvDataLoader(packageName, config);
            }
            return inst;
        }
    
        /**
         * 调用load方法加载所有的配置文件
         */
        public void load() {
            SAXReader reader = new SAXReader();
            InputStream resourceStream = this.getClass().getResourceAsStream(
                    this.config);
            InputStreamReader resourceStreamReader = new InputStreamReader(
                    resourceStream);
            try {
                Document doc = reader.read(resourceStreamReader);
                List<?> nodes = doc.selectNodes("/config/file");
                Map<String, List<?>> dataMap = new HashMap<String, List<?>>();
                List<String> files = new LinkedList<String>();
                for (Object n : nodes) {
                    Element t = (Element) n;
                    String f = t.attributeValue("name");
                    List<?> dataList = this.loadFile(f, true);
                    for (Object o : dataList) {
                        TempletService.getInstance().registerObject(o, dataMap);
                    }
                    files.add(f);
                }
                logger.info("读取配置完毕,准备afterLoad");
                TempletService.templetMap = dataMap;
                TempletService.getInstance().afterLoad();
                logger.info("afterLoad 完毕");
            } catch (DocumentException e) {
                e.printStackTrace();
            } finally {
                if (resourceStreamReader != null) {
                    try {
                        resourceStreamReader.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (resourceStream != null) {
                    try {
                        resourceStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        private List<?> loadFile(String file, boolean exitWhenFail) {// 读文件
            InputStream resourceAsStream = null;
            try {
                String clzName = file.replaceAll(".csv", "");
                file = GameInit.confFileBasePath + file;
                logger.info("load file: {}", file);
                // resourceAsStream = this.getClass().getResourceAsStream(file);
                resourceAsStream = this.getClass().getClassLoader()
                        .getResource(file).openStream();
                if (resourceAsStream == null) {
                    logger.error("文件不存在:" + file);
                    if (exitWhenFail) {
                        System.exit(0);
                    }
                    return null;
                }
                return loadFromStream(resourceAsStream, clzName);
            } catch (Exception e) {
                logger.error("载入文件出错:" + file);
                e.printStackTrace();
                System.exit(0);
            } finally {
                if (resourceAsStream != null) {
                    try {
                        resourceAsStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return Collections.EMPTY_LIST;
        }
    
        public List<?> loadFromStream(InputStream resourceAsStream, String clzName)
                throws DocumentException, InstantiationException,
                IllegalAccessException, IOException {// 读csv文件
            CsvParser csvParser = new CsvParser(resourceAsStream);
            List<String> nodes = csvParser.getListWithNoHeader();
            // get clazz
            String className = this.packageName + clzName;
            try {
                Class<?> classObject = Class.forName(className);
                if (classObject == null) {
                    logger.error("未找到类" + className);
                    return null;
                }
                // Get all the declared fields
                Field[] fields = classObject.getDeclaredFields();
                LinkedList<Field> fieldList = new LinkedList<Field>();
                int length = fields.length;
                for (int i = -1; ++i < length;) {
                    boolean isStaticField = Modifier.isStatic(fields[i]
                            .getModifiers());
                    if (isStaticField)
                        continue;
                    boolean isTransientField = Modifier.isTransient(fields[i]
                            .getModifiers());
                    if (isTransientField)
                        continue;
                    fieldList.add(fields[i]);
                }
                // Get all the declared fields of supper class
                Class<?> tmp = classObject;
                while ((tmp = tmp.getSuperclass()) != Object.class) {
                    System.out.print("the extends class is" + tmp.getName());
                    fields = tmp.getDeclaredFields();
                    length = fields.length;
                    if (length == 0)
                        continue;
                    for (int i = -1; ++i < length;) {
                        boolean isStaticField = Modifier.isStatic(fields[i]
                                .getModifiers());
                        if (isStaticField)
                            continue;
                        boolean isTransientField = Modifier.isTransient(fields[i]
                                .getModifiers());
                        if (isTransientField)
                            continue;
                        fieldList.add(fields[i]);
                    }
                }
                // The truly need to return object
                List<Object> instances = new ArrayList<Object>(nodes.size());
                Object instance = null;
                String fieldName = null;
                String fieldValue = null;
                for (String node : nodes) {
                    if (node != null) {
                        instance = classObject.newInstance();
                        boolean ok = false;
                        // Element row = (Element) node;
                        String[] values = node.split(",");// csv文件以英文逗号分割值
                        for (int i = 0; i < fieldList.size(); i++) {
                            Field field = fieldList.get(i);
                            fieldName = field.getName();
                            fieldValue = values[i];
                            if (fieldValue == null)
                                continue;
                            try {
                                this.setField(instance, field, fieldValue);
                                ok = true;
                            } catch (Exception e) {
                                logger.error("类名称是" + className + "的属性" + fieldName
                                        + "没有被成功赋予静态数据");
                                continue;
                            }
                        }
                        if (ok) {
                            instances.add(instance);
                        }
                    }
                }
                return instances;
            } catch (ClassNotFoundException e1) {
                e1.printStackTrace();
                logger.error("未找到类" + className);
                return null;
            }
        }
    
        /**
         * 
         * @Title: setUnknowField
         * @Description:
         * @param ob
         * @param f
         * @param v
         * @throws IllegalArgumentException
         * @throws IllegalAccessException
         */
        private void setField(Object obj, Field f, String v)
                throws IllegalArgumentException, IllegalAccessException {
            f.setAccessible(true);
            if (f.getType() == int.class) {
                f.setInt(obj, Integer.parseInt(v));
            } else if (f.getType() == short.class) {
                f.setShort(obj, Short.parseShort(v));
            } else if (f.getType() == byte.class) {
                f.setByte(obj, Byte.parseByte(v));
            } else if (f.getType() == long.class) {
                f.setLong(obj, Long.parseLong(v));
            } else if (f.getType() == double.class) {
                f.setDouble(obj, Double.parseDouble(v));
            } else if (f.getType() == float.class) {
                f.setFloat(obj, Float.parseFloat(v));
            } else if (f.getType() == Timestamp.class) {
                f.set(obj, Timestamp.valueOf(v));
            } else {
                f.set(obj, f.getType().cast(v));
            }
        }
    
        /**
         * Test Code
         * 
         * @param args
         */
        public static void main(String[] args) {
            CsvDataLoader dl = new CsvDataLoader("com.hjc._36.template.",
                    "/dataConfig.xml");
            dl.load();
        }
    }
    

    5.CsvParser

    CsvDataLoader中用到的CsvParser是具体对CSV文件按逗号分割的格式的解析的类,代码如下:

    package com.hjc._36.util.csv;
    
    import java.io.BufferedReader;
    import java.io.BufferedWriter;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStreamWriter;
    import java.text.SimpleDateFormat;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.Iterator;
    import java.util.List;
    
    //JAVA 操作 excel 中的 .csv文件格式
    public class CsvParser {
        private BufferedReader bufferedreader = null;
        private List list = new ArrayList();
    
        public CsvParser() {
        }
    
        public CsvParser(InputStream inStream) throws IOException {
            InputStreamReader isr = new InputStreamReader(inStream, "UTF-8");
            bufferedreader = new BufferedReader(isr);
            String stemp;
            while ((stemp = bufferedreader.readLine()) != null) {
                list.add(stemp);
            }
        }
    
        public List getList() throws IOException {
            return list;
        }
        
        public List getListWithNoHeader() throws IOException {
            return list.subList(2, list.size());
        }
    
        // 得到csv文件的行数
        public int getRowNum() {
            return list.size();
        }
    
        // 得到csv文件的列数
        public int getColNum() {
            if (!list.toString().equals("[]")) {
                if (list.get(0).toString().contains(",")) { // csv文件中,每列之间的是用','来分隔的
                    return list.get(0).toString().split(",").length;
                } else if (list.get(0).toString().trim().length() != 0) {
                    return 1;
                } else {
                    return 0;
                }
            } else {
                return 0;
            }
        }
    
        // 取得指定行的值
        public String getRow(int index) {
            if (this.list.size() != 0)
                return (String) list.get(index);
            else
                return null;
        }
    
        // 取得指定列的值
        public String getCol(int index) {
            if (this.getColNum() == 0) {
                return null;
            }
            StringBuffer scol = new StringBuffer();
            String temp = null;
            int colnum = this.getColNum();
            if (colnum > 1) {
                for (Iterator it = list.iterator(); it.hasNext();) {
                    temp = it.next().toString();
                    scol = scol.append(temp.split(",")[index] + ",");
                }
            } else {
                for (Iterator it = list.iterator(); it.hasNext();) {
                    temp = it.next().toString();
                    scol = scol.append(temp + ",");
                }
            }
            String str = new String(scol.toString());
            str = str.substring(0, str.length() - 1);
            return str;
        }
    
        // 取得指定行,指定列的值
        public String getString(int row, int col) {
            String temp = null;
            int colnum = this.getColNum();
            if (colnum > 1) {
                temp = list.get(row).toString().split(",")[col];
            } else if (colnum == 1) {
                temp = list.get(row).toString();
            } else {
                temp = null;
            }
            return temp;
        }
    
        public void CsvClose() throws IOException {
            this.bufferedreader.close();
        }
    
        public List readCvs(String filename) throws IOException {
            CsvParser cu = new CsvParser(new FileInputStream(new File(filename)));
            List list = cu.getList();
    
            return list;
        }
    
        public void createCsv(String biao, List list, String path)
                throws IOException {
            List tt = list;
            String data = "";
            SimpleDateFormat dataFormat = new SimpleDateFormat("yyyyMMdd");
            Date today = new Date();
            String dateToday = dataFormat.format(today);
            File file = new File(path + "resource/expert/" + dateToday
                    + "importerrorinfo.csv");
            if (!file.exists())
                file.createNewFile();
            else
                file.delete();
            String str[];
            StringBuilder sb = new StringBuilder("");
            sb.append(biao);
            FileOutputStream writerStream = new FileOutputStream(file, true);
            BufferedWriter output = new BufferedWriter(new OutputStreamWriter(
                    writerStream, "UTF-8"));
            for (Iterator itt = tt.iterator(); itt.hasNext();) {
                String fileStr = itt.next().toString();
                // str = fileStr.split(",");
                // for (int i = 0; i <= str.length - 1; i++) { // 拆分成数组 用于插入数据库中
                // System.out.print("str[" + i + "]=" + str[i] + " ");
                // }
                // System.out.println("");
                sb.append(fileStr + "
    ");
            }
            output.write(sb.toString());
            output.flush();
            output.close();
        }
    }
    

    6.TempletService.java

    服务器启动时,调用CsvDataLoader的load()方法,以完成对CSV文件的加载,之后就需要使用TempletService的listAll方法来讲数据加载到List中,TempletService根据JavaBean的simpleName来对数据进行加载,代码如下:

    package com.hjc._36.util.csv;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    /**
     * 添加一个数据表需要做以下几步 
     * 1.在包com.hjc._36.template下创建对应的模板类,类名与数据文件一致 
     * 2.在src/main/resources/csv/中添加模板数据文件
     * 3.在src/main/resources/dataConfig.xml加入刚才的模板数据文件
     * 
     * @author 何金成
     * 
     */
    public class TempletService {
        public static Logger log = LoggerFactory.getLogger(TempletService.class);
        public static TempletService templetService = new TempletService();
        /**
         * key:实体名 value:该实体下的所有模板数据
         */
        public static Map<String, List<?>> templetMap = new HashMap<String, List<?>>();
    
        public TempletService() {
    
        }
    
        public static TempletService getInstance() {
            return templetService;
        }
    
        /**
         * 获取该实体类下所有模板数据
         * 
         * @param beanName
         * @return
         */
        @SuppressWarnings("unchecked")
        public static List listAll(String beanName) {
            return templetMap.get(beanName);
        }
    
        /**
         * @Title: registerObject 
         * @Description: 注册对象到对应类的List中
         * @param o
         * @param dataMap
         * @return void
         * @throws
         */
        public void registerObject(Object o, Map<String, List<?>> dataMap) {
            add(o.getClass().getSimpleName(), o, dataMap);
        }
    
    
    
        @SuppressWarnings("unchecked")
        private void add(String key, Object data, Map<String, List<?>> dataMap) {
            List list = dataMap.get(key);
            if (list == null) {
                list = new ArrayList();
                dataMap.put(key, list);
            }
            list.add(data);
        }
        
        public void afterLoad() {
            // 加载后处理
            // List tests = TempletService.listAll(Test.class.getSimpleName());
            // for (Object object : tests) {
            // Test test = (Test)object;
            // System.out.print(test.getEquipLv());
            // System.out.print(","+test.getLv1());
            // System.out.print(","+test.getLv2());
            // System.out.print(","+test.getLv3());
            // System.out.print(","+test.getLv4());
            // System.out.print(","+test.getLv5());
            // System.out.print(","+test.getLv6());
            // System.out.print(","+test.getLv7());
            // System.out.print(","+test.getLv8());
            // System.out.print(","+test.getLv9());
            // System.out.println(","+test.getLv10());
            // }
        }
    
        public void loadCanShu() {
            // 加载全局参数xml配置
        }
    }
    

    7.使用静态数据

    在完成了添加和加载等一系列操作之后,就可以在代码中调用CSV表中加载进来的数据了,例如上文提到的卡牌数据表,加载代码如下:

    // 卡牌数据表
    List<Card> cardList = TempletService
                    .listAll(Card.class.getSimpleName());
    Map<Integer, Card> cardMap = new HashMap<Integer, Card>();
    for (Card card : cardList) {
        cardMap.put(card.getCardId(), card);
    }
    this.cardMap = cardMap;
    

    使用时只需要根据卡牌的Id,就可以取到这张卡牌的所有数据。

    四、Mysql存储数据

    我们使用Mysql作为冷数据的存储数据库,并使用Druid和Hibernate来创建数据库的连接以及增删改查的操作。在游戏数据中,我对游戏中的冷数据做了一个总结,如下图所示:
    Mysql数据
    完成要存储的游戏数据的分析之后,我们就可以进行具体建模建表的工作,完成对数据的设计。由于在游戏服务器的数据存储中,数据库基本上只是一个游戏数据临时存放的地方,所以游戏数据中的关联性并不是特别强,所以不需要严密的数据库设计,只需简单的将玩家所有的数据按照一个userid进行关联即可,在使用Hibernate的时候,我们使用了Hibernate4,使用了它注解JavaBean自动建表的功能,我们只需将需要存储的Model写成JavaBean,并写上注解,在启动时,Hibernate扫描到JavaBean会自动为我们创建或更新表。

    1.Druid数据库连接池

    游戏服务器运行中经常是多个玩家同时在线的,可想而知,如果同时进行某一项涉及数据库的操作时,也会并发请求数据库,多个数据库请求就需要我们对多个数据库连接进行有效的管理,当然,我们可以自己写一个数据库卡连接池来进行数据库管理,但好在以后前辈为我们做足了工作,有很多成型的开源数据库连接池可供我们选择,常见的有c3p0、dbcp、proxool和driud等,这里我们使用阿里巴巴公司的开源产品Druid,这是我个人认为最好用的数据库连接池,它不仅提供了数据库连接池应有的功能,更是提供了良好的数据库监控性能,这是我们作为开发人员在遇到性能瓶颈时最需要的东西,感兴趣的朋友可以参考下官方github,根据官方wiki配置一个Druid的数据监控系统,通过系统可以查看数据库的各种性能指标。
    Druid在github中的地址是:https://github.com/alibaba/druid
    在项目中使用druid,首先我们需要导入druid所需jar包以及Mysql的驱动jar包,由于我们是maven项目,我们就直接添加pom依赖,代码如下:

    <!--Mysql驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.22</version>
        <scope>runtime</scope>
    </dependency>
    <!--数据库连接池-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>0.2.26</version>
    </dependency>
    

    在spring的xml中对druid进行配置

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
            init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.user}" />
        <property name="password" value="${jdbc.password}" />
        <property name="maxActive" value="${jdbc.maxActive}" />
        <property name="initialSize" value="${jdbc.initialSize}" />
        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="${jdbc.maxWait}" />
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}" />
        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}" />
        <!--过滤 -->
        <property name="filters" value="config,wall,mergeStat" />
        <!--密码加密 -->
        <!-- property name="connectionProperties" value="config.decrypt=true" / -->
        <!--合并多个数据源 -->
        <property name="useGloalDataSourceStat" value="true" />
        <property name="proxyFilters">
            <list>
                <ref bean="log-filter" />
                <ref bean="stat-filter" />
            </list>
        </property>
    </bean>
    <bean id="log-filter" class="com.alibaba.druid.filter.logging.Log4jFilter">
        <property name="statementLogErrorEnabled" value="true" />
        <property name="statementLogEnabled" value="true" />
    </bean>
    <bean id="stat-filter" class="com.alibaba.druid.filter.stat.StatFilter">
        <property name="slowSqlMillis" value="1000" />
        <property name="logSlowSql" value="true" />
    </bean>
    

    然后需要在web.xml中再对druid的filter进行配置:

    <filter>
        <filter-name>DruidWebStatFilter</filter-name>
        <filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class>
    <init-param>
        <param-name>exclusions</param-name>
        <param-value>weburi.json,.html,.js,.gif,.jpg,.png,.css,.ico,/fonts/*,/datas/*,images/*</param-value>
    </init-param>
    <init-param>
        <param-name>sessionStatMaxCount</param-name>
        <param-value>1000</param-value>
    </init-param>
    <init-param>
        <param-name>principalSessionName</param-name>
        <param-value>FRONT_USER</param-value>
    </init-param>
    </filter>
    <filter-mapping>
        <filter-name>DruidWebStatFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <servlet>
        <servlet-name>DruidStatView</servlet-name>
        <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>DruidStatView</servlet-name>
        <url-pattern>/druid/*</url-pattern>
    </servlet-mapping>
    

    至此为止,Druid的配置就算是完成,启动工程之后我们还能通过/druid路径来访问Druid提供的监控系统,更多关于Druid的使用可以参照github中的wiki介绍,了解更多Druid配置及参数设置。

    2.Hibernate

    使用Hibernate作为Mysql数据库的ORM框架,主要是因为其良好的封装,首先我个人认为Hibernate的性能是不足与和原生JDBC以及MyBatis这样的框架所匹敌的,封装的更好却带来了更多的性能损失,但我使用他也是看中他良好的封装性,因为我对性能的需求还没有达到很高的级别;其次,Hibernate很难写出复杂的SQL查询,而MyBatis却可以写出一些复杂的SQL,但在我的设计中,我不需要太复杂的查询,基本上我所有的SQL语句的where条件都是"where userid=?",因此在性能需求上以及易用的对比上,我选择了Hibernate。
    我使用的版本你是Hibernate4,因为Hibernate4提供了注解自动创建表的功能,Hibernate集成在spring的配置的xml代码如下:

    <!-- 定义事务管理 -->
    <bean id="transactionManager"
        class="org.springframework.orm.hibernate4.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory" />
    </bean>
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!-- 事务执行方式 REQUIRED:指定当前方法必需在事务环境中运行, 如果当前有事务环境就加入当前正在执行的事务环境, 如果当前没有事务,就新建一个事务。 
                这是默认值。 -->
            <tx:method name="create*" propagation="REQUIRED" />
            <tx:method name="save*" propagation="REQUIRED" />
            <tx:method name="add*" propagation="REQUIRED" />
            <tx:method name="update*" propagation="REQUIRED" />
            <tx:method name="remove*" propagation="REQUIRED" />
            <tx:method name="del*" propagation="REQUIRED" />
            <tx:method name="import*" propagation="REQUIRED" />
            <!-- 指定当前方法以非事务方式执行操作,如果当前存在事务,就把当前事务挂起,等我以非事务的状态运行完,再继续原来的事务。 查询定义即可 
                read-only="true" 表示只读 -->
            <tx:method name="*" propagation="NOT_SUPPORTED" read-only="true" />
        </tx:attributes>
    </tx:advice>
    <!-- hibernate SessionFactory -->
    <bean id="sessionFactory"
        class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <!-- 数据源 -->
        <property name="dataSource" ref="dataSource" />
        <!-- hibernate的相关属性配置 -->
        <property name="hibernateProperties">
            <value>
                <!-- 设置数据库方言 -->
                hibernate.dialect=org.hibernate.dialect.MySQLDialect
                <!-- 设置自动创建|更新|验证数据库表结构 -->
                hibernate.hbm2ddl.auto=update
                <!-- 是否在控制台显示sql -->
                hibernate.show_sql=false
                <!-- 是否格式化sql,优化显示 -->
                hibernate.format_sql=false
                <!-- 是否开启二级缓存 -->
                hibernate.cache.use_second_level_cache=false
                <!-- 是否开启查询缓存 -->
                hibernate.cache.use_query_cache=false
                <!-- 数据库批量查询最大数 -->
                hibernate.jdbc.fetch_size=50
                <!-- 数据库批量更新、添加、删除操作最大数 -->
                hibernate.jdbc.batch_size=50
                <!-- 是否自动提交事务 -->
                hibernate.connection.autocommit=true
                <!-- 指定hibernate在何时释放JDBC连接 -->
                hibernate.connection.release_mode=auto
                <!-- 创建session方式 hibernate4.x 的方式 -->
                hibernate.current_session_context_class=thread
                <!-- javax.persistence.validation.mode默认情况下是auto的,就是说如果不设置的话它是会自动去你的classpath下面找一个bean-validation**包 
                    所以把它设置为none即可 -->
                javax.persistence.validation.mode=none
            </value>
        </property>
        <!-- 自动扫描实体对象 tdxy.bean的包结构中存放实体类 -->
        <property name="packagesToScan" value="com.hjc._36" />
    </bean>
    

    配置完了之后,我们需要对我们需要进行存储的数据进行注解,如君主信息的Model如下:

    package com.hjc._36.manager.junzhu;
    
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.Id;
    import javax.persistence.Table;
    
    import com.hjc._36.util.cache.MCSupport;
    
    @Entity
    @Table(name = "JunZhu")
    public class JunZhu implements MCSupport {
        // id,用户id,用户名字,用户所属国家,用户头像(用数字表示),用户等级,用户vip等级,用户军令数,用户金宝,用户银宝,用户粮食,用户精铁,用户木材,
        // 用户兵数量,用户军工数,用户将魂数
        private static final long serialVersionUID = -5385044598250102957L;
        @Id
        public long id;// —用户id
        public String name;// — 用户名
        @Column(columnDefinition = "INT default 1")
        public int headImg;// —用户头像
        public int role;// —用户角色
        public int country;// —用户所属国家 1表示蜀国 2表示魏国 3表示吴国
        @Column(columnDefinition = "INT default 0")
        public int exp;// —用户等级 —君主等级
        @Column(columnDefinition = "INT default 1")
        public int level;// —用户等级 —君主等级
        @Column(columnDefinition = "INT default 0")
        public int vip;// —用户vip等级
        @Column(columnDefinition = "INT default 0")
        public int vipExp;// —用户vip经验
        @Column(columnDefinition = "INT default 0")
        public int junling;// —用户军令数
        @Column(columnDefinition = "INT default 0")
        public int coin;// —用户金币
        @Column(columnDefinition = "INT default 0")
        public int yuanbao;// —用户元宝
        @Column(columnDefinition = "INT default 0")
        public int food;// —用户粮食
        @Column(columnDefinition = "INT default 0")
        public int iron;// —用户精铁
        @Column(columnDefinition = "INT default 0")
        public int wood;// —用户木材
        @Column(columnDefinition = "INT default 0")
        public int soldierNum;// —用户兵力
        @Column(columnDefinition = "INT default 0")
        public int jungongNum;// —用户军功数
        @Column(columnDefinition = "INT default 0")
        public int jianghunNum;// —将魂数量
        @Column(columnDefinition = "INT default 0")
        public int shengwang;// —声望
        @Column(columnDefinition = "INT default 0")
        public int zxId;// —阵型id
        @Column(columnDefinition = "INT default 0")
        public int paySum;// -充值总额
        @Column(columnDefinition = "INT default 0")
        public int conduction;// -新手引导
    
        // getter/setter
    }
    

    以上代码中,@Entity和@Table(name = "JunZhu")就可以使Hibernate在启动时自动创建一个JunZhu表,@Id的属性即设为主键的字段。创建好Model之后,就可以使用Hibernate的session进行数据库操作,这里我将数据库的操作封装为一个工具类HibernateUtil,这个工具类大家可以拿去直接使用,具体代码如下:

    package com.hjc._36.util.hibernate;
    
    import java.lang.reflect.Field;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.Iterator;
    import java.util.List;
    import java.util.Map;
    
    import net.sf.json.JSONObject;
    
    import org.hibernate.Query;
    import org.hibernate.SQLQuery;
    import org.hibernate.Session;
    import org.hibernate.SessionFactory;
    import org.hibernate.Transaction;
    import org.hibernate.annotations.Where;
    import org.hibernate.transform.Transformers;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.FileSystemXmlApplicationContext;
    
    import com.alibaba.fastjson.JSON;
    import com.hjc._36.core.GameInit;
    import com.hjc._36.manager.card.CardInfo;
    import com.hjc._36.manager.equip.EquipInfo;
    import com.hjc._36.manager.mail.MailInfo;
    import com.hjc._36.manager.pve.PveInfo;
    import com.hjc._36.manager.zhenxing.ZhenXingInfo;
    import com.hjc._36.manager.zhenxing.ZhenxingMgr;
    import com.hjc._36.task.ExecutorPool;
    import com.hjc._36.util.cache.MC;
    import com.hjc._36.util.cache.MCSupport;
    import com.hjc._36.util.memcached.MemcachedCRUD;
    
    public class HibernateUtil {
        public static boolean showMCHitLog = false;
        public static Logger log = LoggerFactory.getLogger(HibernateUtil.class);
        public static Map<Class<?>, String> beanKeyMap = new HashMap<Class<?>, String>();
        public static long synDelayT = 0;// 延迟同步周期
        private static SessionFactory sessionFactory;
    
        public static void init() {
            sessionFactory = buildSessionFactory();
        }
    
        public static SessionFactory getSessionFactory() {
            return sessionFactory;
        }
    
        public static Throwable insert(Object o, long id) {
            Session session = sessionFactory.getCurrentSession();
            session.beginTransaction();
            try {
                session.save(o);
                session.getTransaction().commit();
                if (MC.cachedList.contains(o.getClass().getSimpleName())) {
                    synMC4Insert(o.getClass().getSimpleName(), o,
                            String.valueOf(id));
                }
            } catch (Throwable e) {
                log.error("0要insert的数据{}", o == null ? "null" : JSONObject
                        .fromObject(o).toString());
                log.error("0保存出错", e);
                session.getTransaction().rollback();
                return e;
            }
            return null;
        }
    
        /**
         * FIXME 不要这样返回异常,没人会关系返回的异常。
         * 
         * @param o
         * @return
         */
        public static Throwable save(Object o, long id) {
            Session session = sessionFactory.getCurrentSession();
            Transaction t = session.beginTransaction();
            boolean mcOk = false;
            try {
                if (o instanceof MCSupport) {
                    MCSupport s = (MCSupport) o;// 需要对控制了的对象在第一次存库时调用MC.add
                    MC.update(o, String.valueOf(s.getIdentifier()));// MC中控制了哪些类存缓存。
                    mcOk = true;
                    // session.update(o);
                    session.saveOrUpdate(o);
                } else {
                    session.saveOrUpdate(o);
                }
                t.commit();
                if (o instanceof MCSupport) {
                    if (MC.cachedList.contains(o.getClass().getSimpleName())) {
                        synMC4Save(o.getClass().getSimpleName(), o,
                                String.valueOf(id));
                    }
                }
            } catch (Throwable e) {
                log.error("1要save的数据{},{}", o, o == null ? "null" : JSONObject
                        .fromObject(o).toString());
                if (mcOk) {
                    log.error("MC保存成功后报错,可能是数据库条目丢失。");
                }
                log.error("1保存出错", e);
                t.rollback();
                return e;
            }
            return null;
        }
    
        public static Throwable update(Object o, String id) {
            Session session = sessionFactory.getCurrentSession();
            Transaction t = session.beginTransaction();
            try {
                if (o instanceof MCSupport) {
                    MCSupport s = (MCSupport) o;// 需要对控制了的对象在第一次存库时调用MC.add
                    MC.update(o, String.valueOf(s.getIdentifier()));// MC中控制了哪些类存缓存。
                    session.update(o);
                } else {
                    session.update(o);
                }
                t.commit();
                if (o instanceof MCSupport) {
                    if (MC.cachedList.contains(o.getClass().getSimpleName())) {
                        synMC4Update(o.getClass().getSimpleName(), o,
                                String.valueOf(id));
                    }
                }
            } catch (Throwable e) {
                log.error("1要update的数据{},{}", o, o == null ? "null" : JSONObject
                        .fromObject(o).toString());
                log.error("1保存出错", e);
                t.rollback();
                return e;
            }
            return null;
        }
    
        public static <T> T find(Class<T> t, long id) {
            String keyField = getKeyField(t);
            if (keyField == null) {
                throw new RuntimeException("类型" + t + "没有标注主键");
            }
            if (!MC.cachedClass.contains(t)) {
                return find(t, "where " + keyField + "=" + id, false);
            }
            T ret = MC.get(t, String.valueOf(id));
            if (ret == null) {
                if (showMCHitLog)
                    log.info("MC未命中{}#{}", t.getSimpleName(), id);
                ret = find(t, "where " + keyField + "=" + id, false);
                if (ret != null) {
                    if (showMCHitLog)
                        log.info("DB命中{}#{}", t.getSimpleName(), id);
                    MC.add(ret, String.valueOf(id));
                } else {
                    if (showMCHitLog)
                        log.info("DB未命中{}#{}", t.getSimpleName(), id);
                }
            } else {
                if (showMCHitLog)
                    log.info("MC命中{}#{}", t.getSimpleName(), id);
            }
            return ret;
        }
    
        public static <T> T find(Class<T> t, String where) {
            return find(t, where, true);
        }
    
        public static <T> T find(Class<T> t, String where, boolean checkMCControl) {
            // if (checkMCControl && MC.cachedClass.contains(t)) {
            // // 请使用static <T> T find(Class<T> t,long id)
            // throw new BaseException("由MC控制的类不能直接查询DB:" + t);
            // }
            Session session = sessionFactory.getCurrentSession();
            Transaction tr = session.beginTransaction();
            T ret = null;
            try {
                // FIXME 使用 session的get方法代替。
                String hql = "from " + t.getSimpleName() + " " + where;
                Query query = session.createQuery(hql);
                ret = (T) query.uniqueResult();
                tr.commit();
            } catch (Exception e) {
                tr.rollback();
                log.error("list fail for {} {}", t, where);
                log.error("list fail", e);
            }
            return ret;
        }
    
        public static <T> List<T> list(Class<T> t, long id, String where) {
            String keyField = getKeyField(t);
            if (keyField == null) {
                throw new RuntimeException("类型" + t + "没有标注主键");
            }
            if (!MC.cachedList.contains(t.getSimpleName())) {
                return list(t, where, false);
            }
            List<T> ret = MC.getList(t, String.valueOf(id), where);
            if (ret == null) {
                if (showMCHitLog)
                    log.info("MC未命中{}#{}", t.getSimpleName(), where);
                ret = list(t, where, false);
                if (ret != null) {
                    if (showMCHitLog)
                        log.info("DB命中{}#{}", t.getSimpleName(), where);
                    MC.addList(ret, t.getSimpleName(), String.valueOf(id), where);
                } else {
                    if (showMCHitLog)
                        log.info("DB未命中{}#{}", t.getSimpleName(), where);
                }
            } else {
                if (showMCHitLog)
                    log.info("MC命中{}#{}", t.getSimpleName(), where);
            }
            return ret;
        }
    
        /**
         * @param t
         * @param where
         *            例子: where uid>100
         * @return
         */
        public static <T> List<T> list(Class<T> t, String where,
                boolean checkMCControl) {
            // if (checkMCControl && MC.cachedList.contains(t)) {
            // // 请使用static <T> T find(Class<T> t,long id)
            // throw new BaseException("由MC控制的类不能直接查询DB:" + t);
            // }
            Session session = sessionFactory.getCurrentSession();
            Transaction tr = session.beginTransaction();
            List<T> list = Collections.EMPTY_LIST;
            try {
                String hql = "from " + t.getSimpleName() + " " + where;
                Query query = session.createQuery(hql);
                list = query.list();
                tr.commit();
            } catch (Exception e) {
                tr.rollback();
                log.error("list fail for {} {}", t, where);
                log.error("list fail", e);
            }
            return list;
        }
    
        public static SessionFactory buildSessionFactory() {
            log.info("开始构建hibernate");
            String path = "classpath*:spring-conf/applicationContext.xml";
            ApplicationContext ac = new FileSystemXmlApplicationContext(path);
            sessionFactory = (SessionFactory) ac.getBean("sessionFactory");
            log.info("结束构建hibernate");
            return sessionFactory;
        }
    
        public static Throwable delete(Object o, long id) {
            if (o == null) {
                return null;
            }
            Session session = sessionFactory.getCurrentSession();
            session.beginTransaction();
            try {
                session.delete(o);
                session.getTransaction().commit();
                if (o instanceof MCSupport) {
                    MCSupport s = (MCSupport) o;// 需要对控制了的对象在第一次存库时调用MC.add
                    MC.delete(o.getClass(), String.valueOf(s.getIdentifier()));// MC中控制了哪些类存缓存。
                    if (MC.cachedList.contains(o.getClass().getSimpleName())) {
                        synMC4Delete(o.getClass().getSimpleName(), o,
                                String.valueOf(id));
                    }
                }
            } catch (Throwable e) {
                log.error("要删除的数据{}", o);
                log.error("出错", e);
                session.getTransaction().rollback();
                return e;
            }
            return null;
        }
    
        /**
         * 注意这个方法会返回大于等于1的值。数据库无记录也会返回1,而不是null
         * 
         * @param t
         * @return
         */
        public static <T> Long getTableIDMax(Class<T> t) {
            Long id = null;
            Session session = sessionFactory.getCurrentSession();
            Transaction tr = session.beginTransaction();
            String hql = "select max(id) from " + t.getSimpleName();
            try {
                Query query = session.createQuery(hql);
                Object uniqueResult = query.uniqueResult();
                if (uniqueResult == null) {
                    id = 1L;
                } else {
                    id = Long.parseLong(uniqueResult + "");
                    id = Math.max(1L, id);
                }
                tr.commit();
            } catch (Exception e) {
                tr.rollback();
                log.error("query max id fail for {} {}", t, hql);
                log.error("query max id fail", e);
            }
            return id;
        }
    
        public static List<Map<String, Object>> querySql(String hql) {
            Session session = sessionFactory.getCurrentSession();
            Transaction tr = session.beginTransaction();
            List list = Collections.emptyList();
            try {
                SQLQuery query = session.createSQLQuery(hql);
                query.setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);
                list = query.list();
                tr.commit();
            } catch (Exception e) {
                tr.rollback();
                log.error("query  failed {}", hql);
                log.error("query count(type) fail", e);
            }
            return list;
        }
    }
    

    以上代码中,除了调用session的数据库操作API之外,我还使用了Memcache进行结果集的缓存,具体关系Memcache和Mysql的集合使用,在下文中在进行讲解。上文代码中首先在服务器启动时,需要构建SessionFactory,然后通过操作session开始事务,通过session调用CRUD方法进行操作,之后再调用commit方法提交并结束事务,中间如果发生异常则进行rollback操作回滚事务。

    五、Redis存储数据

    游戏中的热数据的存储我选用了Redis,Redis不仅是运行在内存上的内存数据库,并且它的数据存储结构也是很丰富的,包括String,Set,List,Sorted Set和Hash五种数据结构,我对游戏数据的热数据进行了分析,如下图:
    Redis数据
    使用Redis首先得了解Redis的五种基本数据类型,每一种数据类型都对应不同的Redis操作API,在Java中使用Redis可以使用官方提供的Jedis客户端,Jedis客户端中包含了各种数据类型的操作,我将所有的Redis操作都封装在了Redis类中,启动时调用init方法进行Redis连接,使用时通过getInstance获取实例,再调用相应的API即可完成相关的Redis操作,在init方法中,我是通过调用JedisSentinelPool去获取Redis的连接,因为我在服务器对Redis做了Sentinel的集群部署,大家可以直接拿这个Redis工具类去使用,Redis类的方法如下:

    package com.hjc._36.util.redis;
    
    import java.beans.BeanInfo;
    import java.beans.IntrospectionException;
    import java.beans.Introspector;
    import java.beans.PropertyDescriptor;
    import java.lang.reflect.InvocationTargetException;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.LinkedHashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    
    import org.apache.commons.beanutils.BeanUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import redis.clients.jedis.HostAndPort;
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisSentinelPool;
    import redis.clients.jedis.Tuple;
    
    import com.hjc._36.core.GameInit;
    
    public class Redis {
        private static Redis instance;
        public static Logger log = LoggerFactory.getLogger(Redis.class);
        public static final int GLOBAL_DB = 0;// 全局
        public static final int LOGIC_DB = GameInit.serverId;// 模块库
        public static String password = null;
    
        public static Redis getInstance() {
            if (instance == null) {
                instance = new Redis();
            }
            return instance;
        }
    
        // private JedisPool pool;
        private JedisSentinelPool sentinelPool;
        public String host;
        public int port;
    
        // public int getDB(long userid){
        //
        // }
    
        public Jedis getJedis() {
            return this.sentinelPool.getResource();
        }
    
        public void returnResource(Jedis jedis) {
            jedis.close();
            // this.sentinelPool.returnResource(jedis);
        }
    
        public void init() {
            String redisServer = null;
            if (GameInit.cfg != null) {
                redisServer = GameInit.cfg.get("redisServer");
                password = GameInit.cfg.get("redisPwd");
            }
            if (redisServer == null) {
                redisServer = "127.0.0.1:6440";
            }
            redisServer = redisServer.trim();
            String[] tmp = redisServer.split(":");
            host = tmp[0];
            port = Integer.parseInt(tmp[1]);
            if (tmp.length == 2) {
                port = Integer.parseInt(tmp[1].trim());
            }
            log.info("Redis sentinel at {}:{}", host, port);
            // sentinelPool = new JedisPool(host, port);
            Set sentinels = new HashSet();
            sentinels.add(new HostAndPort(host, port).toString());
            sentinelPool = new JedisSentinelPool("master1", sentinels);
        }
    
        // private void init() {
        // String redisServer = null;
        // if (GameInit.cfg != null) {
        // redisServer = GameInit.cfg.get("redisServer");
        // password = GameInit.cfg.get("redisPwd");
        // }
        // if (redisServer == null) {
        // redisServer = "127.0.0.1:6379";
        // }
        // redisServer = redisServer.trim();
        // String[] tmp = redisServer.split(":");
        // host = tmp[0];
        // port = Integer.parseInt(tmp[1]);
        // if (tmp.length == 2) {
        // port = Integer.parseInt(tmp[1].trim());
        // }
        // log.info("Redis at {}:{}", host, port);
        // // sentinelPool = new JedisPool(host, port);
        // JedisPoolConfig config = new JedisPoolConfig();
        // sentinelPool = new JedisPool(config, host, port, 100000, password);
        // }
    
        public void test() {
            Jedis j = getJedis();
            j.auth(password);
            returnResource(j);
        }
    
        public void select(int index) {
            Jedis j = getJedis();
            j.auth(password);
            j.select(index);
            returnResource(j);
        }
    
        public boolean hexist(int db, String key, String field) {
            if (key == null) {
                return false;
            }
            Jedis redis = getJedis();
            redis.auth(password);
            redis.select(db);
            boolean ret = redis.hexists(key, field);
            returnResource(redis);
            return ret;
        }
    
        public Long hdel(int db, String key, String... fields) {
            Jedis redis = getJedis();
            redis.auth(password);
            redis.select(db);
            Long cnt = redis.hdel(key, fields);
            returnResource(redis);
            return cnt;
        }
    
        public String hget(int db, String key, String field) {
            if (key == null) {
                return null;
            }
            Jedis redis = getJedis();
            redis.auth(password);
            redis.select(db);
            String ret = redis.hget(key, field);
            returnResource(redis);
            return ret;
        }
    
        public Map<String, String> hgetAll(int db, String key) {
            if (key == null) {
                return null;
            }
            Jedis redis = getJedis();
            redis.auth(password);
            redis.select(db);
            Map<String, String> ret = redis.hgetAll(key);
            returnResource(redis);
            return ret;
        }
    
        public void hset(int db, String key, String field, String value) {
            if (field == null || field.length() == 0) {
                return;
            }
            if (value == null || value.length() == 0) {
                return;
            }
            Jedis redis = getJedis();
            redis.auth(password);
            redis.select(db);
            redis.hset(key, field, value);
            returnResource(redis);
        }
    
        public void add(int db, String group, String key, String value) {
            if (value == null || key == null) {
                return;
            }
            Jedis redis = getJedis();
            redis.auth(password);
            redis.select(db);
            redis.hset(group, key, value);
            returnResource(redis);
        }
    
        public void set(int db, String key, String value) {
            if (value == null || key == null) {
                return;
            }
            Jedis redis = getJedis();
            redis.auth(password);
            redis.select(db);
            redis.set(key, value);
            returnResource(redis);
        }
    
        public String get(int db, String key) {
            Jedis redis = getJedis();
            redis.auth(password);
            redis.select(db);
            String ret = redis.get(key);
            returnResource(redis);
            return ret;
        }
    
        /**
         * 添加元素到集合中
         * 
         * @param key
         * @param element
         */
        public boolean sadd(int db, String key, String... element) {
            if (element == null || element.length == 0) {
                return false;
            }
            Jedis redis = getJedis();
            redis.auth(password);
            redis.select(db);
            boolean success = redis.sadd(key, element) == 1;
            returnResource(redis);
            return success;
        }
    
        public boolean smove(int db, String oldKey, String newKey, String element) {
            if (element == null) {
                return false;
            }
            Jedis redis = getJedis();
            redis.auth(password);
            redis.select(db);
            boolean success = (redis.smove(oldKey, newKey, element) == 1);
            returnResource(redis);
            return success;
        }
    
        public static void destroy() {
            getInstance().sentinelPool.destroy();
        }
    
        // 中间省略一万字:中间都是Redis的各种API的操作,需要了解的朋友可以去看看Jedis的API文档
    

    六、Memcache数据结果集缓存

    上文在介绍Hibernate中说到了Memcache对Mysql结果集的缓存,Memcache作为一种内存数据库,经常用作应用系统的缓存系统,我也将Memcache引入到项目作为Mysql数据结果集的缓存系统,其实在实现Memcache对Mysql查询的缓存的过程中,我曾进行了多种尝试,具体有以下几种缓存模型:
    1.无缓存
    这种方式不使用Memcache缓存,游戏服务器的操作直接穿透到Mysql中,这种方式在高并发环境下容易引起Mysql服务器高负载情况,如下图所示:
    缓存模型1
    2.查询使用缓存,更新穿透到数据库,数据库同步数据到缓存
    这种方式在客户端表现来看可以提供一部分速度,因为查询操作都是基于缓存的,但实际上Mysql的负担反而加大了,因为每一个更新请求,都需要Mysql同步最新的查询结果集给Memcache,因为每一个更新操作都会带来一个查询操作,当然这个同步过程可以使异步,但是就算我们感受不到这个同步的过程,但在实际上也是加大了数据库的负载,如下图所示:
    缓存模型2
    3.更新和查询都使用缓存,缓存按策略与数据库进行同步
    这种方式是比较好的方式,因为客户端的所有操作都是被缓存给拦截下来了,所有操作均是基于缓存,不会穿透到数据库,而缓存与数据库之间可以按照一定策略进行同步,如每5分钟同步一次数据到数据库等,具体同步策略可根据情况具体调整,当然这种方式的缺陷就是一旦服务器宕机,那么在上次同步到宕机这段时间之间的数据都会丢失,如下图所示:
    缓存模型3
    4.更新和查询都是用缓存,更新操作同时穿透到数据库,数据库同步缓存的查询
    这种方式是我最终使用的方式,虽然更新操作穿透到数据库,但是我可以在保证查询效率的同时,也保证数据的安全稳定性,因为每一步更新操作都是要进行数据库存储的,并且所有的查询操作可以直接在缓存中进行,如下图所示:
    缓存模型4

    需要支持缓存的类需实现MCSupport接口:

    package com.hjc._36.util.cache;
    
    import java.io.Serializable;
    
    /**
     * 实现此接口后还需要再MC类中增加cachedClass,并使用
     * com.qx.persistent.HibernateUtil.find(Class<T>, long)
     * 代替where进行查询。
     * 需要对控制了的对象在第一次存库时调用MC.add,再调用HIbernateUtil.insert
     * @author 何金成
     *
     */
    public interface MCSupport extends Serializable{
        long getIdentifier();
    }
    

    Memcache的工具类MemcacheCRUD实现了Memcache的连接,以及add,update,delete等操作,具体代码如下:

    package com.hjc._36.util.memcached;
    
    import java.util.Arrays;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import com.danga.MemCached.MemCachedClient;
    import com.danga.MemCached.SockIOPool;
    import com.hjc._36.core.GameInit;
    
    /**
     * @author 何金成
     */
    public class MemcachedCRUD {
    
        protected static Logger logger = LoggerFactory
                .getLogger(MemcachedCRUD.class);
        public static String poolName = "gameDBPool";
        protected static MemCachedClient memCachedClient;
        protected static MemcachedCRUD memcachedCRUD = null;
        public static SockIOPool sockIoPool;
    
        private MemcachedCRUD() {
        }
    
        public void init() {
            sockIoPool = init(poolName, "cacheServer");
            memCachedClient = new MemCachedClient(poolName);
            // if true, then store all primitives as their string value.
            memCachedClient.setPrimitiveAsString(true);
        }
    
        public static SockIOPool init(String poolName, String confKey) {
            // 缓存服务器
            String cacheServers = null;
            if (GameInit.cfg != null) {
                cacheServers = GameInit.cfg.getServerByName(confKey);
            }
            String server[] = { "127.0.0.1:11211" };
            if (cacheServers == null || "".equals(cacheServers)) {
            } else {
                server[0] = cacheServers;
            }
            // 创建一个连接池
            SockIOPool pool = SockIOPool.getInstance(poolName);
            logger.info("连接池{}缓存配置 {}", poolName, Arrays.toString(server));
            pool.setServers(server);// 缓存服务器
            pool.setInitConn(50); // 初始化链接数
            pool.setMinConn(50); // 最小链接数
            pool.setMaxConn(500); // 最大连接数
            pool.setMaxIdle(1000 * 60 * 60);// 最大处理时间
            pool.setMaintSleep(3000);// 设置主线程睡眠时,每3秒苏醒一次,维持连接池大小
            pool.setNagle(false);// 关闭套接字缓存
            pool.setSocketTO(3000);// 链接建立后超时时间
            pool.setSocketConnectTO(0);// 链接建立时的超时时间
            pool.initialize();
            return pool;
        }
    
        public static void destroy() {
            sockIoPool.shutDown();
        }
    
        public static MemcachedCRUD getInstance() {
            if (memcachedCRUD == null) {
                memcachedCRUD = new MemcachedCRUD();
            }
            return memcachedCRUD;
        }
    
        private static final long INTERVAL = 100;
    
        public boolean exist(String key) {
            return memCachedClient.keyExists(key);
        }
    
        public boolean add(String key, Object o) {
            return memCachedClient.add(key, o);
        }
    
        public boolean update(String key, Object o) {
            return memCachedClient.replace(key, o);
        }
    
        public boolean saveObject(String key, Object msg) {
            boolean o = memCachedClient.keyExists(key);
            if (o) {// 存在替换掉
                return memCachedClient.replace(key, msg);
            } else {
                return memCachedClient.add(key, msg);
            }
        }
    
        public boolean keyExist(String key) {
            return memCachedClient.keyExists(key);
        }
    
        /**
         * delete
         * 
         * @param key
         */
        public boolean deleteObject(String key) {
            return memCachedClient.delete(key);
        }
    
        public Object getObject(String key) {
            Object obj = memCachedClient.get(key);
            return obj;
        }
    
        public static MemCachedClient getMemCachedClient() {
            return memCachedClient;
        }
    }
    

    使用Memcache进行缓存,则需要封装一个缓存的操作工具类MC,封装各种缓存操作方法,具体代码如下:

    package com.hjc._36.util.cache;
    
    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import com.alibaba.fastjson.JSON;
    import com.hjc._36.core.GameInit;
    import com.hjc._36.manager.bag.ItemInfo;
    import com.hjc._36.manager.building.BuildingInfo;
    import com.hjc._36.manager.card.CardInfo;
    import com.hjc._36.manager.equip.EquipInfo;
    import com.hjc._36.manager.junzhu.JunZhu;
    import com.hjc._36.manager.mail.MailInfo;
    import com.hjc._36.manager.pve.PveInfo;
    import com.hjc._36.manager.task.MainTaskInfo;
    import com.hjc._36.manager.tec.TecInfo;
    import com.hjc._36.manager.zhenxing.ZhenXingInfo;
    import com.hjc._36.util.memcached.MemcachedCRUD;
    
    public class MC {
        /**
         * 控制哪些类进行memcached缓存。 被控制的类在进行创建时,需要注意调用MC的add和hibernate的insert。
         */
        public static Set<Class<? extends MCSupport>> cachedClass = new HashSet<Class<? extends MCSupport>>();
        public static Set<String> cachedList = new HashSet<String>();
        static {
            cachedClass.add(JunZhu.class);
            // 添加需要缓存find操作的类
        }
        static {
            cachedList.add(CardInfo.class.getSimpleName());
            // 添加需要缓存list操作的类
        }
    
        public static <T> T get(Class<T> t, String id) {
            if (!cachedClass.contains(t)) {
                return null;
            }
            StringBuffer key = new StringBuffer();
            key.append(GameInit.serverId).append("#").append(t.getSimpleName())
                    .append("#").append(id);
            Object o = MemcachedCRUD.getInstance().getObject(key.toString());
            return (T) o;
        }
    
        public static <T> List<T> getList(Class<T> t, String id, String where) {
            if (!cachedList.contains(t.getSimpleName())) {
                return null;
            }
            StringBuffer key = new StringBuffer();
            key.append(GameInit.serverId).append("#").append(id).append("#")
                    .append(t.getSimpleName()).append("#").append(where);
            Object o = MemcachedCRUD.getInstance().getObject(key.toString());
            return (List<T>) o;
        }
    
        public static <T> String getListKeys(String tName, String id) {
            if (!cachedList.contains(tName)) {
                return null;
            }
            StringBuffer key = new StringBuffer();
            key.append(GameInit.serverId).append("#").append(id).append("#")
                    .append(tName);
            Object o = MemcachedCRUD.getInstance().getObject(key.toString());
            return (String) o;
        }
    
        public static Object getValue(String key) {
            Object o = MemcachedCRUD.getInstance().getObject(key);
            return o;
        }
    
        public static boolean add(Object t, String id) {
            if (!cachedClass.contains(t.getClass())) {
                return false;
            }
            StringBuffer key = new StringBuffer();
            key.append(GameInit.serverId).append("#")
                    .append(t.getClass().getSimpleName()).append("#").append(id);
            return MemcachedCRUD.getInstance().add(key.toString(), t);
        }
    
        public static boolean addList(List list, String tName, String id,
                String where) {
            if (!cachedList.contains(tName)) {
                return false;
            }
            StringBuffer key = new StringBuffer();
            key.append(GameInit.serverId).append("#").append(id).append("#")
                    .append(tName);
            String c = key.toString();
            key.append("#").append(where);
            Object tmp = MemcachedCRUD.getInstance().getObject(c);
            String keys = tmp == null ? "" : (String) tmp;
            if (keys.equals("")) {
                MemcachedCRUD.getInstance().add(c, key.toString());
            } else if (!keys.contains(key.toString())) {
                MemcachedCRUD.getInstance().update(c, keys + "," + key.toString());
            }
            return MemcachedCRUD.getInstance().add(key.toString(), list);
        }
    
        public static boolean addKeyValue(String key, Object value) {
            return MemcachedCRUD.getInstance().add(key, value);
        }
    
        public static void update(Object t, String id) {
            if (!cachedClass.contains(t.getClass())) {
                return;
            }
            StringBuffer key = new StringBuffer();
            key.append(GameInit.serverId).append("#")
                    .append(t.getClass().getSimpleName()).append("#").append(id);
            MemcachedCRUD.getInstance().update(key.toString(), t);
        }
    
        public static boolean updateList(List list, String tName, String id,
                String where) {
            if (!cachedList.contains(tName)) {
                return false;
            }
            StringBuffer key = new StringBuffer();
            key.append(GameInit.serverId).append("#").append(id).append("#")
                    .append(tName);
            String c = key.toString();
            key.append("#").append(where);
            Object tmp = MemcachedCRUD.getInstance().getObject(c);
            String keys = tmp == null ? "" : (String) tmp;
            if (keys.equals("")) {
                MemcachedCRUD.getInstance().add(c, key.toString());
            } else if (!keys.contains(key)) {
                MemcachedCRUD.getInstance().update(c, keys + "," + key.toString());
            }
            return MemcachedCRUD.getInstance().update(key.toString(), list);
        }
    
        /**
         * 根据主键删除缓存
         * 
         * @param obj
         *            删除对象
         * @param id
         *            主键id
         */
        public static void delete(Class clazz, String id) {
            if (!cachedClass.contains(clazz)) {
                return;
            }
            StringBuffer key = new StringBuffer();
            key.append(GameInit.serverId).append("#").append(clazz.getSimpleName())
                    .append("#").append(id);
            MemcachedCRUD.getInstance().deleteObject(key.toString());
        }
    }
    

    以上代码中,find方法的缓存和list方法的缓存需要分别实现,find方法缓存只需要将类名和id作为key,对象作为value即可,而list的缓存不仅需要缓存所有的结果集,还需要缓存所有的where查询条件,根据类型查询出where条件,然后根据where条件分别进行缓存。
    HibernateUtil中使用缓存部分的java代码如下,其中注释的方法为上面第二种缓存模型的实现(现已被我淘汰):

    public static void synMC4Insert(String tName, Object o, String id) {
        String keys = MC.getListKeys(tName, id);
        if (keys != null && keys.length() > 0) {// 遍历所有的where缓存
            String[] wheres = keys.split(",");
            for (String where : wheres) {
                where = where.split("#")[3];
                List list = MC.getList(o.getClass(), id, where);
                list.add(o);
                MC.updateList(list, tName, id, where);
            }
        }
    }
    
    public static void synMC4Save(String tName, Object o, String id) {
        String keys = MC.getListKeys(tName, id);
        if (keys != null && keys.length() > 0) {// 遍历所有的where缓存
            String[] wheres = keys.split(",");
            for (String where : wheres) {
                where = where.split("#")[3];
                List list = MC.getList(o.getClass(), id, where);
                MCSupport mc = (MCSupport) o;
                boolean flag = false;
                int index = 0;
                for (Iterator iterator = list.iterator(); iterator.hasNext(); index++) {
                    MCSupport tmpObj = (MCSupport) iterator.next();
                    if (tmpObj.getIdentifier() == mc.getIdentifier()) {
                        list.set(index, o);
                        flag = true;
                    }
                }
                if (!flag) {
                    list.add(o);
                }
                MC.updateList(list, tName, id, where);
            }
        }
    }
    
    public static void synMC4Update(String tName, Object o, String id) {
        String keys = MC.getListKeys(tName, id);
        if (keys != null && keys.length() > 0) {// 遍历所有的where缓存
            String[] wheres = keys.split(",");
            for (String where : wheres) {
                where = where.split("#")[3];
                List list = MC.getList(o.getClass(), id, where);
                MCSupport mc = (MCSupport) o;
                int index = 0;
                for (Iterator iterator = list.iterator(); iterator.hasNext(); index++) {
                    MCSupport tmpObj = (MCSupport) iterator.next();
                    if (tmpObj.getIdentifier() == mc.getIdentifier()) {
                        list.set(index, o);
                        break;
                    }
                }
                MC.updateList(list, tName, id, where);
            }
        }
    }
    
    public static void synMC4Delete(String tName, Object o, String id) {
        String keys = MC.getListKeys(tName, id);
        if (keys != null && keys.length() > 0) {// 遍历所有的where缓存
            String[] wheres = keys.split(",");
            for (String where : wheres) {
                where = where.split("#")[3];
                List list = MC.getList(o.getClass(), id, where);
                MCSupport mc = (MCSupport) o;
                for (Iterator iterator = list.iterator(); iterator.hasNext();) {
                    MCSupport tmpObj = (MCSupport) iterator.next();
                    if (tmpObj.getIdentifier() == mc.getIdentifier()) {
                        iterator.remove();
                        break;
                    }
                }
                MC.updateList(list, tName, id, where);
            }
        }
    }
    
    // public static <T> void synchronizeDB2MemAsy(final Class<T> t,
    // final String id) {
    // ExecutorPool.dbThread.execute(new Runnable() {
    // @Override
    // public void run() {
    // String keys = MC.getListKeys(t.getSimpleName(), id);
    // if (keys != null && keys.length() > 0) {// 遍历所有的where缓存
    // String[] wheres = keys.split(",");
    // for (String where : wheres) {
    // where = where.split("#")[3];
    // List<T> list = list(t, where, false);
    // MC.updateList(list, t.getSimpleName(), id, where);
    // if (showMCHitLog) {
    // log.info("DB 同步 MC 成功 t:{},where:{}",
    // t.getSimpleName(), where);
    // }
    // }
    // }
    // }
    // });
    // }
    //
    // public static <T> void synchronizeDB2MemSyn(final Class<T> t,
    // final String id) {
    // String keys = MC.getListKeys(t.getSimpleName(), id);
    // if (keys != null && keys.length() > 0) {// 遍历所有的where缓存
    // String[] wheres = keys.split(",");
    // for (String where : wheres) {
    // where = where.split("#")[3];
    // List<T> list = list(t, where, false);
    // MC.updateList(list, t.getSimpleName(), id, where);
    // if (showMCHitLog) {
    // log.info("DB 同步 MC 成功 t:{},where:{}", t.getSimpleName(),
    // where);
    // }
    // }
    // }
    // }
    

    七、总结

    以上是本文对我们这款游戏中的数据管理的介绍,游戏服务器中的各种数据是多种多样的,我们应该根据各种数据的各种性质,合理利用进行存取,以保证不管什么类型的游戏数据,在我们的游戏服务器中都可以安全稳定的运行,数据安全,玩家才会放心!

  • 相关阅读:
    iOS NSDictionary或NSArray与JSON字符串相互转换
    iOS 如何用Xib画一个Button
    iOS 你不知道的字符串用法
    IOS ScrollView pagingEnabled移动指定偏移
    TableView行缩进 自定义cell时候
    支付宝回调
    微信支付回调
    关于Bundle传递消息
    关于Handler
    ExpandableListView
  • 原文地址:https://www.cnblogs.com/hjcenry/p/5856941.html
Copyright © 2011-2022 走看看