Netty游戏服务器实战开发(15):游戏服务器中的数据缓存

导读

游戏服务器缓存作用非常重要:很大部分决定游戏服务器性能问题。用好缓存能够一定程度上提高服务器性能和响应时间。给游戏带来流畅体验。

缓存的作用

在很多游戏服务器开发过程中,有需要提前加载到内存中的数据,有不需要加载到内存中的数据,当然,加载到内存中的数据可分为字典数据和部分玩家数据。比如:配表信息,玩家查询信息,玩家基础信息等。

通常我们使用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<String, String> 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 <T>
     * @param list
     * @param skip
     * @param pageSize
     */
    public static <T> List<T> getSubListPage(List<T> 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<String, String> getMap(Object obj, String... strings) {
        Map<String, String> map = new HashMap<String, String>();
        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 <T>
     * @return
     */
    public static <T> T getObjFromMap(Map<String, String> 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> T getObjFromMap(Map<String, String> 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 <T>
     * @param list
     * @return
     */
    public static <T> boolean isEmpty(List<T> list) {
        boolean b = false;
        if (list == null || list.isEmpty()) {
            b = true;
        }
        return b;
    }

    /**
     * 判断一个map是否为空
     *
     * @param map
     * @param <T>
     * @return
     */
    public static boolean isEmpty(Map<Object, Object> map) {
        boolean b = false;
        if (map == null || map.isEmpty()) {
            b = true;
        }
        return b;
    }

    /**
     * 反射获取某个类的熟悉名称集合
     *
     * @param obj
     * @return
     */
    public static Map<String, String> getAllFeildsToHash(Object obj) {
        Map<String, String> map = new HashMap<>();
        if (obj != null) {
            Field[] fields = getAllFields(obj);
            for (Field field : fields) {
                field.setAccessible(true);
                map.put(field.getName(), getFieldsValueStr(obj, field.getName()));
            }
        }
        return 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<String, String> 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 <T>
     * @param key
     * @param clazz
     * @return
     */
    @SuppressWarnings("unchecked")
    public <T> T getObjectFromHash(String key, Class<?> clazz) {
        return (T) getObjectFromHash(key, clazz, RedisKey.NORMAL_LIFECYCLE);
    }

    /**
     * 通过反射从缓存里获取一个对象 缺省默认时间
     *
     * @param <T>
     * @param key
     * @param clazz
     * @return
     */

    @SuppressWarnings("unchecked")
    public <T> 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> 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> 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<Map<String,String>> respMap=t.hgetAll(key);
            //t.expire(key, seconds);
            //t.exec();
            //Map<String, String> map = respMap.get();
            Map<String, String> map = jedis.hgetAll(key);
            if (map.size() > 0) {
                Object obj = clazz.newInstance();
                if (obj instanceof RedisInterface && !(obj instanceof RedisListInterface)) {
                    if (map.size() != ((RedisInterface) obj).getFieldLength()) {
                        logger.info("+-+ redis getObjectFromHash:" + clazz.getName() + " expire.hash list size is more than expact. map:" + JSON.toJSONString(map));
                        jedis.expire(key, 0);
                        return null;
                    }
                }
                map.put(uniqueKey, key.split("#")[1]);
                if (seconds >= 0) {
                    jedis.expire(key, seconds);
                }
                return (T) ZCollectionUtil.getObjFromMap(map, obj);
            }
        } catch (Exception e) {
            sucess = false;
            returnBrokenResource(jedis, "getObjectFromHash:" + key, e);
        } finally {
            if (sucess && jedis != null) {
                returnResource(jedis);
            }
        }
        return null;
    }

    /*
     * 通过反射从缓存里获取一个对象
     * 如果redis map少参数 返回默认值
     * @param key
     * @param clazz
     * @param uniqueKey 此key由哪个字段拼接而成的
     * @return
     */
    @SuppressWarnings("unchecked")
    public <T> T getObjectFromHashWithTrue(String key, Class<?> clazz, String uniqueKey, int seconds) {
        Jedis jedis = null;
        boolean sucess = true;
        try {
            jedis = jedisPool.getResource();
            Map<String, String> map = jedis.hgetAll(key);
            if (map.size() > 0) {
                Object obj = clazz.newInstance();
                map.put(uniqueKey, key.split("#")[1]);
                if (seconds >= 0) {
                    jedis.expire(key, seconds);
                }
                return (T) ZCollectionUtil.getObjFromMap(map, obj);
            }
        } catch (Exception e) {
            sucess = false;
            returnBrokenResource(jedis, "getObjectFromHash:" + key, e);
        } finally {
            if (sucess && jedis != null) {
                returnResource(jedis);
            }
        }
        return null;
    }

    /**
     * 将一个列表对象放入缓存
     *
     * @param key
     * @param list
     */

    public void setListToHash(String key, List<? extends RedisListInterface> list) {
        setListToHash(key, list, RedisKey.NORMAL_LIFECYCLE);
    }

    /*
     * 将一个列表对象放入缓存,并设置有效期
     * @param key
     * @param list
     * @param seconds
     */
    public boolean setListToHash(String key, List<? extends RedisListInterface> list, int seconds) {
        Jedis jedis = null;
        boolean sucess = true;
        try {
            //Transaction t = jedis.multi();
            Map<String, String> map = new HashMap<String, String>();
            Map<String, String> keyMap = null;
            String[] keyNames = null;
            for (RedisListInterface po : list) {
                keyNames = po.getSubUniqueKey();
                keyMap = ZCollectionUtil.getMap(po, keyNames);
                StringBuilder sb = new StringBuilder();
                for (String keyName : keyNames) {
                    sb.append(keyMap.get(keyName)).append("#");
                }
                map.put(sb.toString(), GameUtil.getJsonStr(po.getAllFeildsToHash()));
            }
            //t.hmset(key, map);
            //t.expire(key, seconds);
            //t.exec();
            jedis = jedisPool.getResource();
            jedis.hmset(key, map);
            if (seconds >= 0) {
                jedis.expire(key, seconds);
            }
        } catch (Exception e) {
            sucess = false;
            returnBrokenResource(jedis, "setListToHash:" + key, e);
        } finally {
            if (sucess && jedis != null) {
                returnResource(jedis);
            }
        }
        return sucess;
    }

    /**
     * 从缓存里还原一个列表对象
     *
     * @param key
     * @param clazz
     * @return
     */
    @SuppressWarnings("unchecked")

    public <T> List<T> getListFromHash(String key, Class<?> clazz, int seconds) {
        Jedis jedis = null;
        boolean sucess = true;
        Map<String, String> map = null;
        try {
            jedis = jedisPool.getResource();
            map = jedis.hgetAll(key);
            if (map != null && map.size() > 0) {
                List<T> rt = new ArrayList<T>();
                RedisListInterface po = null;
                Map<String, String> mapFields = null;
                String keyNames[] = null;
                for (Entry<String, String> entry : map.entrySet()) {
                    String fieldKey = entry.getKey();
                    mapFields = GameUtil.getMapFromJson(entry.getValue());
                    po = (RedisListInterface) clazz.newInstance();
                    mapFields.put(po.getUniqueKey(), key.split("#")[1]);
                    keyNames = po.getSubUniqueKey();
                    String uniqueKeys[] = fieldKey.split("#");
                    for (int i = 0, j = keyNames.length; i < j; i++) {
                        mapFields.put(keyNames[i], uniqueKeys[i]);
                    }
                    ZCollectionUtil.getObjFromMap(mapFields, po);
                    rt.add((T) po);
                }
                if (seconds >= 0) {
                    jedis.expire(key, seconds);
                }
                return rt;
            }
        } catch (Exception e) {
            sucess = false;
            returnBrokenResource(jedis, "getListFromHash:" + key, e);
        } finally {
            if (sucess && jedis != null) {
                returnResource(jedis);
            }
        }
        return null;
    }

    /**
     * 从缓存里还原一个列表对象
     *
     * @param key
     * @param clazz
     * @return
     */
    @SuppressWarnings("unchecked")

    public <T> T getObjectFromList(String key, String subUnionkey, Class<?> clazz, int seconds) {
        Jedis jedis = null;
        boolean sucess = true;
        Map<String, String> map = null;
        try {
            jedis = jedisPool.getResource();
            String value = jedis.hget(key, subUnionkey);
            if (ZStringUtil.isEmptyStr(value)) {
                return null;
            }
            Map<String, String> mapFields = GameUtil.getMapFromJson(value);
            RedisListInterface po = (RedisListInterface) clazz.newInstance();
            mapFields.put(po.getUniqueKey(), key.split("#")[1]);
            String[] keyNames = po.getSubUniqueKey();
            String uniqueKeys[] = subUnionkey.split("#");
            for (int i = 0, j = keyNames.length; i < j; i++) {
                mapFields.put(keyNames[i], uniqueKeys[i]);
            }
            ZCollectionUtil.getObjFromMap(mapFields, po);
            if (seconds >= 0) {
                jedis.expire(key, seconds);
            }
            return (T) po;
        } catch (Exception e) {
            sucess = false;
            returnBrokenResource(jedis, "getListFromHash:" + key, e);
        } finally {
            if (sucess && jedis != null) {
                returnResource(jedis);
            }
        }
        return null;
    }

    /**
     * 批量删除对象
     *
     * @param key
     * @param list
     */

    public boolean deleteList(String key, List<? extends RedisListInterface> list) {
        Jedis jedis = null;
        boolean sucess = true;
        try {
            String keys[] = new String[list.size()];
            String keyNames[] = null;
            Map<String, String> keyMap = null;
            int index = 0;
            for (RedisListInterface po : list) {
                keyNames = po.getSubUniqueKey();
                keyMap = ZCollectionUtil.getMap(po, keyNames);
                StringBuilder sb = new StringBuilder();
                for (String keyName : keyNames) {
                    sb.append(keyMap.get(keyName)).append("#");
                }
                keys[index++] = sb.toString();
            }
            jedis = jedisPool.getResource();
            jedis.hdel(key, keys);
        } catch (Exception e) {
            sucess = false;
            returnBrokenResource(jedis, "deleteList:" + key, e);
        } finally {
            if (sucess && jedis != null) {
                returnResource(jedis);
            }
        }
        return sucess;
    }

最后我们使用这些方法就能够对我们定义的对象进行存储和修改了。
源码可参考 https://github.com/twjitm/twjitm-core

好一个大布丁 CSDN认证博客专家 Java Redis 分布式
手游服务器研发工程师。有参与射击类型手游《雷霆战机》。MMO《末日危机》,SLG+ARPG《末日生存》开发经验。现在主要研究全球游戏服务器SLG类型游戏开发。研究分布式和微服务在游戏服务器中的应用。
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页