您的位置:首页 > 编程语言 > Java开发

手游服务端框架之使用Guava构建缓存系统

2017-07-23 15:03 447 查看

缓存的作用与应用场景

缓存,在项目中的应用非常之广泛。诸如这样的场景,某些对象计算或者获取的代码比较昂贵,并且在程序里你不止一次要用到这些对象,那么,你就应该使用缓存。
缓存跟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;
}

}

测试缓存

写个简单的JUnit测试类跑一下吧^_^
/**
* 测试玩家缓存系统
* @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()));
}

}

文章预告:下一篇主要介绍GM命令系统的设计。手游服务端开源框架系列完整的代码请移步github ->>game_server
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息