简单服务端缓存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##
简单服务端缓存API设计的更多相关文章
- 前端用node+mysql实现简单服务端
node express + mysql实现简单服务端前端新人想写服务端不想学PHP等后端语言怎么办,那就用js写后台吧!这也是我这个前端新人的学习成果分享,如有那些地方不对,请给我指出. 1.准备工 ...
- 前端使用node.js+express+mockjs+mysql实现简单服务端,2种方式模拟数据返回
今天,我教大家来搭建一个简单服务端 参考文章: https://www.jianshu.com/p/cb89d9ac635e https://www.cnblogs.com/jj-notes/p/66 ...
- 服务端用例设计的思(tao)路!
服务端的测试简单来说就是除了前端以外的的测试. 总的来说可以分为以下两类: 1. WEB或者APP的提供业务逻辑的服务端接口测试 2. 数据库.缓存系统.中间件..jar包依赖.输入输 ...
- 基于 IOCP 的通用异步 Windows Socket TCP 高性能服务端组件的设计与实现
设计概述 服务端通信组件的设计是一项非常严谨的工作,其中性能.伸缩性和稳定性是必须考虑的硬性质量指标,若要把组件设计为通用组件提供给多种已知或未知的上层应用使用,则设计的难度更会大大增加,通用性.可用 ...
- vivo 服务端监控架构设计与实践
一.业务背景 当今时代处在信息大爆发的时代,信息借助互联网的潮流在全球自由的流动,产生了各式各样的平台系统和软件系统,越来越多的业务也会导致系统的复杂性. 当核心业务出现了问题影响用户体验,开发人员没 ...
- 推荐:让你快速搞定各服务端(api,pc,mobile,wechat)代码
如果你在写服务端 (PHP) ,会因为项目须求(做app.pc.mobiel.微信) 而写几套代码的,你不觉得很累吗? 现在的很多开源框架商用版本在做程序方面都是这么一套一套的,维护起来,二开起来特别 ...
- nuxt.js服务端缓存lru-cache
对于部分网页进行服务端的缓存,可以获得更好的渲染性能,但是缓存又涉及到一个数据的及时性的问题,所以在及时性和性能之间要有平衡和取舍. 官方文档里面写的使用方法 按照这个配置,试过了没什么用,但是从文档 ...
- 服务端缓存HttpRuntime.Cache的使用
HttpRuntime.Cache.Insert("缓存key", "缓存content", null, DateTime.Now.AddMinutes(3), ...
- git分布式的理解----简单服务端搭建
Git是分布式的,并没有服务端跟客户端之分,所谓的服务端安装的其实也是git.Git支持四种协议,file,ssh,git,http.ssh是使用较多的,下面使用ssh搭建一个免密码登录的服务端. 1 ...
随机推荐
- ansible入门四(Ansible playbook基础组件介绍)
本节内容: ansible playbook介绍 ansible playbook基础组件 playbook中使用变量 一.ansible playbook介绍 playbook是由一个或多个“pla ...
- 说说C++多重继承
尽管大多数应用程序都使用单个基类的公用继承,但有些时候单继承是不够用的,因为可能无法为问题域建模或对模型带来不必要的复杂性.在这种情况下,多重继承可以更直接地为应用程序建模. 一.基本概念 多重继承是 ...
- iOS实现程序长时间未操作退出
大部分银行客户端都有这样的需求,在用户一定时间内未操作,即认定为token失效,但未操作是任何判定的呢?我的想法是用户未进行任何touch时间,原理就是监听runloop事件.我们需要进行的操作是创建 ...
- TP5 volist
VOLIST标签 volist标签通常用于查询数据集(select方法)的结果输出,通常模型的select方法返回的结果是一个二维数组,可以直接使用volist标签进行输出. 在控制器中首先对模版赋值 ...
- css 中相对定位和绝对定位
1. css中定位机制有三种: 标准文档流, 浮动, 绝对定位 2. 绝对定位就属于第三种定位, 用到position属性, 下面就是具体设置 相对定位: 相对于自身原有位置(就是普通流的时候)进行偏 ...
- SQL Server里查询表结构命令
现提供两条命令查询表结构: 1.sp_help table_name; 如: [sql] sp_help Student; 2.sp_columns table_name; ...
- 如何让PPT播放时仅电脑显示备注页,而投影仪不显示
完全可以!第一步:在电脑上右键点击桌面选择属性,进入显示属性选着设置,点击2号屏(前提已连接投影仪或第2显示器),并且在“将WINDOS桌面扩展到改监视器上”(这个关键)前面打钩,且自主选择分辨率,应 ...
- openoffice在连接时有错误,无法连接上
openoffice在连接时有错误,无法连接上 报如下错误: java.net.ConnectException: connection failed: socket,host=localhost,p ...
- Shell 命令行批量处理图片文件名
Shell 命令行批量处理图片文件名 从网上下载了一堆图片,有的是*.jpg的,有的是*.jpeg的.并且文件名有长有短,很是糟心.因此,我想把这些文件给全部整理好,当然是用shell来处理啦! 说干 ...
- 《Drools7.0.0.Final规则引擎教程》第4章 4.3 日历
日历 日历可以单独应用于规则中,也可以和timer结合使用在规则中使用.通过属性calendars来定义日历.如果是多个日历,则不同日历之间用逗号进行分割. 在Drools中,日历的概念只是将日历属性 ...