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

17 篇文章 0 订阅
游戏中为了提高系统运行速度和游戏承载量,使用缓存是一个必要的手段。本文中的缓存是在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( 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<Integer, TmpListMultikey> 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
 
    本文简单介绍了缓存的结构及几种常用方法,接下来几章我会分别从读取、写入、数据过期和异步写入等几个方面来介绍缓存的内部实现,敬请期待。

该博文转载自 http://www.cnblogs.com/cm4j/p/cc_0.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值