先简单解释下什么是分布式多级缓存,所谓分布式简单理解就是异地跨机房服务应用部署;所谓多级缓存,这里狭义语义指定的是应用服务级别的缓存,通常泛指Redis、Memcached等;所谓多级缓存,这里是将JVM级的驻留缓存和外部依赖的缓存服务相比而言的。Redis、Memcached等都提供了性能优越的缓存服务,在高并发场景下作为提高吞吐量、优化服务性能的利器立下了汗马功劳。
一般情况下,缓存我们只使用Redis作为唯一缓存就可以满足大多数业务场景。这里我们不考虑一般的业务场景,现在试图将服务场景复杂化去进行设计,进一步提高对服务性能的追求。
首先概述下业务场景, 我们的应用服务每天需要提供亿级别调用量的查询业务,在最原始阶段,外部业务提供有效入参请求服务接口返回业务数据即可,然而在之后需求迭代中,增加了对调用方权限校验(渠道校验、授权码校验、入参许可校验)和对返回业务数据的保护(涉及脱敏和非授权字段的过滤排除),业务逻辑瞬间丰富和复杂起来。
一般场景
复杂场景
以上复杂场景下,需要解决如下问题:
问题 | 解决方向 |
---|---|
数据校验对比 | 哈希表存储避免遍历,O(1)时间复杂度 |
校验数据读取 | 预加载配置数据到缓存 JVM作为一级缓存,减少网络消耗 Redis作为二级缓存兜底 |
数据存储 | 配置数据量小,变更低频,读取高频,适合驻留JVM一级缓存 业务数据量巨大,变更不可控,读取高频,适合存储Redis二级缓存 |
通常,我们会选择HashMap或线程安全的ConcurrentHashMap作为JVM缓存容器来存储数据。
这三者都是Key-Value形式存储,具体的实现细节不同,JDK自带的HashMap、ConcurrentHashMap操作和实现简单、Caffeine则是一套封装良好天生为本地缓存服务的框架,提供了很多缓存特性。
定义本地缓存服务的能力定义
/**
* @author: guanjian
* @date: 2020/07/06 16:11
* @description: 本地缓存接口定义
*/
public interface LocalCache {
Object get(Object key);
void put(Object key, Object value);
void putIfAbsent(Object key, Object value);
void put(Map map);
void remove(Object key);
Collection<?> getKeys();
void clear();
boolean hasKey(Object key);
void destroy();
long size();
boolean isEmpty();
String getRegion();
Map asMap();
}
由于缓存都是Key-Value形式存储,只能支持Key单维度数据存储,为了提供更为便捷和可扩展的数据存储与读取场景,引入了Region分区使得缓存支持多维度业务。其实这里每个缓存实现内部都持有一个可见性的Map
ConcurrentHashMap本地缓存的实现
/**
* @author: guanjian
* @date: 2020/07/06 16:15
* @description: 通过ConcurrentHashMap构建本地缓存
*/
public class ConcurrentHashMapCache implements LocalCache {
/**
* 多分区单例
* {@String region 缓存分区标识}
*/
private static volatile Map<String, ConcurrentHashMapCache> INSTANCES = Maps.newConcurrentMap();
/**
* 缓存分区标识
*/
private String region;
/**
* 缓存容器
* {@code Map
private Map<Object, Object> cache = Maps.newConcurrentMap();
@Override
public Object get(Object key) {
return cache.get(key);
}
@Override
public void put(Object key, Object value) {
cache.put(key, value);
}
@Override
public void putIfAbsent(Object key, Object value) {
cache.putIfAbsent(key, value);
}
@Override
public void put(Map map) {
cache.putAll(map);
}
@Override
public void remove(Object key) {
cache.remove(key);
}
@Override
public Collection<?> getKeys() {
return cache.keySet();
}
@Override
public void clear() {
cache.clear();
}
@Override
public boolean hasKey(Object key) {
return cache.containsKey(key);
}
@Override
public void destroy() {
INSTANCES.remove(region);
}
@Override
public long size() {
return cache.size();
}
@Override
public boolean isEmpty() {
return cache.isEmpty();
}
@Override
public String getRegion() {
return this.region;
}
@Override
public Map asMap() {
return cache;
}
public static ConcurrentHashMapCache getInstance(String region) {
if (INSTANCES.containsKey(region)) {
return INSTANCES.get(region);
}
ConcurrentHashMapCache instance = null;
if (!INSTANCES.containsKey(region)) {
synchronized (INSTANCES) {
if (!INSTANCES.containsKey(region)) {
instance = new ConcurrentHashMapCache(region);
INSTANCES.putIfAbsent(region, instance);
}
}
}
return instance;
}
private ConcurrentHashMapCache(String region) {
this.region = region;
}
}
Caffeine本地缓存的实现
/**
* @author: guanjian
* @date: 2020/07/08 9:17
* @description: 通过Caffeine构建本地缓存
*/
public class CaffeineCache implements LocalCache {
private final static Logger LOGGER = LoggerFactory.getLogger(CaffeineCache.class);
/**
* 多分区单例
* {@String region 缓存分区标识}
*/
private static volatile Map<String, CaffeineCache> INSTANCES = Maps.newConcurrentMap();
/**
* 缓存分区标识
*/
private String region;
/**
* 缓存容器
* {@code Cache
private Cache<Object, Object> cache = Caffeine.newBuilder()
.recordStats()
.initialCapacity(2 << 2)
.build();
private Object synLock = new Object();
@Override
public Object get(Object key) {
Object value = cache.getIfPresent(key);
LOGGER.debug("[CaffeineCache] region={}, key={},value={} getted.", region, key, JSON.toJSONString(value));
return value;
}
@Override
public void put(Object key, Object value) {
cache.put(key, value);
LOGGER.debug("[CaffeineCache] region={}, key={},value={} putted.", region, key, JSON.toJSONString(value));
}
@Override
public void putIfAbsent(Object key, Object value) {
synchronized (synLock) {
if (null != cache.getIfPresent(key)) return;
cache.put(key, value);
LOGGER.debug("[CaffeineCache] region={}, key={},value={} putted.", region, key, JSON.toJSONString(value));
}
}
@Override
public void put(Map map) {
cache.putAll(map);
LOGGER.debug("[CaffeineCache] region={}, map={} putted.", region, JSON.toJSONString(map));
}
@Override
public void remove(Object key) {
cache.cleanUp();
}
@Override
public Collection<?> getKeys() {
return cache.asMap().keySet();
}
@Override
public void clear() {
cache.invalidateAll();
}
@Override
public boolean hasKey(Object key) {
LOGGER.debug("[CaffeineCache] region={}, key={}, map={}.", region, key, JSON.toJSONString(cache.asMap()));
return null != cache.getIfPresent(key);
}
@Override
public void destroy() {
INSTANCES.remove(region);
}
@Override
public long size() {
return cache.asMap().keySet().size();
}
@Override
public boolean isEmpty() {
return 0 == cache.asMap().keySet().size();
}
@Override
public String getRegion() {
return region;
}
@Override
public Map asMap() {
return cache.asMap();
}
public static CaffeineCache getInstance(String region) {
if (INSTANCES.containsKey(region)) {
return INSTANCES.get(region);
}
CaffeineCache instance = null;
if (!INSTANCES.containsKey(region)) {
synchronized (INSTANCES) {
if (!INSTANCES.containsKey(region)) {
instance = new CaffeineCache(region);
INSTANCES.putIfAbsent(region, instance);
LOGGER.debug("[CaffeineCache] region={} is established.", region);
}
}
} else {
instance = INSTANCES.get(region);
}
return instance;
}
private CaffeineCache(String region) {
this.region = region;
}
}
由于Redis提供了非常高效、便捷的数据结构,数据存储及选取的数据结构如下
数据名称 | 数据类型 | 存储数据结构 |
---|---|---|
渠道值 | 配置数据 | Hash |
授权码 | 配置数据 | Hash |
业务授权 | 配置数据 | Hash |
授权字段 | 配置数据 | String(JSON序列化) |
脱敏字段 | 配置数据 | String(JSON序列化) |
业务信息 | 业务数据 | String(JSON序列化) |