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


        游戏中为了提高系统运行速度和游戏承载量,使用缓存是一个必要的手段。本文中的缓存是在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和前缀的映射,而这两步主要是由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<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
     
        本文简单介绍了缓存的结构及几种常用方法,接下来几章我会分别从读取、写入、数据过期和异步写入等几个方面来介绍缓存的内部实现,敬请期待。
     
    原创文章,请注明引用来源:CM4J
  • 相关阅读:
    Html页面渲染
    神思SS628(100)型第二代身份证验证阅读机B/S二次开发
    ASP.NET面试问题一天5问(四)
    ASP.NET面试问题一天5问(三)
    asp.net 面试问题一天5问(二)
    ASP.NET面试问题一天5问(一)
    ASP.NET MVC 3 第一天笔记
    2019 END → 2020 BEGIN
    vs中自己常用的快捷方式
    Dapper使用入门Demo
  • 原文地址:https://www.cnblogs.com/cm4j/p/cc_0.html
Copyright © 2020-2023  润新知