并发读写缓存实现机制(零):缓存操作指南


    游戏中为了提高系统运行速度和游戏承载量,使用缓存是一个必要的手段。本文中的缓存是在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和前缀的映射,而这两步主要是由CacheDefiniensPrefixMapping完成。   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 load(String... params) {
        Preconditions.checkArgument(params.length == 1);
        HibernateDao hibernate = ServiceManager.getInstance().getSpringBean("hibernateDao");
        hibernate.setPersistentClass(TmpListMultikey.class);
        String hql = "from TmpListMultikey where id.NPlayerId = ?";
        List all = hibernate.findAll(hql, NumberUtils.toInt(params[0]));

        Map map = new HashMap();
        for (TmpListMultikey tmpListMultikey : all) {
            map.put(tmpListMultikey.getId().getNType(), tmpListMultikey);
        }
        return new MapReference(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 singleRef = ConcurrentCache.getInstance().get(new TmpFhhdCache(50769));
    TmpFhhd fhhd = singleRef.get();
    TmpFhhd fhhd2 = new TmpFhhdCache(50769).ref().get();
    Assert.assertTrue(fhhd == fhhd2);

    // List格式缓存获取
    List list = ConcurrentCache.getInstance().get(new TmpListMultikeyListCache(50705)).get();
    // Map格式缓存获取
    Map 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 singleRef = new TmpFhhdCache(50769).ref();
    TmpFhhd tmpFhhd = singleRef.get();
    if (tmpFhhd == null) {
        // 新增
        tmpFhhd = new TmpFhhd(507691010"");
    } 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 mapRef = new TmpListMultikeyMapCache(1001).ref();
    TmpListMultikey value = mapRef.get(1);
    if (value == null) {
        mapRef.put(1new TmpListMultikey(new TmpListMultikeyPK(10011), 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