0%

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

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

前言

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

游戏数据分析

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

游戏静态数据

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

dataConfig.xml

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

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

CSV文件

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

1
2
3
4
卡牌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秒

JavaBean

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.kidbear._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

}

CsvDataLoader.java

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
package com.kidbear._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.kidbear._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.kidbear._36.template.",
"/dataConfig.xml");
dl.load();
}
}

CsvParser

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package com.kidbear._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();
}
}

TempletService.java

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package com.kidbear._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.kidbear._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配置
}
}

使用静态数据

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

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

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

Mysql存储数据

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

Druid数据库连接池

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

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--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进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<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进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<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配置及参数设置。

Hibernate

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!-- 定义事务管理 -->
<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.kidbear._36" />
</bean>

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.kidbear._36.manager.junzhu;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import com.kidbear._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,这个工具类大家可以拿去直接使用,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
package com.kidbear._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.kidbear._36.core.GameInit;
import com.kidbear._36.manager.card.CardInfo;
import com.kidbear._36.manager.equip.EquipInfo;
import com.kidbear._36.manager.mail.MailInfo;
import com.kidbear._36.manager.pve.PveInfo;
import com.kidbear._36.manager.zhenxing.ZhenXingInfo;
import com.kidbear._36.manager.zhenxing.ZhenxingMgr;
import com.kidbear._36.task.ExecutorPool;
import com.kidbear._36.util.cache.MC;
import com.kidbear._36.util.cache.MCSupport;
import com.kidbear._36.util.memcached.MemcachedCRUD;

