缓存降级设计思想
接前文缺陷点
- 本地探针应该增加计数器,多次异常再设置,避免网络波动造成误判。
- 耦合度过高,远端缓存和本地缓存应该平行关系被设计为上下游关系了。
- 公用的远端缓存的操作方法应该私有化,避免集成方代码误操作,导致受到攻击。
- 探针改为轮训请求,类似jedis底层心跳检测。
- 抽象多层策略,提供集成方自定义实现配置来决定采用什么方式进行降级,缓存工厂或者控制器来决定Redis是否异常和异常后走本地缓存还是数据库还是zk等策略。
- 底层应该做好职责分离,异常往上抛,由上层根据用户配置的策略做对应的处理逻辑。
- 业务层采用模板模式,对Redis降级后的业务逻辑提供有结构性的模板方法,并要支持用户灵活重写,支持重写的方法名不要取的具体,应取的抽象。
1.创建缓存操作接口定义缓存数据的增删改查方法,底层由redis工具类实现作为一级缓存,另一个实现可以选择本地缓存工具类或第三方数据存储工具类实现,作为二级缓存在redis降级时使用。
java
public interface IWecareCache {
/**
* 根据key,获取到对应的value值
* 注: 若key不存在, 则返回null
*
* @param key key-value对应的key
* @return 该key对应的值。
*/
String get(String key);
。。。
}
@Component(WecareSsoConstant.CACHE_TYPE_REDIS)
public class WecareRedisCache implements IWecareCache {
private final JedisCluster jedis = StaticSingletonFactory.getJedisSingleton();
@Override
public String get(String key) {
log.debug("[JedisCluster]: get -> key={}", key);
return jedis.get(key);
}
。。。
}
@Component(WecareSsoConstant.CACHE_TYPE_LOCAL)
public class WecareLocalCache implements IWecareCache {
/**
* 本地缓存记录str的key
*/
private static final String STRING_KEY = "StringKey@";
private final Cache<String, Map<String, String>> wecareLocalCache = StaticSingletonFactory.getLocalCacheSingleton();
@Override
public String get(String key) {
// 先判断是否key对应的map是否存在
Map<String, String> map = wecareLocalCache.getIfPresent(key);
if (CollectionUtils.isEmpty(map)) {
return null;
}
log.debug("[localCache]: get -> key={},value={}", key, map.get(STRING_KEY));
return map.get(STRING_KEY);
}
。。。
}
2.创建缓存策略接口用于实现根据策略选择获取什么缓存核心进行操作,并提供一个默认策略实现,再在工厂中提供热加载方法,如果存在自定义实现则优先获取自定义策略实现
java
public interface ICacheStrategy {
// 根据策略自动选择具体缓存实现的方法
IWecareCache getCacheByStrategy();
// 根据指定缓存名称获取缓存实现的方法
IWecareCache getCacheByName(String cacheName);
// 查询redis运行状态是否OK
boolean isRedisOK();
// 设置redis状态为正常
void setRedisAvailable();
// 设置redis状态为异常
void setRedisNotAvailable();
}
// SDK缓存策略默认实现,根据redis监听状态进行远端缓存和本地缓存的热切换
@Component(WecareSsoConstant.DEFAULT_CACHE_STRATEGY)
public class DefaultCacheStrategy implements ICacheStrategy {
// redis是否可用--策略读取,监控更新
private final AtomicBoolean redisIsLive = new AtomicBoolean(false);
// 可用缓存工具-自动注入
@Autowired
private Map<String, IWecareCache> cacheMap;
//默认缓存策略-如果redis异常则使用jvm缓存
//--如果不需要降级,请实现自定义策略始终返回redis缓存即可
@Override
public IWecareCache getCacheByStrategy() {
IWecareCache cacheByDefaultStrategy = getCacheByDefaultStrategy();
if (cacheByDefaultStrategy == null) {
log.error("no config cache");
throw new BizException("no config cache");
}
return cacheByDefaultStrategy;
}
@Override
public IWecareCache getCacheByName(String cacheName) {
return cacheMap.get(cacheName);
}
@Override
public boolean isRedisOK() {
return redisIsLive.get();
}
// 默认redis状态实现-通过AtomicBoolean控制
@Override
public void setRedisAvailable() {
redisIsLive.set(true);
}
// 默认redis状态实现-通过AtomicBoolean控制
@Override
public void setRedisNotAvailable() {
redisIsLive.set(false);
}
private IWecareCache getCacheByDefaultStrategy() {
if (redisIsLive.get()) {
return cacheMap.get(WecareSsoConstant.CACHE_TYPE_REDIS);
} else {
return cacheMap.get(WecareSsoConstant.CACHE_TYPE_LOCAL);
}
}
}
3.创建缓存工具类,提供缓存操作方法,每次操作缓存都通过策略获取对应的缓存核心进行操作,实现热降级
java
public class WecareCacheUtil {
private static final ICacheStrategy cacheStrategy = StaticSingletonFactory.getCacheStrategy();
// 根据策略获取缓存实现
private static IWecareCache getCacheByStrategy() {
return cacheStrategy.getCacheByStrategy();
}
// 根据缓存名称获取指定缓存实现--默认拥有REDIS和JVM两个
private static IWecareCache getCacheByName(String cacheName) {
return cacheStrategy.getCacheByName(cacheName);
}
public static boolean isRedisOK(){
return cacheStrategy.isRedisOK();
}
public static String get(String key) {
return getCacheByStrategy().get(key);
}
// 根据名称操作指定缓存实现,业务需要在正常流程时预先在jvm存储用户信息。
public static String get(String key, String cacheName) {
return getCacheByName(cacheName).get(key);
}
。。。
4.创建redis监控接口用于定义redis的监控方式,提供默认实现并同策略接口一样支持自定义实现的热加载,创建执行监控的线程类
java
public interface IRedisMonitor {
void healthCheck();
}
@Component(WecareSsoConstant.DEFAULT_REDIS_MONITOR)
public class DefaultRedisMonitor implements IRedisMonitor {
// 连接异常最大次数
private static final int MAX_ERROR_COUNT = 2;
// 心跳频率:多少毫秒检测一次
private static final int HEART_BEAT_FREQUENCY = 5000;
private final JedisCluster jedis = StaticSingletonFactory.getJedisSingleton();
private final ICacheStrategy cacheStrategy = StaticSingletonFactory.getCacheStrategy();
// redis集群为6主多备,因此检测到任意一个主节点宕机就算集群failed,集群failed超过阈值则设置redis集群状态不可用,此方法是否可监控连接数满导致的异常还有待测试
private boolean isAllNodesActivated() {
try {
Map<String, JedisPool> clusterNodes = jedis.getClusterNodes();
for (Map.Entry<String, JedisPool> entry : clusterNodes.entrySet()) {
JedisPool pool = entry.getValue();
String clusterInfo;
try (Jedis jedisResource = pool.getResource()) {
clusterInfo = jedisResource.clusterInfo();
}
if (!clusterInfo.contains("cluster_state:ok")) {
log.error("redis node:{} cluster_state:fail", entry.getKey());
log.error("clusterInfo:{}", clusterInfo);
return false;
}
}
return true;
}catch (JedisException jedisException){
log.error("redis 读取节点信息异常 jedisException:",jedisException);
}catch (Exception exception){
try {
jedis.set("testHealthCheck","true");
String testHealthCheck = jedis.get("testHealthCheck");
if ("true".equals(testHealthCheck)) {
return true;
}
}catch (Exception e){
log.error("redis 操作测试异常 exception:",e);
}
log.error("redis 读取节点信息异常 exception:",exception);
}
return false;
}
@Override
public void healthCheck() {
int threadException = 0;
int redisErrorCount = 0;
LocalDateTime lastLogTime = LocalDateTime.now().minusMinutes(1);
while (true) {
try {
Thread.sleep(HEART_BEAT_FREQUENCY);
if (isAllNodesActivated()) {
redisErrorCount = 0;
cacheStrategy.setRedisAvailable();
LocalDateTime now = LocalDateTime.now();
if (Duration.between(lastLogTime,now).toMinutes()>0) {
log.info("Redis Cluster Nodes Health Check OK");
lastLogTime = now;
}
} else {
redisErrorCount++;
if (redisErrorCount >= MAX_ERROR_COUNT) {
redisErrorCount = 0;
cacheStrategy.setRedisNotAvailable();
log.info("Redis Cluster Nodes Health Check Failed!!!");
}
}
} catch (InterruptedException interruptedException) {
log.error("redis监控线程休眠异常!", interruptedException);
if (threadException > 3) {
log.error("redis监控线程因休眠异常强制终止!", interruptedException);
break;
}
threadException++;
}
}
}
}
// 监控线程类:提供启动redis监控的方法
public class RedisMonitorThread implements Runnable {
private final IRedisMonitor monitor;
public RedisMonitorThread(IRedisMonitor monitor) {
this.monitor = monitor;
}
@Override
public void run() {
monitor.healthCheck();
}
public static void startMonitor(IRedisMonitor monitor) {
new Thread(new RedisMonitorThread(monitor),monitor.getClass().getSimpleName()+"-Thread").start();
}
}
5.创建单例转静态对象的工厂,用于将spring管理的动态单例转换为静态单例,全局提供静态方法,并定义线程初始化和类加载
java
@Component
public class StaticSingletonFactory implements ApplicationContextAware {
// 缓存策略实现
private static Map<String, ICacheStrategy> cacheStrategyMap;
// redis监控实现
private static Map<String, IRedisMonitor> redisMonitorMap;
private static JedisCluster jedis;
// 本地缓存
private static Cache<String, Map<String, String>> wecareSsoLocalCache;
// 配置参数
private static ConfigProperty configProperty;
// 静态工厂bean初始化--赋值顺序要按照使用顺序,且代码避免循环依赖
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
configProperty = applicationContext.getBean(ConfigProperty.class);
// 策略集合赋值
cacheStrategyMap = applicationContext.getBeansOfType(ICacheStrategy.class);
if (configProperty.isRedisEnabled()) {
// 确认开启使用redis功能
jedis = applicationContext.getBean("sdkJedisClient", JedisCluster.class);
// 监控集合赋值
redisMonitorMap = applicationContext.getBeansOfType(IRedisMonitor.class);
// 在赋值jedis客户端后启动监听线程
RedisMonitorThread.startMonitor(getRedisMonitor());
}
wecareSsoLocalCache = (Cache<String, Map<String, String>>) applicationContext.getBean("wecareSsoLocalCache");
}
public static JedisCluster getJedisSingleton() {
return jedis;
}
public static Cache<String, Map<String, String>> getLocalCacheSingleton() {
return wecareSsoLocalCache;
}
public static ConfigProperty getConfigProperty() {
return configProperty;
}
// 指定策略实现
public static ICacheStrategy getCacheStrategy(String cacheStrategyName) {
ICacheStrategy iCacheStrategy = cacheStrategyMap.get(cacheStrategyName);
if (iCacheStrategy == null) {
throw new BizException(
"Select CacheStrategy Error, cacheStrategyName " + cacheStrategyName + " undefined !");
}
return iCacheStrategy;
}
// 获取缓存策略实现
public static ICacheStrategy getCacheStrategy() {
return hotLoading(cacheStrategyMap, WecareSsoConstant.DEFAULT_CACHE_STRATEGY, ICacheStrategy.class);
}
// 获取redis监控实现
public static IRedisMonitor getRedisMonitor() {
return hotLoading(redisMonitorMap, WecareSsoConstant.DEFAULT_REDIS_MONITOR, IRedisMonitor.class);
}
// 接口实现类热加载,有自定义实现返回自定义,无自定义实现返回默认实现
private static <T> T hotLoading(Map<String, T> map, String defaultName, Class<T> obj) {
String className = obj.getSimpleName();
int size = map.size();
switch (size) {
case 0:
throw new BizException(className + " init Error, no implements !");
case 1:
return map.get(defaultName);
case 2:
for (Map.Entry<String, T> entry : map.entrySet()) {
if (!defaultName.equals(entry.getKey())) {
return entry.getValue();
}
}
break;
default:
break;
}
throw new BizException("Select " + className + " Error, expected 1 but found " + size);
}
}