游戏中为了提高系统运行速度和游戏承载量,使用缓存是一个必要的手段。本文中的缓存是在guava缓存的基础上增加了数据的持久化状态和异步同步数据的功能,同时对调用API做了封装,以达到简化操作、屏蔽内部实现的目的。
在介绍缓存的原理之前,为了一些朋友阅读方便,本文先介绍下缓存的API和使用方法,以帮助大家对本缓存有个大概的理解。这篇文章大家简单阅读即可,后面我们会详细介绍缓存的实现细节。
系列文章目录:
并发读写缓存实现机制(三):API封装和简化
文中缓存最新源码请参考:https://github.com/cm4j/cm4j-all
缓存的操作指南
1.数据结构简介
本文缓存的目的就是为了减少开发的编码量、提高编码的效率,同时为了方便调用,本缓存在对外接口上做了许多封装,内部也提供了一些常用的缓存类型以供使用。在进一步了解使用方法前,我们先来看下缓存的结构图:
清单1:缓存简略结构图
类的功能简介:
ConcurrentCache:核心操作类,大部分业务都是由此类完成
CacheLoader:缓存的加载类
AbsReference:缓存数据封装抽象类,缓存中实际存储的就是此对象,此类提供了一些常用的方法以方便调用者使用,默认提供了增删改查等方法,文中缓存默认提供了3种常用缓存的实现。为什么需要这个类?主要是为了屏蔽缓存的内部状态。
CacheEntry:单个缓存对象或集合缓存中的一个元素,应该与DB的entity一一对应,持久化时需要把它转化为实体entity然后进行持久化操作
CacheDefiniens:缓存的定义抽象类,主要用于定义缓存如何从db加载
PrefixMapping:缓存key与前缀的映射类
缓存的数据流转:
1.使用一个缓存,首先我们需要定义一个缓存,定义缓存是CacheDefiniens实现的功能,它描述了缓存是如何从DB加载的。
2.每个缓存就像我们一样,每个都应该有一个独一无二的名字,名字和具体的缓存是有映射关系的,这个关系就是通过PrefixMapping来维护的。
3.在本系列中,缓存的核心操作都是通过ConcurrentCache实现的,包括了缓存的读取、保存、过期以及持久化等等,当然也包含了对缓存的具体数据AbsReference的操作。
4.缓存的加载是通过CacheLoader实现的,加载之后,每个数据的存在形态就是AbsReference,它可以是single、list、map或者其他自定义结构。
5.AbsReference内部结构允许有一个或多个元素,如果这些元素需要保存DB,则它们必须是CacheEntry的子类,因为缓存就是通过CacheEntry来进行持久化的。
因此大部分情况下缓存的创建,我们只需要扩展CacheDefiniens、修改PrefixMapping类就可以了,详情可参照下面的例子。
3种常见的缓存类型
日常来说,我们最常用到的数据结构就是单个对象、List对象或者Map对象。AbsReference是对缓存数据的一种封装,缓存中存储的数据就是它,其继承结构请看清单2
清单2:默认实现的3种常见的缓存类型
2.缓存的创建
上面提到系统默认提供了3种常见的数据结构,如果我们要使用这3种结构,那仅仅需要两步即可完成:一是定义缓存是如何从DB加载,二是定义缓存key和前缀的映射,而这两步主要是由CacheDefiniens和PrefixMapping完成。
step1:缓存的定义
清单3:map类型的缓存定义
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class TmpListMultikeyMapCache extends CacheDefiniens<MapReference<Integer, TmpListMultikey>> {
public TmpListMultikeyMapCache() { } public TmpListMultikeyMapCache(int playerId) { super(playerId); } @Override public MapReference<Integer, TmpListMultikey> load(String... params) { Preconditions.checkArgument(params.length == 1); HibernateDao<TmpListMultikey, Integer> hibernate = ServiceManager.getInstance().getSpringBean("hibernateDao"); hibernate.setPersistentClass(TmpListMultikey.class); String hql = "from TmpListMultikey where id.NPlayerId = ?"; List<TmpListMultikey> all = hibernate.findAll(hql, NumberUtils.toInt(params[0])); Map<Integer, TmpListMultikey> map = new HashMap<Integer, TmpListMultikey>(); for (TmpListMultikey tmpListMultikey : all) { map.put(tmpListMultikey.getId().getNType(), tmpListMultikey); } return new MapReference<Integer, TmpListMultikey>(map); } } |
这段代码非常简洁:两个构造函数外加覆盖父类的load方法。其中,根据名称我们知道load()方法就是从DB中加载数据,空参的构造函数是创建描述类使用,非空构造函数则是传递参数的需要。
为了代码生成的便捷,CacheDefiniens采用了范型来规范代码结构。内部实现中,有参构造函数将参数拼为字符串,在需要从DB加载时会再把字符串切分为字符串数组,然后作为参数调用load方法,因此load的params参数和有参构造函数中的参数其实是一致的。
注意19行返回的就是缓存的封装类,构造函数参数就是从DB中查询出来的map结果;而TmpListMultikey则是CacheEntry的一个子类,它是map集合的一个元素,同时提供了parseEntity()方法将对象转化Entity保存到DB中。
step2:缓存的映射
清单4:缓存定义与前缀的映射
1
2 3 4 5 6 7 |
public enum PrefixMappping {
$1(TmpFhhdCache.class), $2(TmpListMultikeyListCache.class), $3(TmpListMultikeyMapCache.class); // 部分代码省略 } |
上面这段就更简单了,一个枚举类,一个键一个缓存描述类,非常简单。
至此,我们就完成了缓存的创建,仅仅必须的两步操作我们就拥有了对缓存的增删改查权限,没有复杂的设定和配置、无需关注内部实现和异步写入DB,内部实现机制已经屏蔽了所有不相关的代码和步骤。
3.缓存的读取
创建好了缓存的定义、对缓存进行了键的映射之后,接下来我们就要看下缓存的使用,大家由清单1可以看到ConcurrentCache是缓存的核心操作类,因此大部分操作最后都是操作在这个类上。在此基础上,为了调用方便,缓存也扩展了一些其他便捷方法来简化调用,请看下面对缓存读取的一些例子:
清单4:缓存的读取
1
2 3 4 5 6 7 8 9 10 11 12 13 |
@Test
public void getTest() { // Single格式缓存获取 SingleReference<TmpFhhd> singleRef = ConcurrentCache.getInstance().get(new TmpFhhdCache(50769)); TmpFhhd fhhd = singleRef.get(); TmpFhhd fhhd2 = new TmpFhhdCache(50769).ref().get(); Assert.assertTrue(fhhd == fhhd2); // List格式缓存获取 List<TmpListMultikey> list = ConcurrentCache.getInstance().get(new TmpListMultikeyListCache(50705)).get(); // Map格式缓存获取 Map<Integer, TmpListMultikey> map = new TmpListMultikeyMapCache(1001).ref().get(); } |
由上面的例子,我们可以看到,不管是那种类型的缓存,我们都有两种方式获取:
1.ConcurrentCache.getInstance().get(new TmpFhhdCache(50769))
2.new TmpFhhdCache(50769).ref()
上面的new TmpFhhdCache(50769)就是我们前面的缓存的定义类,这两种方式都能获取到AbsReference,也就是缓存中实际存储的数据,后面可以使用这个对象来对缓存进行增删改查操作。
4.缓存的增删改查
对于增删改查,缓存更多的依赖于AbsReference类。一方面,缓存读取直接获取的就是这个封装类;另一方面,这个类也屏蔽了ConcurrentCache和缓存状态控制,减少调用者出错的概率。
清单5:缓存的增删改查I
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@Test
public void updateTest() { SingleReference<TmpFhhd> singleRef = new TmpFhhdCache(50769).ref(); TmpFhhd tmpFhhd = singleRef.get(); if (tmpFhhd == null) { // 新增 tmpFhhd = new TmpFhhd(50769, 10, 10, ""); } else { // 修改 tmpFhhd.setNCurToken(10); } // 新增或修改都可以调用update singleRef.update(tmpFhhd); Assert.assertTrue(new TmpFhhdCache(50769).ref().get().getNCurToken() == 10); // 删除 singleRef.delete(); Assert.assertNull(new TmpFhhdCache(50769).ref().get()); // 立即保存缓存到DB singleRef.persist(); } |
对于已经存在于缓存中的对象,我们可以直接调用update()进行修改,也可以直接调用delete()进行删除
这样如果直接从缓存中拿到对象,如果对象存在,可直接修改或删除,而无需AbsReference的介入
清单6:缓存的增删改查II
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Test
public void update2Test() { MapReference<Integer, TmpListMultikey> mapRef = new TmpListMultikeyMapCache(1001).ref(); TmpListMultikey value = mapRef.get(1); if (value == null) { mapRef.put(1, new TmpListMultikey(new TmpListMultikeyPK(1001, 1), 99)); } TmpListMultikey newValue = new TmpListMultikeyMapCache(1001).ref().get(1); newValue.setNValue(2); // 对于已经存在于缓存中的对象 // 我们可以直接调用update()进行修改 newValue.update(); Assert.assertTrue(new TmpListMultikeyMapCache(1001).ref().get(1).getNValue() == 2); // 也可以直接调用delete()进行删除 newValue.delete(); Assert.assertNull(new TmpListMultikeyMapCache(1001).ref().get(1)); } |
5.缓存的扩展
上面的几个例子,我们演示了常用的缓存的使用方法,一般来说已基本可以满足大部分需求,但是需求总是无止境的,在无法满足的情况下,我们就需要对现有系统进行扩展,本缓基于基本框架提供了部分扩展点。
首先,我们最常遇到的就是业务需要更复杂的数据类型,现有缓存提供简单的single、list或map已经无法满足业务需求,这时只要继承AbsReference类,实现其内部业务即可。
其次,如果需要的缓存类型恰巧是single、list或map,同时又需要增加些额外功能,那只要继承对应的类扩展功能就可以了。
大部分情况下,我们可把DB的entity直接设为CacheEntry的子类,这样代码量比较少,而且entity可直接生成。但某些情况,我们需要比Entity更多的属性,也就是我们需要单独的POJO来存储缓存,这时候我们也可以新建POJO来继承CacheEntry
本文简单介绍了缓存的结构及几种常用方法,接下来几章我会分别从读取、写入、数据过期和异步写入等几个方面来介绍缓存的内部实现,敬请期待。
原创文章,请注明引用来源:CM4J