一种通用的简易缓存设计方案

1,领域模型设计

该设计方案定义了三个基础接口: Cache,Cleanable和CacheManager;和一个默认实现类DefaultCacheManager。

  • Cache接口抽象了非内存缓存所能提供的基础操作,Cache接口隔离了外部缓存的具体实现方案,可以是Redis/Codis等任意形式的缓存方案;
  • Cleanable接口定义了被缓存对象(value)的基本属性,所以需要被缓存的对象都必须实现Cleanable接口;
  • CacheManager抽象了该缓存方案支持的功能,直接提供给客户端调用,屏蔽了具体缓存实现方案,允许提供多个解决方案;
  • DefaultCacheManager是CacehManager的实现类,支持使用本地内存和外部缓存的实现。

2,Cache interface

该接口的定义没有多少争议,就是实现序列化对象通过key进行存取。

3,Cleanable interface

前文已说明,所有需要被缓存的对象都必须实现该接口。接口定义了,缓存对象必须是可清理的,有过期时间: expiredTime,创建时间: createdTime;如果使用的是本地内存实现,则会通过isValid方法方便的检查对象是否过期,否则使用timeout方法来设置外部缓存时长。

4,CacheManager interface

CacheManager接口是真正面向客户端使用的,缓存的value是Cleanable对象,key允许是任何可序列化值(常用String);特别地,需要携带一个枚举类ModuleType。在微服务或分布式的架构系统中,无论是在项目初期或中后期,都可能公用同一个外部缓存服务器,因此为了避免缓存数据冲突和方便数据追踪,都需要对Key进行模块分割。

5,DefaultCacheManager

public class DefaultCacheManager implements CacheManager, Runnable {

    private static ConcurrentMap<String, Cleanable> dataMap = new ConcurrentHashMap<String, Cleanable>();

    private final Long cleanPeriod;
    private final boolean enabledRedisCache;
    private final ScheduledExecutorService validationService;
    private final Cache cacheClient;

    public DefaultCacheManager() {
        this(1000L, null);
    }

    public DefaultCacheManager(Cache cacheClient) {
        this(null, cacheClient);
    }

    public DefaultCacheManager(Long cleanPeriod) {
        this(cleanPeriod, null);
    }

    /**
     * @param cleanPeriod
     * @param cacheClient
     */
    private DefaultCacheManager(Long cleanPeriod, Cache cacheClient) {
        this.enabledRedisCache = cacheClient != null ? true : false;
        this.cacheClient = cacheClient;
        this.cleanPeriod = cleanPeriod;
        if(!enabledRedisCache) {
            /**
             * 在本地完成expired清理动作
             */
            this.validationService = Executors.newSingleThreadScheduledExecutor();
            this.validationService.scheduleAtFixedRate(this, this.cleanPeriod,
                    this.cleanPeriod, TimeUnit.MILLISECONDS);
        } else
            this.validationService = null;
    }

    @Override
    public boolean exist(Serializable srcKey, ModuleType keyType) {
        String key = keyType.key(srcKey);
        if(enabledRedisCache) {
            return cacheClient.getValue(key) != null;
        }
        Cleanable c = dataMap.get(key);
        if (c == null)
            return false;
        if (!c.isValid()) {
            dataMap.remove(key);
            return false;
        }
        return true;
    }

    @Override
    public void put(Serializable srcKey, ModuleType keyType, Cleanable value) {
        String key = keyType.key(srcKey);
        if(enabledRedisCache) {
            cacheClient.setValue(key, value, value.timeout(), TimeUnit.SECONDS);
            return;
        }
        dataMap.put(key, value);
    }

    @Override
    public Cleanable get(Serializable srcKey, ModuleType keyType) {
        String key = keyType.key(srcKey);
        Cleanable value;
        if(enabledRedisCache) {
            value = ((Cleanable) cacheClient.getValue(key));
        } else {
            value = dataMap.get(key);
        }
        return value;
    }

    @Override
    public void remove(Serializable srcKey, ModuleType keyType) {
        String key = keyType.key(srcKey);
        if(enabledRedisCache) {
            cacheClient.removeValue(key);
        } else {
            dataMap.remove(key);
        }
    }

    public void run() {
        /**
         * 清理过期对象
         */
        Iterator<String> iter = dataMap.keySet().iterator();
        while (iter.hasNext()){
            String key = iter.next();
            // 由于并发缘故
            // 可能已经把该{@param key}对应的对象, 清理掉了
            Cleanable c = dataMap.get(key);
            if(c != null && !c.isValid()){
                iter.remove();
            }
        }
    }

}