public class HibernateUtil {
public static boolean showMCHitLog = false;
public static Logger log = LoggerFactory.getLogger(HibernateUtil.class);
public static Map<Class<?>, String> beanKeyMap = new HashMap<Class<?>, String>();
public static long synDelayT = 0;// 延迟同步周期
private static SessionFactory sessionFactory;

public static void init() {
sessionFactory = buildSessionFactory();
}

public static SessionFactory getSessionFactory() {
return sessionFactory;
}

public static Throwable insert(Object o, long id) {
Session session = sessionFactory.getCurrentSession();
session.beginTransaction();
try {
session.save(o);
session.getTransaction().commit();
if (MC.cachedList.contains(o.getClass().getSimpleName())) {
synMC4Insert(o.getClass().getSimpleName(), o,
String.valueOf(id));
}
} catch (Throwable e) {
log.error("0要insert的数据{}", o == null ? "null" : JSONObject
.fromObject(o).toString());
log.error("0保存出错", e);
session.getTransaction().rollback();
return e;
}
return null;
}

/**
* FIXME 不要这样返回异常,没人会关系返回的异常。
*
* @param o
* @return
*/
public static Throwable save(Object o, long id) {
Session session = sessionFactory.getCurrentSession();
Transaction t = session.beginTransaction();
boolean mcOk = false;
try {
if (o instanceof MCSupport) {
MCSupport s = (MCSupport) o;// 需要对控制了的对象在第一次存库时调用MC.add
MC.update(o, String.valueOf(s.getIdentifier()));// MC中控制了哪些类存缓存。
mcOk = true;
// session.update(o);
session.saveOrUpdate(o);
} else {
session.saveOrUpdate(o);
}
t.commit();
if (o instanceof MCSupport) {
if (MC.cachedList.contains(o.getClass().getSimpleName())) {
synMC4Save(o.getClass().getSimpleName(), o,
String.valueOf(id));
}
}
} catch (Throwable e) {
log.error("1要save的数据{},{}", o, o == null ? "null" : JSONObject
.fromObject(o).toString());
if (mcOk) {
log.error("MC保存成功后报错,可能是数据库条目丢失。");
}
log.error("1保存出错", e);
t.rollback();
return e;
}
return null;
}

public static Throwable update(Object o, String id) {
Session session = sessionFactory.getCurrentSession();
Transaction t = session.beginTransaction();
try {
if (o instanceof MCSupport) {
MCSupport s = (MCSupport) o;// 需要对控制了的对象在第一次存库时调用MC.add
MC.update(o, String.valueOf(s.getIdentifier()));// MC中控制了哪些类存缓存。
session.update(o);
} else {
session.update(o);
}
t.commit();
if (o instanceof MCSupport) {
if (MC.cachedList.contains(o.getClass().getSimpleName())) {
synMC4Update(o.getClass().getSimpleName(), o,
String.valueOf(id));
}
}
} catch (Throwable e) {
log.error("1要update的数据{},{}", o, o == null ? "null" : JSONObject
.fromObject(o).toString());
log.error("1保存出错", e);
t.rollback();
return e;
}
return null;
}

public static <T> T find(Class<T> t, long id) {
String keyField = getKeyField(t);
if (keyField == null) {
throw new RuntimeException("类型" + t + "没有标注主键");
}
if (!MC.cachedClass.contains(t)) {
return find(t, "where " + keyField + "=" + id, false);
}
T ret = MC.get(t, String.valueOf(id));
if (ret == null) {
if (showMCHitLog)
log.info("MC未命中{}#{}", t.getSimpleName(), id);
ret = find(t, "where " + keyField + "=" + id, false);
if (ret != null) {
if (showMCHitLog)
log.info("DB命中{}#{}", t.getSimpleName(), id);
MC.add(ret, String.valueOf(id));
} else {
if (showMCHitLog)
log.info("DB未命中{}#{}", t.getSimpleName(), id);
}
} else {
if (showMCHitLog)
log.info("MC命中{}#{}", t.getSimpleName(), id);
}
return ret;
}

public static <T> T find(Class<T> t, String where) {
return find(t, where, true);
}

public static <T> T find(Class<T> t, String where, boolean checkMCControl) {
// if (checkMCControl && MC.cachedClass.contains(t)) {
// // 请使用static <T> T find(Class<T> t,long id)
// throw new BaseException("由MC控制的类不能直接查询DB:" + t);
// }
Session session = sessionFactory.getCurrentSession();
Transaction tr = session.beginTransaction();
T ret = null;
try {
// FIXME 使用 session的get方法代替。
String hql = "from " + t.getSimpleName() + " " + where;
Query query = session.createQuery(hql);
ret = (T) query.uniqueResult();
tr.commit();
} catch (Exception e) {
tr.rollback();
log.error("list fail for {} {}", t, where);
log.error("list fail", e);
}
return ret;
}

public static <T> List<T> list(Class<T> t, long id, String where) {
String keyField = getKeyField(t);
if (keyField == null) {
throw new RuntimeException("类型" + t + "没有标注主键");
}
if (!MC.cachedList.contains(t.getSimpleName())) {
return list(t, where, false);
}
List<T> ret = MC.getList(t, String.valueOf(id), where);
if (ret == null) {
if (showMCHitLog)
log.info("MC未命中{}#{}", t.getSimpleName(), where);
ret = list(t, where, false);
if (ret != null) {
if (showMCHitLog)
log.info("DB命中{}#{}", t.getSimpleName(), where);
MC.addList(ret, t.getSimpleName(), String.valueOf(id), where);
} else {
if (showMCHitLog)
log.info("DB未命中{}#{}", t.getSimpleName(), where);
}
} else {
if (showMCHitLog)
log.info("MC命中{}#{}", t.getSimpleName(), where);
}
return ret;
}

/**
* @param t
* @param where
* 例子: where uid>100
* @return
*/
public static <T> List<T> list(Class<T> t, String where,
boolean checkMCControl) {
// if (checkMCControl && MC.cachedList.contains(t)) {
// // 请使用static <T> T find(Class<T> t,long id)
// throw new BaseException("由MC控制的类不能直接查询DB:" + t);
// }
Session session = sessionFactory.getCurrentSession();
Transaction tr = session.beginTransaction();
List<T> list = Collections.EMPTY_LIST;
try {
String hql = "from " + t.getSimpleName() + " " + where;
Query query = session.createQuery(hql);
list = query.list();
tr.commit();
} catch (Exception e) {
tr.rollback();
log.error("list fail for {} {}", t, where);
log.error("list fail", e);
}
return list;
}

public static SessionFactory buildSessionFactory() {
log.info("开始构建hibernate");
String path = "classpath*:spring-conf/applicationContext.xml";
ApplicationContext ac = new FileSystemXmlApplicationContext(path);
sessionFactory = (SessionFactory) ac.getBean("sessionFactory");
log.info("结束构建hibernate");
return sessionFactory;
}

public static Throwable delete(Object o, long id) {
if (o == null) {
return null;
}
Session session = sessionFactory.getCurrentSession();
session.beginTransaction();
try {
session.delete(o);
session.getTransaction().commit();
if (o instanceof MCSupport) {
MCSupport s = (MCSupport) o;// 需要对控制了的对象在第一次存库时调用MC.add
MC.delete(o.getClass(), String.valueOf(s.getIdentifier()));// MC中控制了哪些类存缓存。
if (MC.cachedList.contains(o.getClass().getSimpleName())) {
synMC4Delete(o.getClass().getSimpleName(), o,
String.valueOf(id));
}
}
} catch (Throwable e) {
log.error("要删除的数据{}", o);
log.error("出错", e);
session.getTransaction().rollback();
return e;
}
return null;
}

/**
* 注意这个方法会返回大于等于1的值。数据库无记录也会返回1,而不是null
*
* @param t
* @return
*/
public static <T> Long getTableIDMax(Class<T> t) {
Long id = null;
Session session = sessionFactory.getCurrentSession();
Transaction tr = session.beginTransaction();
String hql = "select max(id) from " + t.getSimpleName();
try {
Query query = session.createQuery(hql);
Object uniqueResult = query.uniqueResult();
if (uniqueResult == null) {
id = 1L;
} else {
id = Long.parseLong(uniqueResult + "");
id = Math.max(1L, id);
}
tr.commit();
} catch (Exception e) {
tr.rollback();
log.error("query max id fail for {} {}", t, hql);
log.error("query max id fail", e);
}
return id;
}

public static List<Map<String, Object>> querySql(String hql) {
Session session = sessionFactory.getCurrentSession();
Transaction tr = session.beginTransaction();
List list = Collections.emptyList();
try {
SQLQuery query = session.createSQLQuery(hql);
query.setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP);
list = query.list();
tr.commit();
} catch (Exception e) {
tr.rollback();
log.error("query failed {}", hql);
log.error("query count(type) fail", e);
}
return list;
}
}

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

