• 文章版权归腾讯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 clazzString className = this.packageName + clzName;try {Class<?> classObject = Class.forName(className);if (classObject == null) {logger.error("未找到类" + className);return null;}// Get all the declared fieldsField[] 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 classClass<?> 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 objectList<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);elsereturn 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();elsefile.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来对数据进行加载,代码如下:

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来创建数据库的连接以及增删改查的操作。在游戏数据中,我对游戏中的冷数据做了一个总结,如下图所示:

完成要存储的游戏数据的分析之后,我们就可以进行具体建模建表的工作,完成对数据的设计。由于在游戏服务器的数据存储中,数据库基本上只是一个游戏数据临时存放的地方,所以游戏数据中的关联性并不是特别强,所以不需要严密的数据库设计,只需简单的将玩家所有的数据按照一个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;@Idpublic long id;// —用户idpublic 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.addMC.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.addMC.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.addMC.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操作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服务器高负载情况,如下图所示:

2.查询使用缓存,更新穿透到数据库,数据库同步数据到缓存
这种方式在客户端表现来看可以提供一部分速度,因为查询操作都是基于缓存的,但实际上Mysql的负担反而加大了,因为每一个更新请求,都需要Mysql同步最新的查询结果集给Memcache,因为每一个更新操作都会带来一个查询操作,当然这个同步过程可以使异步,但是就算我们感受不到这个同步的过程,但在实际上也是加大了数据库的负载,如下图所示:

3.更新和查询都使用缓存,缓存按策略与数据库进行同步
这种方式是比较好的方式,因为客户端的所有操作都是被缓存给拦截下来了,所有操作均是基于缓存,不会穿透到数据库,而缓存与数据库之间可以按照一定策略进行同步,如每5分钟同步一次数据到数据库等,具体同步策略可根据情况具体调整,当然这种方式的缺陷就是一旦服务器宕机,那么在上次同步到宕机这段时间之间的数据都会丢失,如下图所示:

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

七、总结

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

转载于:https://www.cnblogs.com/hjcenry/p/5856941.html

SLG手游Java服务器的设计与开发——数据管理相关推荐

  1. SLG手游Java服务器的设计与开发——架构分析

    微信公众号[程序员江湖] 作者黄小斜,斜杠青年,某985硕士,阿里 Java 研发工程师,于 2018 年秋招拿到 BAT 头条.网易.滴滴等 8 个大厂 offer,目前致力于分享这几年的学习经验. ...

  2. SLG手游Java服务器数据管理方案

    前言 这一年左右的时间,我参与并完成了一款SLG手游的研发,我负责游戏的服务端研发.这是一款以三国为题材的游戏,除了有三国名将的卡牌养成.多种多样装备养成.PVE,玩家竞技场等常见玩法外,我们的游戏的 ...

  3. 从《乱世王者》看腾讯SLG手游如何搭建完整安全服务

    WeTest 导读 <乱世王者>是由腾讯旗下天美工作室群自主研发的一款战争策略手游,在经历了2015年-2017年的SLG品类手游的爆发之势下,于2017年11月21日正式公测. < ...

  4. SLG手游的战斗抽象

    说明:本文仅讨论王国纪元.乱世王者这类的SLG手游战斗. 战争策略游戏的核心追求点 1. 玩家通过进行兵种选择和分配,能够战胜对方 2. 玩家通过进行兵种选择和分配,在战胜对方的基础上,以自身最小的损 ...

  5. 一些关于SLG手游的想法

    目前,市面上RPG手游产品已处于成熟的阶段,不管是游戏中的战斗模块还是其他玩法,在RPG手游中变化不大,因而同质化现象比较严重.相比之下,SLG手游还处于成长阶段,各个方面都还有很大的提升空间,比如战 ...

  6. Unity手游实战:ECS设计思想和Entitas插件

    Unity手游实战:ECS设计思想和Entitas插件 一.ECS设计思想 ECS设计理念并不是一个新兴的事物,早在90年代就存在了.但是走入大众视野则要归功于<守望先锋>这款游戏.201 ...

  7. 从《乱世王者》看腾讯 SLG 手游如何搭建完整安全服务

    作者:WeTest小编 商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处. 原文链接:https://wetest.qq.com/lab/view/453.html WeTest 导读 & ...

  8. 楚留香手游系统互通的服务器,楚留香手游互通服务器汇总 哪些服能一起玩

    楚留香手游互通服务器汇总 哪些服能一起玩由第一手游网小编为大家带来,游戏中有一些服务器它的安卓和ios是互通的可以一起玩的,楚留香手游哪些服能一起玩?哪些服务器互通?来看看吧! 楚留香手游互通服务器汇 ...

  9. 手游复古传奇服务器维护,复古传奇手游:服务器中那些极品装备,属性也是逆天!...

    原标题:复古传奇手游:服务器中那些极品装备,属性也是逆天! 说起传奇早期的顶级装备,屠龙噬魂龙纹大家都知道.但是有些顶级装备却不为人知,因为他们本身的出现就是一个秘密.很多人说这些装备的出现本身就是游 ...

  10. 如何解除服务器注册,《天龙八部手游》服务器注册上限怎么办 服务器注册方法...

    导 读 天龙八部手游服务器注册上限解决办法由九游小编为大家带来,有玩家反映现在注册天龙八部手游有的服务器会提示"本服务器的注册已达到上限",天龙八部手游服务器的注册已达到上限怎么办 ...

最新文章

  1. Oracle学习之段区块初步概念
  2. 2、Java Swing JFrame和JPanel:窗口容器和面板容器
  3. python编程怎么做游戏主播_如何成为一名成功的编程主播?
  4. linux删除目录排除,Linux中移动,复制,删除,打包排除某个目录或文件
  5. the code place where the binding is converted to final value displayed in u
  6. mysql text类型效率_mysql使用笔记:vachar,char,text比较
  7. POJ 2492 A Bug's Life
  8. c语言小程序 祝你生日快乐!
  9. 合宙 4G模块Air720D Mini PCI-E 开发教程
  10. 用插值法求国债收益率
  11. 拍牌服务器提交中断,沪牌拍牌的经验总结
  12. 什么是无线信号的灵敏度和保真度
  13. 有关计算机病毒种类书籍,计算机病毒的分类
  14. rm -rf /* 数据恢复记录
  15. java中通物流api详解
  16. win10打开软件提示无法成功完成操作 因为文件包含病毒
  17. 像素、移动端网页——CSS
  18. 云原生架构的核心技术
  19. java多线程之--(1)深入理解多线程的原理以及使用线程的方法--(原理图+内存图)
  20. busyer试做MFC简易画板

热门文章

  1. 中文编程发展与兴起的重要意义
  2. 论文阅读:NEURAL MACHINE TRANSLATIONBY JOINTLY LEARNING TO ALIGN AND TRANSLATE
  3. Citrix PVS7.6 测试调试
  4. HackTheBox-Spider WP
  5. MATLAB数字图像处理小系统 【带GUI界面,源码实现】
  6. 你需要明白的索引和约束的前缀(AK,PK,IX,CK,FK,DF,UQ)
  7. simplest_ffmpeg_demuxer_simple(新版ffmpeg函数)
  8. springboot毕设项目商城积分兑换系统pez18(java+VUE+Mybatis+Maven+Mysql)
  9. 大数据IMF传奇行动绝密课程第91课:SparkStreaming基于Kafka Direct案例实战和内幕源码解密
  10. matlab 圣诞树,搞气氛!用MATLAB画一棵Bling Bling的圣诞树