游戏服务器缓存作用非常重要:很大部分决定游戏服务器性能问题。用好缓存能够一定程度上提高服务器性能和响应时间。给游戏带来流畅体验。
在很多游戏服务器开发过程中,有需要提前加载到内存中的数据,有不需要加载到内存中的数据,当然,加载到内存中的数据可分为字典数据和部分玩家数据。比如:配表信息,玩家查询信息,玩家基础信息等。
通常我们使用redis作为一个缓存中间件,当然,redis不仅用于游戏服务器,也适用于很多传统互联网行业。是一款优秀的KV缓存数据库。性能还是得到很多开发者的认同。今天,我们在redis的基础上来封装一层缓存框架,使用到常用的游戏服务器当中。
有的设计方式其实通过另起一个进程,通过分配机器内存的形式来通过缓存。这种方式的优点就是可以扩展到不同的物理机器上面。易于维护。缺点就是必须得通过编写响应得程序来实现。但是对于一般得游戏开发团队来说无疑是一大压力。所以,很多都是利用开源的工具库。如图设计:
最流行的可能应该是redis这个内存NOSQL KV数据库了,应用层面非常广泛。而通常在游戏开发服务器后台来说,redis采用集群模式来提供缓存内存支持。redis天生的优化性能,能够支撑极高的IO和并发。因此,游戏中请求频繁的数据就可以保存到redis中。
使用场景:
-游戏活动信息
一般游戏中都有活动数据,一般有全服活动,单服活动等非常复杂的活动,通过游戏多元化,然而每一个玩家都会请求活动数据,因此,通常把活动相关的数据就会保存的redis中。
- 玩家基础数据
玩家信息来回读取,来回修改比较频繁。 读取频率较高,所以这类数据也适合保存到redis中。
使用场景其实很多,根据不同的业务,选不同的数据存储方式即可。下面我们采用redis模式,来实现一条高效方便的缓存框架工具。
实现方式
下面,通过代码的形式来实现一套缓存框架,基于redis的Java api实现我们系统中使用起来方便的模式。
第一步:通过定义公共接口的形式,来实现一个RedisInterface
package com.twjitm.jump.logic.core.database.redis;
import java.util.Map;
/**
* @author twjitm - [Created on 2018-10-22 14:21]
*/
public interface RedisInterface {
/**
* 将所有的po对象的属性值放入到集合中
*
* @return
*/
Map getAllFeildsToHash();
/**
* 获取一个唯一键值
*
* @return
*/
String getUniqueKey();
/**
* 获取属相列表长度
*
* @return
*/
int getFieldLength();
}
多个对象形式的接口:
/**
* @author twjitm - [Created on 2018-10-22 14:27]
*/
public interface RedisListInterface extends RedisInterface {
/**
* 列表对象的子唯一主键属性数组(除去uid这个field之外的)
*/
String[] getSubUniqueKey();
}
定义来三个方法,一个是获取全部字段表,一个获取唯一主键表,一个是获取属性字段的个数。子接口中有获取子对象的唯一键。
定义这几根方法有啥用呢?我们下面来看一个实际例子:在此之前,先介绍一下个工具类,也就是反射相关的东西,要连反射不知道的可以先看看相关的知识,在这就不细说连,工具通过反射获取一个实体类的属性字段并赋值,通过反射将字符串对象转化为实体对象的一个过程。
工具类源码:
package com.twjitm.jump.common.utils.zutils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author twjitm
*/
public class ZCollectionUtil {
/**
* 截取某列表的部分数据
*
* @param
* @param list
* @param skip
* @param pageSize
*/
public static List getSubListPage(List list, int skip, int pageSize) {
if (list == null || list.isEmpty()) {
return null;
}
int startIndex = skip;
int endIndex = skip + pageSize;
if (startIndex > endIndex || startIndex > list.size()) {
return null;
}
if (endIndex > list.size()) {
endIndex = list.size();
}
return list.subList(startIndex, endIndex);
}
/**
* 通过远程URL获取本地IP地址
*
* @param urlCanGainIp
*/
public static String getInetIpAddress(String urlCanGainIp) {
InputStream in = null;
try {
URL url = new URL(urlCanGainIp);
in = url.openStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String line = "", ip = null;
while ((line = reader.readLine()) != null) {
ip = parseIpAddress(line);
if (!ZStringUtil.isEmptyStr(ip)) {
return ip;
}
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
/**
* 判断某个地址是否是IP地址
*
* @param content
*/
public static boolean isIpAddress(String content) {
String rt = parseIpAddress(content);
if (!ZStringUtil.isEmptyStr(rt)) {
if (rt.equals(content)) {
return true;
}
}
return false;
}
/*
* 解释IP地址
*/
private static String parseIpAddress(String content) {
String regexIp = "((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)\\.){3}(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|[1-9])";
Pattern pattern = Pattern.compile(regexIp);
Matcher matcher = pattern.matcher(content);
String rt = null;
while (matcher.find()) {
rt = matcher.group();
}
return rt;
}
/**
* 获取某个对象某些字段的Map
*
* @param obj
*/
public static Map getMap(Object obj, String... strings) {
Map map = new HashMap();
boolean addAllFields = false;
if (strings == null || strings.length == 0) {
addAllFields = true;
}
if (obj != null) {
Field[] fields = getAllFields(obj);
for (Field field : fields) {
field.setAccessible(true);
try {
boolean needsAddToMap = false;
for (String s : strings) {
if (field.getName().equals(s)) {
needsAddToMap = true;
break;
}
}
if (needsAddToMap || addAllFields) {
map.put(field.getName(), getFieldsValueStr(obj, field.getName()));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
if (!addAllFields && strings.length != map.size()) {
return new HashMap<>(16);
}
return map;
}
private static Field[] getAllFields(Object obj) {
Class> clazz = obj.getClass();
Field[] rt = null;
for (; clazz != Object.class; clazz = clazz.getSuperclass()) {
Field[] tmp = clazz.getDeclaredFields();
rt = combine(rt, tmp);
}
return rt;
}
private static Field[] combine(Field[] a, Field[] b) {
if (a == null) {
return b;
}
if (b == null) {
return a;
}
Field[] rt = new Field[a.length + b.length];
System.arraycopy(a, 0, rt, 0, a.length);
System.arraycopy(b, 0, rt, a.length, b.length);
return rt;
}
private static Object getFieldsValueObj(Object obj, String fieldName) {
Field field = getDeclaredField(obj, fieldName);
field.setAccessible(true);
try {
return field.get(obj);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static String getFieldsValueStr(Object obj, String fieldName) {
Object o = ZCollectionUtil.getFieldsValueObj(obj, fieldName);
if (o instanceof Date) {
return ZDateUtil.dateToString((Date) o);
}
if (o == null) {
return null;
}
return o.toString();
}
private static Field getDeclaredField(Object object, String fieldName) {
Class> clazz = object.getClass();
for (; clazz != Object.class; clazz = clazz.getSuperclass()) {
try {
return clazz.getDeclaredField(fieldName);
} catch (Exception e) {
}
}
return null;
}
private static Method getSetMethod(Object object, String method, Class> fieldType) {
Class> clazz = object.getClass();
for (; clazz != Object.class; clazz = clazz.getSuperclass()) {
try {
return clazz.getDeclaredMethod(method, fieldType);
} catch (Exception e) {
}
}
return null;
}
/**
* map集合里反序列化对象
*
* @param map map数据字段
* @param obj 对象
* @param
* @return
*/
public static T getObjFromMap(Map map, Object obj) {
try {
for (String key : map.keySet()) {
Field field = getDeclaredField(obj, key);
Method method = getSetMethod(obj, buildSetMethod(key), field.getType());
if (field.getType() == Integer.class || field.getType() == int.class) {
method.invoke(obj, Integer.parseInt(map.get(key)));
} else if (field.getType() == Boolean.class || field.getType() == boolean.class) {
method.invoke(obj, Boolean.parseBoolean(map.get(key)));
} else if (field.getType() == Long.class || field.getType() == long.class) {
method.invoke(obj, Long.parseLong(map.get(key)));
} else if (field.getType() == Float.class || field.getType() == float.class) {
method.invoke(obj, Float.parseFloat(map.get(key)));
} else if (field.getType() == Double.class || field.getType() == double.class) {
method.invoke(obj, Double.parseDouble(map.get(key)));
} else if (field.getType() == Byte.class || field.getType() == byte.class) {
method.invoke(obj, Byte.parseByte(map.get(key)));
} else if (field.getType() == Short.class || field.getType() == short.class) {
method.invoke(obj, Short.parseShort(map.get(key)));
} else if (field.getType() == String.class) {
method.invoke(obj, map.get(key));
} else if (field.getType() == Date.class) {
method.invoke(obj, ZDateUtil.stringToDate(map.get(key)));
}
}
return (T) obj;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 从map里构造出一个实例对象
*
* @param map
* @param clazz
* @return
*/
public static T getObjFromMap(Map map, Class> clazz) {
try {
Object obj = clazz.newInstance();
return getObjFromMap(map, obj);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static String buildSetMethod(String fieldName) {
StringBuffer sb = new StringBuffer("set");
if (fieldName.length() > 1) {
String first = fieldName.substring(0, 1);
String next = fieldName.substring(1);
sb.append(first.toUpperCase()).append(next);
} else {
sb.append(fieldName.toUpperCase());
}
return sb.toString();
}
/**
* 判断某个list是否没有数据
*
* @param
* @param list
* @return
*/
public static boolean isEmpty(List list) {
boolean b = false;
if (list == null || list.isEmpty()) {
b = true;
}
return b;
}
/**
* 判断一个map是否为空
*
* @param map
* @param
* @return
*/
public static boolean isEmpty(Map
回到主题,我们实现一个需要缓存到对象类,例如Item 玩家的道具这种东西。
package com.twjitm.jump.logic.game.item.entity;
import com.twjitm.jump.common.utils.zutils.ZCollectionUtil;
import com.twjitm.jump.logic.core.database.redis.RedisListInterface;
import java.util.Map;
/**
* @author twjitm - [Created on 2018-10-24 18:08]
*/
public class ItemPo implements RedisListInterface {
public ItemPo(){
}
/**
* playerId
*/
private long playerId;
/**
*
*/
private long id;
/**
* item id
*/
private int sdId;
/**
* 类型
*/
private int type;
/**
* 品质
*/
private int quality;
/**
* 等级
*/
private int level;
/**
* 数量
*/
private int count;
public ItemPo(long playerId, int id, int sdId, int type, int quality, int level, int count) {
this.playerId = playerId;
this.id = id;
this.sdId = sdId;
this.type = type;
this.quality = quality;
this.level = level;
this.count = count;
}
public long getPlayerId() {
return playerId;
}
public void setPlayerId(long playerId) {
this.playerId = playerId;
}
public int getSdId() {
return sdId;
}
public void setSdId(int sdId) {
this.sdId = sdId;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public int getQuality() {
return quality;
}
public void setQuality(int quality) {
this.quality = quality;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
@Override
public String[] getSubUniqueKey() {
return new String[]{"id", "sdId"};
}
@Override
public Map getAllFeildsToHash() {
return ZCollectionUtil.getAllFeildsToHash(this);
}
@Override
public String getUniqueKey() {
return "playerId";
}
@Override
public int getFieldLength() {
return 0;
}
}
实现前面提到的RedisListInterface接口中定义的方法。例如使用playerid来作为标示字段,利用id和sdid组合来标示一条一条数据的唯一表示。
在redis提供的Java api接口文档中。我们进行扩方法:
/**
* 通过反射从缓存里获取一个对象 缺省默认时间,默认的key是有uid这个字段拼接而成
*
* @param
* @param key
* @param clazz
* @return
*/
@SuppressWarnings("unchecked")
public T getObjectFromHash(String key, Class> clazz) {
return (T) getObjectFromHash(key, clazz, RedisKey.NORMAL_LIFECYCLE);
}
/**
* 通过反射从缓存里获取一个对象 缺省默认时间
*
* @param
* @param key
* @param clazz
* @return
*/
@SuppressWarnings("unchecked")
public T getObjectFromHash(String key, Class> clazz, String uniqueKey) {
return (T) getObjectFromHash(key, clazz, uniqueKey, RedisKey.NORMAL_LIFECYCLE);
}
/*
* 通过反射从缓存里获取一个对象,默认的key是有uid这个字段拼接而成
* @param key
* @param clazz
* @return
*/
@SuppressWarnings("unchecked")
public T getObjectFromHash(String key, Class> clazz, int second) {
return (T) getObjectFromHash(key, clazz, "uid", second);
}
/*
* 通过反射从缓存里获取一个对象
* @param key
* @param clazz
* @param uniqueKey 此key由哪个字段拼接而成的
* @return
*/
@SuppressWarnings("unchecked")
public T getObjectFromHash(String key, Class> clazz, String uniqueKey, int seconds) {
Jedis jedis = null;
boolean sucess = true;
try {
jedis = jedisPool.getResource();
//Transaction t = jedis.multi();
//Response
最后我们使用这些方法就能够对我们定义的对象进行存储和修改了。
源码可参考 https://github.com/twjitm/twjitm-core