SLG手遊Java服務器的設計與開發——數據管理

  • 文章版權歸騰訊GAD全部,禁止匿名轉載;禁止商業使用;禁止我的使用。

1、前言

上文介紹了咱們的SLG手遊的服務器架構設計以及網絡通訊部分,本文介紹數據管理部分,在數據存儲方面,我選擇了Mysql、Memcache和Redis,利用這三種數據庫各自的優點,各自發揮所長,在項目中有着不一樣的應用。css

2、遊戲數據分析

前文已經對遊戲數據作了大概的分析,以下圖所示:
遊戲數據
這是我我的對遊戲中的數據進行的一種劃分。其中,遊戲數據直接使用代碼將表文件中的數據加載到內存,而後從內存中讀取數據便可。玩家數據則分爲了熱數據和冷數據兩部分分別進行存儲,最大程度利用Mysql和Redis各自的優點。html

3、遊戲靜態數據

在咱們的這款遊戲中,咱們的靜態數據配置在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結構)java

1.dataConfig.xml

dataConfig.xml中存儲全部CSV表的路徑,在CsvLoader.java中直接對這個xml表中的路徑下的CSV文件進行讀取加載。node

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

2.CSV文件

CSV文件中存儲具體的遊戲數據,這個數據表通常是由數值策劃來進行配置。CSV表的本質就是按逗號進行分割的數據,以下是卡牌表的前兩行數據。mysql

卡牌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。git

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文件的內容的讀取,代碼以下:github

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文件按逗號分割的格式的解析的類,代碼以下:web

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 + "\r\n");
        }
        output.write(sb.toString());
        output.flush();
        output.close();
    }
}

6.TempletService.java

服務器啓動時,調用CsvDataLoader的load()方法,以完成對CSV文件的加載,以後就須要使用TempletService的listAll方法來說數據加載到List中,TempletService根據JavaBean的simpleName來對數據進行加載,代碼以下:redis

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表中加載進來的數據了,例如上文提到的卡牌數據表,加載代碼以下:spring

// 卡牌數據表
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,就能夠取到這張卡牌的全部數據。

4、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操做回滾事務。

5、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文檔

6、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);
// }
// }
// }
// }

7、總結

以上是本文對咱們這款遊戲中的數據管理的介紹,遊戲服務器中的各類數據是多種多樣的,咱們應該根據各類數據的各類性質,合理利用進行存取,以保證無論什麼類型的遊戲數據,在咱們的遊戲服務器中均可以安全穩定的運行,數據安全,玩家纔會放心!

相關文章
相關標籤/搜索