Redis存储数据

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
package com.kidbear._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.kidbear._36.core.GameInit;

public class Redis {
private static Redis instance;
public static Logger log = LoggerFactory.getLogger(Redis.class);
public static final int GLOBAL_DB = 0;// 全局
public static final int LOGIC_DB = GameInit.serverId;// 模块库
public static String password = null;

public static Redis getInstance() {
if (instance == null) {
instance = new Redis();
}
return instance;
}

// private JedisPool pool;
private JedisSentinelPool sentinelPool;
public String host;
public int port;

// public int getDB(long userid){
//
// }

public Jedis getJedis() {
return this.sentinelPool.getResource();
}

public void returnResource(Jedis jedis) {
jedis.close();
// this.sentinelPool.returnResource(jedis);
}

public void init() {
String redisServer = null;
if (GameInit.cfg != null) {
redisServer = GameInit.cfg.get("redisServer");
password = GameInit.cfg.get("redisPwd");
}
if (redisServer == null) {
redisServer = "127.0.0.1:6440";
}
redisServer = redisServer.trim();
String[] tmp = redisServer.split(":");
host = tmp[0];
port = Integer.parseInt(tmp[1]);
if (tmp.length == 2) {
port = Integer.parseInt(tmp[1].trim());
}
log.info("Redis sentinel at {}:{}", host, port);
// sentinelPool = new JedisPool(host, port);
Set sentinels = new HashSet();
sentinels.add(new HostAndPort(host, port).toString());
sentinelPool = new JedisSentinelPool("master1", sentinels);
}

// private void init() {
// String redisServer = null;
// if (GameInit.cfg != null) {
// redisServer = GameInit.cfg.get("redisServer");
// password = GameInit.cfg.get("redisPwd");
// }
// if (redisServer == null) {
// redisServer = "127.0.0.1:6379";
// }
// redisServer = redisServer.trim();
// String[] tmp = redisServer.split(":");
// host = tmp[0];
// port = Integer.parseInt(tmp[1]);
// if (tmp.length == 2) {
// port = Integer.parseInt(tmp[1].trim());
// }
// log.info("Redis at {}:{}", host, port);
// // sentinelPool = new JedisPool(host, port);
// JedisPoolConfig config = new JedisPoolConfig();
// sentinelPool = new JedisPool(config, host, port, 100000, password);
// }

public void test() {
Jedis j = getJedis();
j.auth(password);
returnResource(j);
}

public void select(int index) {
Jedis j = getJedis();
j.auth(password);
j.select(index);
returnResource(j);
}

public boolean hexist(int db, String key, String field) {
if (key == null) {
return false;
}
Jedis redis = getJedis();
redis.auth(password);
redis.select(db);
boolean ret = redis.hexists(key, field);
returnResource(redis);
return ret;
}

public Long hdel(int db, String key, String... fields) {
Jedis redis = getJedis();
redis.auth(password);
redis.select(db);
Long cnt = redis.hdel(key, fields);
returnResource(redis);
return cnt;
}

public String hget(int db, String key, String field) {
if (key == null) {
return null;
}
Jedis redis = getJedis();
redis.auth(password);
redis.select(db);
String ret = redis.hget(key, field);
returnResource(redis);
return ret;
}

public Map<String, String> hgetAll(int db, String key) {
if (key == null) {
return null;
}
Jedis redis = getJedis();
redis.auth(password);
redis.select(db);
Map<String, String> ret = redis.hgetAll(key);
returnResource(redis);
return ret;
}

public void hset(int db, String key, String field, String value) {
if (field == null || field.length() == 0) {
return;
}
if (value == null || value.length() == 0) {
return;
}
Jedis redis = getJedis();
redis.auth(password);
redis.select(db);
redis.hset(key, field, value);
returnResource(redis);
}

public void add(int db, String group, String key, String value) {
if (value == null || key == null) {
return;
}
Jedis redis = getJedis();
redis.auth(password);
redis.select(db);
redis.hset(group, key, value);
returnResource(redis);
}

public void set(int db, String key, String value) {
if (value == null || key == null) {
return;
}
Jedis redis = getJedis();
redis.auth(password);
redis.select(db);
redis.set(key, value);
returnResource(redis);
}

public String get(int db, String key) {
Jedis redis = getJedis();
redis.auth(password);
redis.select(db);
String ret = redis.get(key);
returnResource(redis);
return ret;
}

/**
* 添加元素到集合中
*
* @param key
* @param element
*/
public boolean sadd(int db, String key, String... element) {
if (element == null || element.length == 0) {
return false;
}
Jedis redis = getJedis();
redis.auth(password);
redis.select(db);
boolean success = redis.sadd(key, element) == 1;
returnResource(redis);
return success;
}

public boolean smove(int db, String oldKey, String newKey, String element) {
if (element == null) {
return false;
}
Jedis redis = getJedis();
redis.auth(password);
redis.select(db);
boolean success = (redis.smove(oldKey, newKey, element) == 1);
returnResource(redis);
return success;
}

public static void destroy() {
getInstance().sentinelPool.destroy();
}

// 中间省略一万字:中间都是Redis的各种API的操作,需要了解的朋友可以去看看Jedis的API文档

Memcache数据结果集缓存

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.kidbear._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等操作,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package com.kidbear._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.kidbear._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,封装各种缓存操作方法,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package com.kidbear._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.kidbear._36.core.GameInit;
import com.kidbear._36.manager.bag.ItemInfo;
import com.kidbear._36.manager.building.BuildingInfo;
import com.kidbear._36.manager.card.CardInfo;
import com.kidbear._36.manager.equip.EquipInfo;
import com.kidbear._36.manager.junzhu.JunZhu;
import com.kidbear._36.manager.mail.MailInfo;
import com.kidbear._36.manager.pve.PveInfo;
import com.kidbear._36.manager.task.MainTaskInfo;
import com.kidbear._36.manager.tec.TecInfo;
import com.kidbear._36.manager.zhenxing.ZhenXingInfo;
import com.kidbear._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代码如下,其中注释的方法为上面第二种缓存模型的实现(现已被我淘汰):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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);
// }
// }
// }
// }

总结

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

大爷赏点儿呗.

欢迎关注我的其它发布渠道