zoukankan      html  css  js  c++  java
  • 简单服务端缓存API设计

    Want#

    我们希望设计一套缓存API,适应不同的缓存产品,并且基于Spring框架完美集成应用开发。

    本文旨在针对缓存产品定义一个轻量级的客户端访问框架,目标支持多种缓存产品,面向接口编程,目前支持简单的CRUD。

    引导#

    目前大多数NoSQL产品的Java客户端API都以完全实现某个NoSQL产品的特性而实现,而缓存只是一个feature,如果缓存API只针对缓存这一个feature,那么它能否可以定义的更易于使用,API是否能定义的更合理呢?

    即:站在抽象缓存产品设计的角度定义一个API,而不是完整封装NoSQL产品的客户端访问API

    缓存产品定义#

    以Memcached、Redis、MongoDB三类产品为例,后两者可不止缓存这样的一个feature:

    • Memcached:纯粹的分布式缓存产品,支持简单kv存储结构,优势在于内存利用率高
    • Redis:优秀的分布式缓存产品,支持多种存储结构(set,list,map),优势在于数据持久化和性能,不过还兼顾轻量级消息队列这样的私活
    • MongoDB:远不止缓存这一点feature,文档型的数据库,支持类SQL语法,性能据官网介绍很不错(3.x版本使用了新的存储引擎)

    也许有人会说,为什么把MongoDB也作为缓存产品的一种选型呢?

    广义上讲,内存中的一个Map结构就可以成为一个缓存了,因此MongoDB这种文档型的NoSQL数据库更不用说了。

    以百度百科对缓存的解释,适当补充

    • 定义:数据交换的缓冲区
    • 目标:提高数据读取命中率,减少直接访问底层存储介质
    • 特性:缓存数据持久化,读写同步控制,缓存数据过期,异步读写等等

    仅仅以缓存定义来看,任何存取数据性能高于底层介质的存储结构都可以作为缓存

    缓存应用场景#

    • db数据缓冲池,常见的orm框架比如Mybatis、Hibernate都支持缓存结构设计,并支持以常见缓存产品redis,memcached等作为底层存储。
    • 缓存业务逻辑状态,比如一段业务逻辑执行比较复杂并且消耗资源(cpu、内存),可考虑将执行结果缓存,下一次相同请求(请求参数相同)执行数据优先从缓存读取。

    业务逻辑增加缓存处理的样例代码

    // 从缓存中获取数据
    Object result = cacheClient.get(key);
    // 结果为空
    if(result == null) {
        // 执行业务处理
        result = do(...);
        // 存入缓存
        cacheClient.put(key, result);
    }
    // 返回结果
    return result;
    

    缓存API定义#

    我们的目标:尽可能的抽象缓存读写定义,最大限度的兼容各种底层缓存产品的能力(没有蛀牙)

    • 泛型接口,支持任意类型参数与返回
    • 多种存储结构(list,map)
    • 过期,同步异步特性

    存储结构在接口方法维度上扩展
    各类操作特性在Option对象上扩展

    翻译成代码(代码过多、非完整版本):

    基础API定义##

    缓存抽象接口

    package org.wit.ff.cache;
    
    import java.util.List;
    import java.util.Map;
    
    /**
     * Created by F.Fang on 2015/9/23.
     * Version :2015/9/23
     */
    public interface IAppCache {
        /**
         *
         * @param key 键
         * @param <K>
         * @return 目标缓存中是否存在键
         */
        <K> boolean contains(K key);
    
        /**
         *
         * @param key 键
         * @param value 值
         * @param <K>
         * @param <V>
         * @return 存储到目标缓存是否成功
         */
        <K,V> boolean put(K key, V value);
    
        /**
         *
         * @param key 键
         * @param value 值
         * @param option 超时,同步异步控制
         * @param <K>
         * @param <V>
         * @return 存储到目标缓存是否成功
         */
        <K,V> boolean put(K key, V value, Option option);
    
        /**
         *
         * @param key 键
         * @param type 值
         * @param <K>
         * @param <V>
         * @return 返回缓存系统目标键对应的值
         */
        <K,V> V get(K key, Class<V> type);
    
        /**
         *
         * @param key 键
         * @param <K>
         * @return 删除目标缓存键是否成功
         */
        <K> boolean remove(K key);
    }
    
    
    

    缓存可选项

    package org.wit.ff.cache;
    
    /**
     * Created by F.Fang on 2015/9/23.
     * Version :2015/9/23
     */
    public class Option {
    
        /**
         * 超时时间.
         */
        private long expireTime;
    
        /**
         * 超时类型.
         */
        private ExpireType expireType;
    
        /**
         * 调用模式.
         * 异步选项,默认同步(非异步)
         */
        private boolean async;
    
        public Option(){
            // 默认是秒设置.
            expireType = ExpireType.SECONDS;
        }
    
        public long getExpireTime() {
            return expireTime;
        }
    
        public void setExpireTime(long expireTime) {
    
            this.expireTime = expireTime;
        }
    
        public boolean isAsync() {
            return async;
        }
    
        public void setAsync(boolean async) {
            this.async = async;
        }
    
        public ExpireType getExpireType() {
            return expireType;
        }
    
        public void setExpireType(ExpireType expireType) {
            this.expireType = expireType;
        }
    }
    

    过期时间枚举

    package org.wit.ff.cache;
    
    /**
     * Created by F.Fang on 2015/9/18.
     * Version :2015/9/18
     */
    public enum ExpireType {
    
        SECONDS, DATETIME
    
    }
    
    

    序列化接口

    package org.wit.ff.cache;
    
    /**
     * Created by F.Fang on 2015/9/15.
     * Version :2015/9/15
     */
    public interface ISerializer<T> {
        byte[] serialize(T obj);
    
        T deserialize(byte[] bytes, Class<T> type);
    }
    

    默认序列化实现

    package org.wit.ff.cache.impl;
    
    import org.springframework.util.SerializationUtils;
    import org.wit.ff.cache.ISerializer;
    
    /**
     * Created by F.Fang on 2015/9/15.
     * Version :2015/9/15
     */
    public class DefaultSerializer<T> implements ISerializer<T>{
    
        @Override
        public byte[] serialize(T obj) {
            return SerializationUtils.serialize(obj);
        }
    
        @Override
        public T deserialize(byte[] bytes, Class<T> type) {
            return (T)SerializationUtils.deserialize(bytes);
        }
    }
    
    

    基于Redis的实现#

    • 基于Jedis客户端API的封装
    • 支持自定义序列化
    • 底层与redis交互的数据类型均为bytes

    缓存API实现##

    Jedis缓存API实现

    package org.wit.ff.cache.impl;
    
    
    import org.wit.ff.cache.ExpireType;
    import org.wit.ff.cache.IAppCache;
    import org.wit.ff.cache.ISerializer;
    import org.wit.ff.cache.Option;
    import org.wit.ff.util.ByteUtil;
    import org.wit.ff.util.ClassUtil;
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    /**
     * Created by F.Fang on 2015/9/16.
     * 目前的实现虽然不够严密,但是基本够用.
     * 因为对于put操作,对于目前的业务场景是允许失败的,因为下次执行正常业务逻辑处理时仍然可以重建缓存.
     * Version :2015/9/16
     */
    public class JedisAppCache implements IAppCache {
    
        /**
         * redis连接池.
         */
        private JedisPool pool;
    
        /**
         * 序列化工具.
         */
        private ISerializer serializer;
    
        /**
         * 全局超时选项.
         */
        private Option option;
    
        public JedisAppCache() {
            serializer = new DefaultSerializer();
            option = new Option();
        }
    
        @Override
        public <K> boolean contains(K key) {
            if (key == null) {
                throw new IllegalArgumentException("key can't be null!");
            }
            try (Jedis jedis = pool.getResource()) {
                byte[] kBytes = translateObjToBytes(key);
                return jedis.exists(kBytes);
            }
        }
    
        @Override
        public <K, V> boolean put(K key, V value) {
            return put(key, value, option);
        }
    
        @Override
        public <K, V> boolean put(K key, V value, Option option) {
            if (key == null || value == null) {
                throw new IllegalArgumentException("key,value can't be null!");
            }
            try (Jedis jedis = pool.getResource()) {
                byte[] kBytes = translateObjToBytes(key);
                byte[] vBytes = translateObjToBytes(value);
                // 暂时不考虑状态码的问题, 成功状态码为OK.
                String code = jedis.set(kBytes, vBytes);
                // 如果设置了合法的过期时间才设置超时.
                setExpire(kBytes, option, jedis);
                return "OK".equals(code);
            }
        }
    
        @Override
        public <K, V> V get(K key, Class<V> type) {
            if (key == null || type == null) {
                throw new IllegalArgumentException("key or type can't be null!");
            }
            try (Jedis jedis = pool.getResource()) {
                byte[] kBytes = translateObjToBytes(key);
                byte[] vBytes = jedis.get(kBytes);
                if (vBytes == null) {
                    return null;
                }
                return translateBytesToObj(vBytes, type);
            }
        }
    
        @Override
        public <K> boolean remove(K key) {
            if (key == null) {
                throw new IllegalArgumentException("key can't be null!");
            }
            try (Jedis jedis = pool.getResource()) {
                byte[] kBytes = translateObjToBytes(key);
                // 状态码为0或1(key数量)都可认为是正确的.0表示key原本就不存在.
                jedis.del(kBytes);
                // 暂时不考虑状态码的问题.
                return true;
            }
        }
    
    
        private <T> byte[] translateObjToBytes(T val) {
            byte[] valBytes;
            if (val instanceof String) {
                valBytes = ((String) val).getBytes();
            } else {
                Class<?> classType = ClassUtil.getWrapperClassType(val.getClass().getSimpleName());
                if (classType != null) {
                    // 如果是基本类型. Boolean,Void不可能会出现在参数传值类型的位置.
                    if (classType.equals(Integer.TYPE)) {
                        valBytes = ByteUtil.intToByte4((Integer) val);
                    } else if (classType.equals(Character.TYPE)) {
                        valBytes = ByteUtil.charToByte2((Character) val);
                    } else if (classType.equals(Long.TYPE)) {
                        valBytes = ByteUtil.longToByte8((Long) val);
                    } else if (classType.equals(Double.TYPE)) {
                        valBytes = ByteUtil.doubleToByte8((Double) val);
                    } else if (classType.equals(Float.TYPE)) {
                        valBytes = ByteUtil.floatToByte4((Float) val);
                    } else if(val instanceof byte[]) {
                        valBytes = (byte[])val;
                    } else {
                        throw new IllegalArgumentException("unsupported value type, classType is:" + classType);
                    }
                } else {
                    // 其它均采用序列化
                    valBytes = serializer.serialize(val);
                }
            }
            return valBytes;
        }
    
        private <T> T translateBytesToObj(byte[] bytes, Class<T> type) {
            Object obj;
            if (type.equals(String.class)) {
                obj = new String(bytes);
            } else {
                Class<?> classType = ClassUtil.getWrapperClassType(type.getSimpleName());
                if (classType != null) {
                    // 如果是基本类型. Boolean,Void不可能会出现在参数传值类型的位置.
                    if (classType.equals(Integer.TYPE)) {
                        obj = ByteUtil.byte4ToInt(bytes);
                    } else if (classType.equals(Character.TYPE)) {
                        obj = ByteUtil.byte2ToChar(bytes);
                    } else if (classType.equals(Long.TYPE)) {
                        obj = ByteUtil.byte8ToLong(bytes);
                    } else if (classType.equals(Double.TYPE)) {
                        obj = ByteUtil.byte8ToDouble(bytes);
                    } else if (classType.equals(Float.TYPE)) {
                        obj = ByteUtil.byte4ToFloat(bytes);
                    } else {
                        throw new IllegalArgumentException("unsupported value type, classType is:" + classType);
                    }
                } else {
                    // 其它均采用序列化
                    obj = serializer.deserialize(bytes,type);
                }
            }
            return (T) obj;
        }
    
        private void setExpire(byte[] kBytes,Option option, Jedis jedis) {
            if (option.getExpireType().equals(ExpireType.SECONDS)) {
                int seconds = (int)option.getExpireTime()/1000;
                if(seconds > 0){
                    jedis.expire(kBytes, seconds);
                }
            } else {
                jedis.expireAt(kBytes, option.getExpireTime());
            }
        }
    
        public void setPool(JedisPool pool) {
            this.pool = pool;
        }
    
        public void setSerializer(ISerializer serializer) {
            this.serializer = serializer;
        }
    
        public void setOption(Option option) {
            this.option = option;
        }
    }
    
    

    Spring配置文件(spring-redis.xml)

    <context:property-placeholder location="redis.properties"/>
    
        <!-- JedisPool -->
        <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
            <property name="maxTotal" value="4" />
            <property name="maxIdle" value="2" />
            <property name="maxWaitMillis" value="10000" />
            <property name="testOnBorrow" value="true" />
        </bean>
    
        <bean id="jedisPool" class="redis.clients.jedis.JedisPool" destroy-method="destroy">
            <constructor-arg index="0" ref="jedisPoolConfig" />
            <constructor-arg index="1" value="${redis.host}" />
            <constructor-arg index="2" value="${redis.port}" />
            <constructor-arg index="3" value="10000" />
            <constructor-arg index="4" value="${redis.password}" />
            <constructor-arg index="5" value="0" />
        </bean>
    
        <bean id="jedisAppCache" class="org.wit.ff.cache.impl.JedisAppCache" >
            <property name="pool" ref="jedisPool" />
        </bean>
    
    

    Redis配置文件

    redis.host=192.168.21.125
    redis.port=6379
    redis.password=xxx
    

    基于memcached实现#

    • 基于Xmemcached API实现
    • 自定义序列化,byte数组类型,默认Xmemcached不执行序列化

    缓存API实现##

    Xmemcached缓存API实现

    package org.wit.ff.cache.impl;
    
    import net.rubyeye.xmemcached.MemcachedClient;
    import net.rubyeye.xmemcached.exception.MemcachedException;
    import org.wit.ff.cache.AppCacheException;
    import org.wit.ff.cache.ExpireType;
    import org.wit.ff.cache.IAppCache;
    import org.wit.ff.cache.Option;
    
    import java.util.List;
    import java.util.Map;
    import java.util.concurrent.TimeoutException;
    
    /**
     * Created by F.Fang on 2015/9/24.
     * 基于xmemcached.
     * Version :2015/9/24
     */
    public class XMemAppCache implements IAppCache {
    
        /**
         * memcached客户端.
         */
        private MemcachedClient client;
    
        /**
         * 选项.
         */
        private Option option;
    
        public XMemAppCache(){
            option = new Option();
        }
    
        @Override
        public <K> boolean contains(K key) {
            String strKey = translateToStr(key);
            try {
                return client.get(strKey) != null;
            } catch (InterruptedException | MemcachedException |TimeoutException e){
                throw new AppCacheException(e);
            }
        }
    
        @Override
        public <K, V> boolean put(K key, V value) {
            return put(key,value,option);
        }
    
        @Override
        public <K, V> boolean put(K key, V value, Option option) {
            if(option.getExpireType().equals(ExpireType.DATETIME)){
                throw new UnsupportedOperationException("memcached no support ExpireType(DATETIME) !");
            }
            // 目前考虑 set, add方法如果key已存在会发生异常.
            // 当前对缓存均不考虑更新操作.
            int seconds = (int)option.getExpireTime()/1000;
            String strKey = translateToStr(key);
            try {
                if(option.isAsync()){
                    // 异步操作.
                    client.setWithNoReply(strKey, seconds, value);
                    return true;
                } else {
                    return client.set(strKey, seconds, value);
                }
    
            } catch (InterruptedException | MemcachedException |TimeoutException e){
                throw new AppCacheException(e);
            }
        }
    
        @Override
        public <K, V> V get(K key, Class<V> type) {
            String strKey = translateToStr(key);
            try {
                return client.get(strKey);
            } catch (InterruptedException | MemcachedException |TimeoutException e){
                throw new AppCacheException(e);
            }
        }
    
        @Override
        public <K> boolean remove(K key) {
            String strKey = translateToStr(key);
            try {
                return client.delete(strKey);
            } catch (InterruptedException | MemcachedException |TimeoutException e){
                throw new AppCacheException(e);
            }
        }
    
        private <K> String translateToStr(K key) {
            if(key instanceof String){
                return (String)key;
            }
            return key.toString();
        }
    
        public void setClient(MemcachedClient client) {
            this.client = client;
        }
    
        public void setOption(Option option) {
            this.option = option;
        }
    }
    
    

    Spring配置文件(spring-memcached.xml)

     <context:property-placeholder location="memcached.properties"/>
    
        <bean
                id="memcachedClientBuilder"
                class="net.rubyeye.xmemcached.XMemcachedClientBuilder"
                p:connectionPoolSize="${memcached.connectionPoolSize}"
                p:failureMode="${memcached.failureMode}">
            <!-- XMemcachedClientBuilder have two arguments.First is server list,and
                second is weights array. -->
            <constructor-arg>
                <list>
                    <bean class="java.net.InetSocketAddress">
                        <constructor-arg>
                            <value>${memcached.server1.host}</value>
                        </constructor-arg>
                        <constructor-arg>
                            <value>${memcached.server1.port}</value>
                        </constructor-arg>
                    </bean>
    
                </list>
            </constructor-arg>
            <constructor-arg>
                <list>
                    <value>${memcached.server1.weight}</value>
                </list>
            </constructor-arg>
            <property name="commandFactory">
                <bean class="net.rubyeye.xmemcached.command.TextCommandFactory"/>
            </property>
            <property name="sessionLocator">
                <bean class="net.rubyeye.xmemcached.impl.KetamaMemcachedSessionLocator"/>
            </property>
            <property name="transcoder">
                <bean class="net.rubyeye.xmemcached.transcoders.SerializingTranscoder"/>
            </property>
        </bean>
        <!-- Use factory bean to build memcached client -->
        <bean
                id="memcachedClient"
                factory-bean="memcachedClientBuilder"
                factory-method="build"
                destroy-method="shutdown"/>
    
        <bean id="xmemAppCache" class="org.wit.ff.cache.impl.XMemAppCache" >
            <property name="client" ref="memcachedClient" />
        </bean>
    

    memcached.properties

    #连接池大小即客户端个数
    memcached.connectionPoolSize=3
    memcached.failureMode=true
    #server1
    memcached.server1.host=xxxx
    memcached.server1.port=21212
    memcached.server1.weight=1
    

    测试#

    示例测试代码:

    package org.wit.ff.cache;
    
    import org.junit.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
    import tmodel.User;
    
    import java.util.concurrent.TimeUnit;
    
    import static org.junit.Assert.assertEquals;
    
    /**
     * Created by F.Fang on 2015/10/19.
     * Version :2015/10/19
     */
    @ContextConfiguration("classpath:spring-redis.xml")
    public class AppCacheTest extends AbstractJUnit4SpringContextTests {
    
        @Autowired
        private IAppCache appCache;
    
        @Test
        public void demo() throws Exception{
            User user = new User(1, "ff", "ff@adchina.com");
            appCache.put("ff", user);
            TimeUnit.SECONDS.sleep(3);
            User result = appCache.get("ff",User.class);
            assertEquals(user, result);
        }
    
    }
    

    小结&展望#

    注:Redis支持支持集合(list,map)存储结构,Memecached则不支持,因此可以考虑在基于Memcached缓存访问API实现中的putList(...)方法直接抛出UnsupportedOperationException异常

    • 支持集合操作(目前Redis版本实际已经实现)
    • 支持更简易的配置
    • 补充对MongoDB的支持

    QA##

  • 相关阅读:
    sitemap
    sitemap
    sitemap
    微信开发 :WeixinPayInfoCollection尚未注册Mch 问题解决
    微信开发 :WeixinPayInfoCollection尚未注册Mch 问题解决
    微信开发 :WeixinPayInfoCollection尚未注册Mch 问题解决
    微信开发 :WeixinPayInfoCollection尚未注册Mch 问题解决
    HTML5 & CSS3初学者指南(2) – 样式化第一个网页
    HTML5 & CSS3初学者指南(2) – 样式化第一个网页
    django 登陆
  • 原文地址:https://www.cnblogs.com/fangfan/p/4891926.html
Copyright © 2011-2022 走看看