DefaultCacheManager类是支持内存缓存和外部缓存的默认实现类。在此,可能会有疑惑,像Redis这样的外部缓存,应用已经非常普遍,为什么还要做内存缓存?我个人有三个考虑:1)即便是开源项目,如Eurka Server也仍然使用内存缓存,具体原因可查阅该项目的源码设计分析;2)对于微服务或分布式架构的系统,在项目早期或对于某些服务而言,需缓存数据量小或不需要共享缓存数据;3)追求高响应时间,不想额外依赖外部服务,提高系统可靠性。

6,一个实现Cleanable接口的案例

/**
* 资源片请求设备列表缓存对象
*/
public class Piece implements Cleanable {

public final Integer id;
public final Long groupId;
public final Long resourceId;
/**
* 拥有该资源片设备节点队列,不允许队列中出现重复设备(因为一些异常情况,设备节点会重复请求);
* 需要保证请求节点有序的添加,因此可以使用非线程安全的数组实现
*/
public final List<DownloadNode> nodes;
final LocalDateTime createdTime;
//默认60分钟后过期
final LocalDateTime expiredTime;

public Piece(Long groupId, Long resourceId, Integer id) {
this.groupId = groupId;
this.resourceId = resourceId;
this.id = id;
nodes = new ArrayList<>();
this.createdTime = LocalDateTime.now();
this.expiredTime = this.createdTime.plusMinutes(60);
}

@JsonCreator
Piece(@JsonProperty("id") Integer id, @JsonProperty("groupId") Long groupId,
@JsonProperty("resourceId") Long resourceId, @JsonProperty("nodes") List<DownloadNode> nodes,
@JsonProperty("createdTime") String createdTime, @JsonProperty("expiredTime") String expiredTime) {
this.id = id;
this.groupId = groupId;
this.resourceId = resourceId;
this.nodes = nodes;
this.createdTime = LocalDateTime.parse(createdTime);
this.expiredTime = LocalDateTime.parse(expiredTime);
}

public String CachedId() {
return "piece_" + groupId + "_" + resourceId + "_" + id;
}

/**
* 将拥有该资源片的节点加入队列
* @param node
*/
public void add(DownloadNode node) {
this.nodes.add(node);
}

/**
*
* 获取可共享资源片的节点
*
* 采用从第一个节点开始递减式分配。如当前序列有如下分配情况:
* 1, 0; 则当请求分配连接后变为 1,0。
* 2, 1,0; 则当请求分配连接后变为 2,0,0。
* 3, 2,0,0; 则当请求分配连接后变为 2,1,0,0
* 4, 2,1,0,0; 。。。。变为3,1,0,0,0
* 5, 3,1,0,0,0; 。。。。变为3,2,0,0,0,0
* 6, 3,2,0,0,0,0; 。。。。变为3,2,1,0,0,0,0
* 以此类推,递减式分配。也即是,
* a,如果索引n节点的conn和索引n+1的conn的差值为2则将n+1节点返回;
* b,否则,将索引为0的节点返回。
*
* @return 可共享资源片节点
*/
public DownloadNode getPieceNode() {
for(int i=0;i<nodes.size()-1;i++) {
DownloadNode n = nodes.get(i);
DownloadNode n_1 = nodes.get(i+1);
int val = n.conns - n_1.conns;
if(val == 2) {
return n_1;
}
}
return nodes.get(0);
}

public boolean hasRequest(DownloadNode dn) {
return nodes.contains(dn);
}

public void remove(DownloadNode dn) {
this.nodes.remove(dn);
}

@Override
public LocalDateTime expiredTime() {
return this.expiredTime;
}

@Override
public LocalDateTime createdTime() {
return this.createdTime;
}

//for json mapper

public Integer getId() {
return id;
}

public Long getGroupId() {
return groupId;
}

public Long getResourceId() {
return resourceId;
}

public List<DownloadNode> getNodes() {
return nodes;
}

public String getCreatedTime() {
return createdTime.toString();
}

public String getExpiredTime() {
return expiredTime.toString();
}
}

如上代码所示,Piece类是一个可缓存对象的实现类,其意义用于缓存所有请求下载过该资源片的设备节点列表。

a, 仔细看Piece类的属性有一个明显的特征,即所有属性都用final关键字修饰了;而实际上,所有Cleanable接口的实现类(或会被缓存的对象)的字段,除非必须的,都应当使用final关键字修饰。因为我们服务处在并发环境下,缓存对象应当尽可能做到线程安全,final修饰的作用就是消除JMM的重排序,保证创建的对象是线程安全的;而如果被缓存对象存在非final修饰的属性,则在发生修改时,若要确保数据的一致性,则必须加锁

b, 另外,可以注意到Piece类有两个构造方法,一个是公开访问,一个是不可公开调用但可以通过反射工具调用的;所以,所有Cleanable的实现类都必须实现一个@JsonCreator注解的保留构造方法,如此才能实现自动的序列化和反序列化。

原文地址:https://www.cnblogs.com/shenjixiaodao/p/12664864.html