并发读写缓存实现机制(零):缓存操作指南
游戏中为了提高系统运行速度和游戏承载量,使用缓存是一个必要的手段。本文中的缓存是在guava缓存的基础上增加了数据的持久化状态和异步同步数据的功能,同时对调用API做了封装,以达到简化操作、屏蔽内部实现的目的。
在介绍缓存的原理之前,为了一些朋友阅读方便,本文先介绍下缓存的API和使用方法,以帮助大家对本缓存有个大概的理解。这篇文章大家简单阅读即可,后面我们会详细介绍缓存的实现细节。 系列文章目录: 并发读写缓存实现机制(零):缓存操作指南 并发读写缓存实现机制(一):为什么ConcurrentHashMap可以这么快? 并发读写缓存实现机制(二):高并发下数据写入与过期 并发读写缓存实现机制(三):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 public TmpListMultikeyMapCache() { } public TmpListMultikeyMapCache(int playerId) { super(playerId); } @Override public MapReference Preconditions.checkArgument(params.length == 1); HibernateDao hibernate.setPersistentClass(TmpListMultikey.class); String hql = "from TmpListMultikey where id.NPlayerId = ?"; List Map for (TmpListMultikey tmpListMultikey : all) { map.put(tmpListMultikey.getId().getNType(), tmpListMultikey); } return new MapReference } } |
1 2 3 4 5 6 7 |
public enum PrefixMappping { $1(TmpFhhdCache.class), $2(TmpListMultikeyListCache.class), $3(TmpListMultikeyMapCache.class); // 部分代码省略 } |
3.缓存的读取
创建好了缓存的定义、对缓存进行了键的映射之后,接下来我们就要看下缓存的使用,大家由清单1可以看到ConcurrentCache是缓存的核心操作类,因此大部分操作最后都是操作在这个类上。在此基础上,为了调用方便,缓存也扩展了一些其他便捷方法来简化调用,请看下面对缓存读取的一些例子: 清单4:缓存的读取
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Test public void getTest() { // Single格式缓存获取 SingleReference TmpFhhd fhhd = singleRef.get(); TmpFhhd fhhd2 = new TmpFhhdCache(50769).ref().get(); Assert.assertTrue(fhhd == fhhd2); // List格式缓存获取 List // Map格式缓存获取 Map } |
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 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(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Test public void update2Test() { MapReference 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)); } |