手游服务端框架之使用Guava构建缓存系统
缓存的作用与应用场景
缓存,在项目中的应用非常之广泛。诸如这样的场景,某些对象计算或者获取的代码比较昂贵,并且在程序里你不止一次要用到这些对象,那么,你就应该使用缓存。
缓存跟java的CoucurrentMap很类似,但青出于蓝胜于蓝。CoucurrentMap的特点是,当你往它里面放元素的时候,你需要自己手动去把它移除。而缓存的最大特点是,你无须手动去移除缓存里的元素,而是通过某些移除策略,如果超时或者内存空间紧张等等。
本文主要使用Google的guava工具库来构建我们的缓存系统。
首先说一下我们的缓存系统需要达到的两个目标。
第一,在获取某个对象时,如果对象已在缓存里则直接返回;否则,自动从数据库读取并加入到缓存,并返回给用户接口。
第二,当对象长时间没有被查询命中的话,自己将对象从缓存里移除。
缓存的实现
好,开始我们的编码......
1.定义抽象缓存容器(CacheContainer.java)
/** * 缓存容器 * @author kingston */ public abstract class CacheContainer<K, V> { private LoadingCache<K, V> cache; public CacheContainer(CacheOptions p) { cache = CacheBuilder.newBuilder() .initialCapacity(p.initialCapacity) .maximumSize(p.maximumSize) //超时自动删除 .expireAfterAccess(p.expireAfterAccessSeconds, TimeUnit.SECONDS) .expireAfterWrite(p.expireAfterWriteSeconds, TimeUnit.SECONDS) .removalListener(new MyRemovalListener()) .build(new DataLoader()); } public final V get(K k) { try { return cache.get(k); } catch (ExecutionException e) { LoggerUtils.error("CacheContainer get error", e); throw new UncheckedExecutionException(e); } } public abstract V loadOnce(K k) throws Exception; public final void put(K k, V v) { cache.put(k, v); } public final void remove(K k) { cache.invalidate(k); } public final ConcurrentMap<K, V> asMap() { return cache.asMap(); } class DataLoader extends CacheLoader<K, V> { @Override public V load(K key) throws Exception { return loadOnce(key); } } class MyRemovalListener implements RemovalListener<K, V> { @Override public void onRemoval(RemovalNotification<K, V> notification) { //logger } } }
这里需要特别说明一下,CacheLoader类表示,当我们从缓存里拿不到对象时,应该从哪里获取。这里,我们覆写了load(K key)方法,并让它去调用缓存容器的loadOnce()抽象方法。怎么获取,我们交给子类去完成吧。
2. 在我们的系统里,缓存所存储的对象都是可以进行持久化的,而持久化的对象一般至少要提供两个接口,一个用于从数据库里读取,一个用于保存到数据库。但由于我们的对象持久化,并不打算放在缓存里处理,而是通过单独的线程进行入库(见上一篇文章)。这里,我们定义一下缓存的对象基本接口(Persistable.java)。
/** * 可持久化的 * @author kingston */ public interface Persistable<K, V> { /** * 能从数据库获取bean * @param k 查询主键 * @return 持久化对象 * @throws Exception */ V load(K k) throws Exception; // /** // * 将对象序列号到数据库 // * @param k // * @param v // * @throws PersistenceException // */ // void save(K k, V v) throws Exception; }
3.抽象缓存容器的一个默认实现,拿不到缓存的读取策略采用上面的Persistable方案
/** * 可持久化的 * @author kingston */ public interface Persistable<K, V> { /** * 能从数据库获取bean * @param k 查询主键 * @return 持久化对象 * @throws Exception */ V load(K k) throws Exception; // /** // * 将对象序列号到数据库 // * @param k // * @param v // * @throws PersistenceException // */ // void save(K k, V v) throws Exception; }
4. 定义抽象缓存服务(CacheService.java)。按理说,缓存系统只需要提供一个获取元素的get(key)方法即可。不过,为了能适应一些奇怪的情形,我们还是可以加入手动添加元素的put()方法,还有手动删除缓存的remove()方法。
/** * 抽象缓存服务 * @author kingston */ public abstract class CacheService<K, V> implements Persistable<K, V> { private final CacheContainer<K, V> container; public CacheService() { this(CacheOptions.defaultCacheOptions()); } public CacheService(CacheOptions p) { container = new DefaultCacheContainer<>(this, p); } /** * 通过key获取对象 * @param key * @return */ public V get(K key) { return container.get(key); } /** * 手动移除缓存 * @param key * @return */ public void remove(K key) { container.remove(key); } /** * 手动加入缓存 * @param key * @return */ public void put(K key, V v) { this.container.put(key, v); } }
5.配置类(CacheOptions.java)只是对缓存的一些配置的封闭,没啥好说的,直接上代码吧。
/** * 缓存相关配置 * @author kingston */ public class CacheOptions { private final static int DEFAULT_INITIAL_CAPACITY = 1024; private final static int DEFAULT_MAXIMUM_SIZE = 65536; private final static int DEFAULT_EXPIRE_AFTER_ACCESS_SECONDS = (int)(5*TimeUtils.ONE_HOUR/TimeUtils.ONE_MILLSECOND); private final static int DEFAULT_EXPIRE_AFTER_WRITE_SECONDS = (int)(5*TimeUtils.ONE_HOUR/TimeUtils.ONE_MILLSECOND); public final int initialCapacity; public final int maximumSize; public final int expireAfterAccessSeconds; public final int expireAfterWriteSeconds; private CacheOptions(int initialCapacity, int maximumSize, int expireAfterAccessSeconds, int expireAfterWriteSeconds) { this.initialCapacity = initialCapacity; this.maximumSize = maximumSize; this.expireAfterAccessSeconds = expireAfterAccessSeconds; this.expireAfterWriteSeconds = expireAfterWriteSeconds; } public static CacheOptions defaultCacheOptions() { return new Builder().build(); } static class Builder { private int initialCapacity; private int maximumSize; private int expireAfterAccessSeconds; private int expireAfterWriteSeconds; private Builder() { } public Builder setInitialCapacity(int initialCapacity) { this.initialCapacity = initialCapacity; return this; } public Builder setMaximumSize(int maximumSize) { this.maximumSize = maximumSize; return this; } public Builder setExpireAfterAccessSeconds(int expireAfterAccessSeconds) { this.expireAfterAccessSeconds = expireAfterAccessSeconds; return this; } public Builder setExpireAfterWriteSeconds(int expireAfterWriteSeconds) { this.expireAfterWriteSeconds = expireAfterWriteSeconds; return this; } private CacheOptions build() { if (initialCapacity == 0) { setInitialCapacity(DEFAULT_INITIAL_CAPACITY); } if (maximumSize == 0) { setMaximumSize(DEFAULT_MAXIMUM_SIZE); } if(expireAfterAccessSeconds == 0) { setExpireAfterAccessSeconds(DEFAULT_EXPIRE_AFTER_ACCESS_SECONDS); } if(expireAfterWriteSeconds == 0) { setExpireAfterWriteSeconds(DEFAULT_EXPIRE_AFTER_WRITE_SECONDS); } return new CacheOptions(initialCapacity, maximumSize, expireAfterAccessSeconds, expireAfterWriteSeconds); } } }
业务逻辑使用缓存系统
工具框架搭起来了,来点业务代码吧
玩家管理,最直接的应用场景。我们通过id来查找玩家的时候,策略肯定是这样的,如果玩家已经登录了,那么一定能在内存里找到,否则,就去数据库捞角色。
所以我们的PlayerManager类就可以继承抽象缓存服务CacheService啦。泛型里的key就是玩家的主键playerId, value就是玩家对象了。
public class PlayerManager extends CacheService<Long, Player> { /** * 从用户表里读取玩家数据 */ @Override public Player load(Long playerId) throws Exception { String sql = "SELECT * FROM Player where Id = {0} "; sql = MessageFormat.format(sql, String.valueOf(playerId)); Player player = DbUtils.queryOne(DbUtils.DB_USER, sql, Player.class); return player; } }
测试缓存
/** * 测试玩家缓存系统 * @author kingston */ public class TestPlayerCache { @Before public void init() { //初始化orm框架 OrmProcessor.INSTANCE.initOrmBridges(); //初始化数据库连接池 DbUtils.init(); } @Test public void testQueryPlayer() { long playerId = 10000L; //预先保证用户数据表playerId = 10000的数据存在 Player player = PlayerManager.getInstance().get(playerId); //改变内存里的玩家名称 player.setName("newPlayerName"); //内存里玩家的新名称 String playerName = player.getName(); //通过同一个id再次获取玩家数据 Player player2 = PlayerManager.getInstance().get(playerId); //验证新的玩家就是内存里的玩家,因为如果又是从数据库里读取,那么名称肯定跟内存的不同!! assertTrue(playerName.equals(player2.getName())); } }
手游服务端框架之使用Guava构建缓存系统的更多相关文章
- 基于Lua的游戏服务端框架简介
基于Lua的游戏服务端框架简介 [转]https://gameinstitute.qq.com/community/detail/106396 基于lua的游戏服务端框架简介 1. 引言 笔者目前在参 ...
- Go游戏服务端框架从零搭建(一)— 架构设计
五邑隐侠,本名关健昌,10年游戏生涯,现隐居海边. 本教程以Go语言分区游戏服务端框架搭建为例. Go语言是Google开发的一种静态强类型.编译型.并发型.具有垃圾回收功能的编程语言.语法上近似C语 ...
- 转:云风skynet服务端框架研究
转: http://forthxu.com/blog/skynet.html skynet是云风编写的服务端底层管理框架,底层由C编写,配套lua作为脚本使用,可换python等其他脚本语言.sky ...
- DNF手游公测或将只有安卓版 iOS系统怎么办?
DNF手游在8月10号确定延期后,目前还不知道新的上线时间.玩家都很关心DNF手游新的公测时间,DNF手游官网的预约数据也是不断突破新高,最终突破了五千万!我们目前拿到的小道消息,DNF手游会在9月1 ...
- Node.js服务端框架谁才是你的真爱
1. Express 背景: Express, 疯一般快速(而简洁)的服务端JavaScript Web开发框架,基于Node.js和V8 JavaScript引擎. Express 是一个基于 No ...
- 《Python》网络编程之客户端/服务端框架、套接字(socket)初使用
一.软件开发的机构 我们了解的涉及到两个程序之间通讯的应用大致可以分为两种: 第一种是应用类:QQ.微信.网盘等这一类是属于需要安装的桌面应用 第二种是web类:比如百度.知乎.博客园等使用浏览器访问 ...
- 手游服务端框架之GM金手指的设计
玩过单机游戏的朋友,应该对金山游侠这个软件很熟悉把.当初我经常嫌刷怪升级非常辛苦,很多时候都是直接用金山游侠来修改游戏的经验或者等级内存,直接把角色调得很牛逼. 游戏开发也非常需要这些可以修改玩家数据 ...
- 分享一个C++与Python开发的中小型通用游戏服务端框架(跨平台,开源,适合MMORPG游戏)
在开发一款游戏项目时,在立项时我们往往会考虑或者纠结很多,比如: 1,对于开发来说:服务端和客户端应该选择什么语言?用什么协议通信才更效率?协议后期如何维护?Socket是用长连接还是短连接?TCP还 ...
- 发个招聘贴,魔都求手游C++后端,UNITY前端,开发实习生
上海游旺网络科技有限公司成立于2015年5月,是一家极具潜力的新创移动游戏公司.公司初创团队均来自腾讯,盛大,畅游,墨麟,蜗牛等知名互联网公司,公司创始人团队参与制作过<鬼吹灯><Q ...
随机推荐
- saltstack之nginx、php的配置
saltstack为nginx提供状态配置 1.创建nginx配置需要的目录 mkdir /srv/salt/prod/nginx mkdir /srv/salt/prod/nginx/files 2 ...
- BZOJ3150: [Ctsc2013]猴子
传送门 这题好神啊..好神啊.. 首先得到简单的DP方程: $f_{\{ i \}}=\frac{\sum_{i \ne j} f_ {\{ i,j \}} \times P_{(i,j)}}{N-1 ...
- Codeforces 235C. Cyclical Quest
传送门 写的时候挺蛋疼的. 刚开始的时候思路没跑偏,无非就是建个SAM然后把串开两倍然后在SAM上跑完后统计贡献.但是卡在第二个样例上就是没考虑相同的情况. 然后开始乱搞,发现会出现相同串的只有可能是 ...
- vSphere SDK for Java 示例
示例代码: package com.vmware.event.connect; import java.net.MalformedURLException; import java.net.URL; ...
- Linux命令:chmod、chgrp、chown的区别
chmod是更改文件的权限: chgrp只是更改文件的属组: chown是更改文件的属主与属组. 1.chmod:更改文件的权限 文件权限的设置方式有两种,分别是数字和标记. mode : 权限设定字 ...
- mybatis的sql映射文件—增删改查
前提:需要的包log4j.jar,mybatis-3.4.1.jar,mysql-connector-java-5.1.37-bin.jar 1.基本类 员工类 package com.hand.my ...
- SSH免密码登录Linux
如果两台linux之间交互频繁,但是每次交互如果都需要输入密码,就会很麻烦,通过配置SSH就可以解决这一问题 下面就说下配置流程(下面流程在不同机器上全部操作一边) 1)cd ~到这个目录中 2)ss ...
- 打开PS是出现“该内存不能为read”是怎么回事?
打开PS是出现“该内存不能为read”是怎么回事? 答:内存不能为read修复工具可以有效修复计算机运行应用程序时提示:该内存不能为read要终止程序的问题,一般XP系统才会出现这个问题. 指令修复法 ...
- idea 快捷键及使用技巧
IDEA中经常使用的快捷键: Ctrl+Shift + Enter 语句完成 可以快速在行末添加分号,或添加大括号{} Ctrl+Shift + F 全文查找 需要把搜狗打字的快捷键关掉 Ctrl+A ...
- nodejs中mysql断线重连
之前写了个小程序Node News,用到了MySQL数据库,在本地测试均没神马问题.放上服务器运行一段时间后,偶然发现打开页面的时候页面一直处于等待状态,直到Nginx返回超时错误.于是上服务器检查了 ...