xiatian vor 5 Jahren
Ursprung
Commit
c6c7735351

+ 8 - 7
examcloud-support/src/main/java/cn/com/qmth/examcloud/support/cache/CacheHelper.java

@@ -24,6 +24,7 @@ import cn.com.qmth.examcloud.support.cache.bean.SmsAssemblyCacheBean;
 import cn.com.qmth.examcloud.support.cache.bean.StudentCacheBean;
 import cn.com.qmth.examcloud.support.cache.bean.SysPropertyCacheBean;
 import cn.com.qmth.examcloud.support.cache.bean.ThirdPartyAccessCacheBean;
+import cn.com.qmth.examcloud.web.cache.HashRedisCacheProcessor;
 import cn.com.qmth.examcloud.web.cache.ObjectRedisCacheProcessor;
 
 /**
@@ -136,9 +137,9 @@ public class CacheHelper {
 	 * @return
 	 */
 	public static ExtractConfigPaperCacheBean getExtractConfigPaper(Long examId, String courseCode,
-			String groupCode, String paperId) {
-		return ObjectRedisCacheProcessor.get("Q_PAPER:EXTRACT_CONFIG_PAPER_",
-				new Object[]{examId, courseCode, groupCode, paperId},
+																	String groupCode, String paperId) {
+		return HashRedisCacheProcessor.get("Q_PAPER:EXTRACT_CONFIG_PAPER_",
+				new Object[]{paperId},new Object[]{examId, courseCode, groupCode},
 				ExtractConfigPaperCacheBean.class, "EC-CORE-QUESTION",
 				"cn.com.qmth.examcloud.core.questions.service.cache.ExtractConfigPaperCache");
 	}
@@ -165,9 +166,9 @@ public class CacheHelper {
 	 * @return
 	 */
 	public static QuestionCacheBean getQuestion(Long examId, String courseCode, String groupCode,
-			String questionId) {
-		return ObjectRedisCacheProcessor.get("Q_QUESTION:",
-				new Object[]{examId, courseCode, groupCode, questionId}, QuestionCacheBean.class,
+												String questionId) {
+		return HashRedisCacheProcessor.get("Q_QUESTION:",
+				new Object[]{questionId},new Object[]{examId, courseCode, groupCode}, QuestionCacheBean.class,
 				"EC-CORE-QUESTION",
 				"cn.com.qmth.examcloud.core.questions.service.cache.QuestionCache");
 	}
@@ -236,7 +237,7 @@ public class CacheHelper {
 	 * @return
 	 */
 	public static ExamStudentPropertyCacheBean getExamStudentProperty(Long examId, Long studentId,
-			String key) {
+																	  String key) {
 		return ObjectRedisCacheProcessor.get("E_EXAM_STUDENT_PROP:",
 				new Object[]{examId, studentId, key}, ExamStudentPropertyCacheBean.class,
 				"EC-CORE-EXAMWORK",

+ 58 - 0
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/cache/CacheCloudServiceProvider.java

@@ -35,6 +35,8 @@ public class CacheCloudServiceProvider implements CloudService {
 			.getLog(CacheCloudServiceProvider.class);
 
 	private static Map<String, ObjectCache<?>> map = Maps.newConcurrentMap();
+	
+	private static Map<String, HashCache<?>> hashCacheMap = Maps.newConcurrentMap();
 
 	@PostMapping("refresh")
 	public String refresh(@RequestBody RefreshCacheReq req) {
@@ -74,5 +76,61 @@ public class CacheCloudServiceProvider implements CloudService {
 		Object object = objectCache.get(expectedKeys);
 		return JsonUtil.toJson(object);
 	}
+	
+	@PostMapping("refreshHash")
+    public String refreshHash(@RequestBody RefreshHashCacheReq req) {
+
+        String className = req.getClassName();
+        String[] keys = req.getKeys();
+        String[] subkeys = req.getSubKeys();
+        BasicDataType[] typeArray = req.getTypeArray();
+        BasicDataType[] subTypeArray = req.getSubTypeArray();
+
+        Object[] expectedKeys = new Object[keys.length];
+
+        for (int i = 0; i < keys.length; i++) {
+            String key = keys[i];
+            BasicDataType type = typeArray[i];
+            if (type.equals(BasicDataType.LONG)) {
+                expectedKeys[i] = Long.parseLong(key);
+            } else if (type.equals(BasicDataType.STRING)) {
+                expectedKeys[i] = key;
+            } else if (type.equals(BasicDataType.INTEGER)) {
+                expectedKeys[i] = Integer.parseInt(key);
+            } else {
+                throw new ExamCloudRuntimeException("key type is not supported");
+            }
+        }
+        
+        Object[] expectedSubKeys = new Object[subkeys.length];
+        for (int i = 0; i < subkeys.length; i++) {
+            String key = subkeys[i];
+            BasicDataType type = subTypeArray[i];
+            if (type.equals(BasicDataType.LONG)) {
+                expectedSubKeys[i] = Long.parseLong(key);
+            } else if (type.equals(BasicDataType.STRING)) {
+                expectedSubKeys[i] = key;
+            } else if (type.equals(BasicDataType.INTEGER)) {
+                expectedSubKeys[i] = Integer.parseInt(key);
+            } else {
+                throw new ExamCloudRuntimeException("subkey type is not supported");
+            }
+        }
+
+        HashCache<?> hashCache = hashCacheMap.get(className);
+        if (null == hashCache) {
+
+            try {
+                Class<?> c = Class.forName(className);
+                hashCache = (HashCache<?>) SpringContextHolder.getBean(c);
+                hashCacheMap.put(className, hashCache);
+            } catch (ClassNotFoundException e) {
+                throw new StatusException("008002", "class not found", e);
+            }
+        }
+        hashCache.refresh(expectedKeys,expectedSubKeys);
+        Object object = hashCache.get(expectedKeys,expectedSubKeys);
+        return JsonUtil.toJson(object);
+    }
 
 }

+ 41 - 0
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/cache/HashCache.java

@@ -0,0 +1,41 @@
+package cn.com.qmth.examcloud.web.cache;
+
+/**
+ * Hash缓存
+ *
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ * @param <T>
+ */
+public interface HashCache<T> {
+
+	/**
+	 * 从缓存中获取
+	 *
+	 * @param keys
+	 * @return
+	 */
+	T get(Object[] keys,Object[] subkeys);
+
+	/**
+	 * 删除缓存
+	 *
+	 * @param keys 大key
+	 */
+	void remove(Object... keys);
+
+	/**
+	 * 刷新缓存
+	 *
+	 * @param keys
+	 */
+	void refresh(Object[] keys,Object[] subkeys);
+
+	/**
+	 * 从数据源(数据库,配置文件等)加载单个缓存项
+	 *
+	 * @param keys
+	 * @return
+	 */
+	T loadFromResource(Object[] keys,Object[] subkeys);
+
+}

+ 89 - 0
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/cache/HashRedisCache.java

@@ -0,0 +1,89 @@
+package cn.com.qmth.examcloud.web.cache;
+
+import org.apache.commons.lang3.StringUtils;
+import org.assertj.core.util.Arrays;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.exception.StatusException;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * Hash redis缓存
+ *
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ * @param <T>
+ */
+public abstract class HashRedisCache<T extends RandomCacheBean> implements HashCache<T> {
+
+    private RedisClient redisClient;
+
+    private RedisClient getRedisClient() {
+        if (null == redisClient) {
+            redisClient = SpringContextHolder.getBean(RedisClient.class);
+        }
+        return redisClient;
+    }
+
+    protected abstract String getKeyPrefix();
+
+    protected abstract int getTimeout();
+
+    protected String buildKey(Object... keys) {
+        String key = getKeyPrefix() + StringUtils.join(Arrays.asList(keys), '_');
+        return key;
+    }
+    
+    protected String buildSubKey(Object... keys) {
+        String key = StringUtils.join(Arrays.asList(keys), '_');
+        return key;
+    }
+
+    private T getFromCache(String key, String subkey) {
+        Object object = getRedisClient().get(key,subkey, Object.class);
+        @SuppressWarnings("unchecked")
+        T t = (T) object;
+        return t;
+    }
+    @Override
+    public T get(Object[] keys,Object[] subkeys) {
+        String key = buildKey(keys);
+        String subkey = buildSubKey(subkeys);
+        T t = getFromCache(key,subkey);
+
+        if (null == t) {
+            refresh(keys,subkeys);
+        }
+        t = getFromCache(key,subkey);
+        return t;
+    }
+
+    @Override
+    public void remove(Object... keys) {
+        String key = buildKey(keys);
+        getRedisClient().delete(key);
+    }
+
+    @Override
+    public void refresh(Object[] keys,Object[] subkeys) {
+        String key = buildKey(keys);
+        String subkey = buildSubKey(subkeys);
+        T t = null;
+        try {
+            t = loadFromResource(keys,subkeys);
+        } catch (StatusException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new ExamCloudRuntimeException("fail to load data. key=" + key+" subkey="+subkey, e);
+        }
+        if (null != t) {
+            int timeout = getTimeout();
+            if (timeout < 60) {
+                timeout = 60;
+            }
+            getRedisClient().set(key,subkey, t, timeout);
+        } else {
+            getRedisClient().delete(key,subkey);
+        }
+    }
+}

+ 134 - 0
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/cache/HashRedisCacheProcessor.java

@@ -0,0 +1,134 @@
+package cn.com.qmth.examcloud.web.cache;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.lang3.StringUtils;
+import org.assertj.core.util.Arrays;
+
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.commons.util.ThreadLocalUtil;
+import cn.com.qmth.examcloud.commons.util.Util;
+import cn.com.qmth.examcloud.web.redis.RedisClient;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * redis Hash缓存处理器
+ *
+ * @date 2019年5月10日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class HashRedisCacheProcessor {
+
+	private static RedisClient redisClient;
+
+	private static HashRedisCacheTrigger hashRedisCacheTrigger;
+
+	private static RedisClient getRedisClient() {
+		if (null == redisClient) {
+			redisClient = SpringContextHolder.getBean(RedisClient.class);
+		}
+		return redisClient;
+	}
+
+	private static HashRedisCacheTrigger getHashRedisCacheTrigger() {
+		if (null == hashRedisCacheTrigger) {
+			hashRedisCacheTrigger = SpringContextHolder.getBean(HashRedisCacheTrigger.class);
+		}
+		return hashRedisCacheTrigger;
+	}
+
+	/**
+	 * 获取缓存对象
+	 *
+	 * @author 
+	 * @param keyPrefix
+	 * @param propKeys
+	 * @param c
+	 * @return
+	 */
+	public static <T> T get(String keyPrefix, Object[] propKeys,Object[] subpropKeys, Class<T> c) {
+		String key = keyPrefix + StringUtils.join(Arrays.asList(propKeys), '_');
+		String hashKey=StringUtils.join(Arrays.asList(subpropKeys), '_');
+		T t = getRedisClient().get(key,hashKey, c);
+		return t;
+	}
+
+	/**
+	 * 取缓存对象(不存在时远程或本地加载)<br>
+	 * 缓存失效时,只允许一个线程加载缓存,防止缓存击穿<br>
+	 * 缓存加载时长不得超过10秒,否则所有取缓存线程无等待抛出异常,只到缓存被正确加载<br>
+	 * 
+	 *
+	 * @author 
+	 * @param keyPrefix
+	 * @param propKeys
+	 * @param c
+	 * @param appName
+	 * @param className
+	 * @return
+	 */
+	public static <T> T get(String keyPrefix, Object[] propKeys,Object[] subpropKeys, Class<? extends RandomCacheBean> c,
+			String appName, String className) {
+
+		String key = keyPrefix + StringUtils.join(Arrays.asList(propKeys), '_');
+		String subKey=StringUtils.join(Arrays.asList(subpropKeys), '_');
+
+		RandomCacheBean t = getRedisClient().get(key,subKey, c);
+
+		if (null == t) {
+
+			int count = 0;
+
+			String cacheLock = "$_CACHE_LOCK:" + key+"_"+subKey;
+			String cacheException = "$_CACHE_EXCEPTION:" + key+"_"+subKey;
+
+			while (true) {
+				count++;
+
+				t = getRedisClient().get(key,subKey, c);
+
+				if (null != t) {
+					break;
+				}
+
+				Boolean locked = getRedisClient().setIfAbsent(cacheLock,
+						ThreadLocalUtil.getTraceId(), 30);
+
+				if (locked) {
+					try {
+					    getHashRedisCacheTrigger().fire(appName, className, propKeys,subpropKeys);
+						getRedisClient().delete(cacheException);
+						t = getRedisClient().get(key,subKey, c);
+						break;
+					} catch (Exception e) {
+						getRedisClient().set(cacheException, true, 60);
+						throw e;
+					} finally {
+						getRedisClient().delete(cacheLock);
+					}
+				} else {
+
+					if (null != getRedisClient().get(cacheException, Boolean.class)) {
+						throw new ExamCloudRuntimeException(
+								"exception happened when loading cache. key=" + key+" subKey="+subKey);
+					}
+
+					// 10秒内未加载完缓存,抛出异常
+					if (200 < count) {
+						throw new ExamCloudRuntimeException(
+								"fail to load cache in 10 seconds. key=" + key+" subKey="+subKey);
+					}
+
+					Util.sleep(TimeUnit.MILLISECONDS, 50);
+
+				}
+			}
+
+		}
+
+		@SuppressWarnings("unchecked")
+		T ret = (T) t;
+		return ret;
+	}
+
+}

+ 109 - 0
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/cache/HashRedisCacheTrigger.java

@@ -0,0 +1,109 @@
+package cn.com.qmth.examcloud.web.cache;
+
+import java.util.Map;
+
+import org.springframework.stereotype.Component;
+
+import com.google.common.collect.Maps;
+
+import cn.com.qmth.examcloud.api.commons.enums.BasicDataType;
+import cn.com.qmth.examcloud.commons.exception.ExamCloudRuntimeException;
+import cn.com.qmth.examcloud.web.cloud.CloudClientSupport;
+import cn.com.qmth.examcloud.web.support.SpringContextHolder;
+
+/**
+ * redis缓存触发器
+ *
+ * @author 
+ * @date 2019年5月9日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+@Component
+public class HashRedisCacheTrigger extends CloudClientSupport {
+
+	private static Map<String, Boolean> hasClassMap = Maps.newConcurrentMap();
+
+	private static Map<String, HashCache<?>> hashCacheMap = Maps.newConcurrentMap();
+
+	/**
+	 * 开火
+	 *
+	 * @author 
+	 * @param appName
+	 * @param keys
+	 * @return
+	 */
+	public void fire(String appName, String className, Object[] keys,Object[] subkeys) {
+
+		Boolean has = hasClassMap.get(className);
+		HashCache<?> hashCache = null;
+
+		if (null != has) {
+			if (has) {
+				hashCache = hashCacheMap.get(className);
+				hashCache.refresh(keys,subkeys);
+				return;
+			}
+		} else {
+			try {
+				Class<?> c = Class.forName(className);
+				hashCache = (HashCache<?>) SpringContextHolder.getBean(c);
+				hashCache.refresh(keys,subkeys);
+				hashCacheMap.put(className, hashCache);
+				hasClassMap.put(className, true);
+				return;
+			} catch (ClassNotFoundException e) {
+				hasClassMap.put(className, false);
+			}
+		}
+
+		String[] keyArray = new String[keys.length];
+		BasicDataType[] typeArray = new BasicDataType[keys.length];
+
+		for (int i = 0; i < keys.length; i++) {
+			keyArray[i] = String.valueOf(keys[i]);
+			Class<? extends Object> c = keys[i].getClass();
+			if (c.equals(Long.class)) {
+				typeArray[i] = BasicDataType.LONG;
+			} else if (c.equals(String.class)) {
+				typeArray[i] = BasicDataType.STRING;
+			} else if (c.equals(Integer.class)) {
+				typeArray[i] = BasicDataType.INTEGER;
+			} else {
+				throw new ExamCloudRuntimeException("key type is not supported");
+			}
+		}
+		
+		String[] subKeyArray = new String[subkeys.length];
+        BasicDataType[] subTypeArray = new BasicDataType[subkeys.length];
+
+        for (int i = 0; i < subkeys.length; i++) {
+            subKeyArray[i] = String.valueOf(subkeys[i]);
+            Class<? extends Object> c = subkeys[i].getClass();
+            if (c.equals(Long.class)) {
+                subTypeArray[i] = BasicDataType.LONG;
+            } else if (c.equals(String.class)) {
+                subTypeArray[i] = BasicDataType.STRING;
+            } else if (c.equals(Integer.class)) {
+                subTypeArray[i] = BasicDataType.INTEGER;
+            } else {
+                throw new ExamCloudRuntimeException("subkey type is not supported");
+            }
+        }
+
+		RefreshHashCacheReq req = new RefreshHashCacheReq();
+		req.setClassName(className);
+		req.setKeys(keyArray);
+		req.setTypeArray(typeArray);
+		req.setSubKeys(subKeyArray);
+		req.setSubTypeArray(subTypeArray);
+		post(appName, "refreshHash", req);
+
+	}
+
+	@Override
+	protected String getRequestMappingPrefix() {
+		return "cache";
+	}
+
+}

+ 76 - 0
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/cache/RefreshHashCacheReq.java

@@ -0,0 +1,76 @@
+package cn.com.qmth.examcloud.web.cache;
+
+import cn.com.qmth.examcloud.api.commons.enums.BasicDataType;
+import cn.com.qmth.examcloud.api.commons.exchange.BaseRequest;
+
+/**
+ * 刷新Hash缓存请求
+ *
+ * @author 
+ * @date 2019年5月9日
+ * @Copyright (c) 2018-? http://qmth.com.cn All Rights Reserved.
+ */
+public class RefreshHashCacheReq extends BaseRequest {
+
+
+	/**
+     * 
+     */
+    private static final long serialVersionUID = 3251806982238101789L;
+
+    private String className;
+
+	private String[] keys;
+
+	private BasicDataType[] typeArray;
+	
+	private String[] subKeys;
+
+    private BasicDataType[] subTypeArray;
+
+	public String getClassName() {
+		return className;
+	}
+
+	public void setClassName(String className) {
+		this.className = className;
+	}
+
+	public String[] getKeys() {
+		return keys;
+	}
+
+	public void setKeys(String[] keys) {
+		this.keys = keys;
+	}
+
+	public BasicDataType[] getTypeArray() {
+		return typeArray;
+	}
+
+	public void setTypeArray(BasicDataType[] typeArray) {
+		this.typeArray = typeArray;
+	}
+
+    
+    public String[] getSubKeys() {
+        return subKeys;
+    }
+
+    
+    public void setSubKeys(String[] subKeys) {
+        this.subKeys = subKeys;
+    }
+
+    
+    public BasicDataType[] getSubTypeArray() {
+        return subTypeArray;
+    }
+
+    
+    public void setSubTypeArray(BasicDataType[] subTypeArray) {
+        this.subTypeArray = subTypeArray;
+    }
+
+
+}

+ 26 - 0
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/redis/RedisClient.java

@@ -35,6 +35,13 @@ public interface RedisClient {
 	 * @param value
 	 */
 	public void set(String key, Object value);
+	
+	/**
+	 * @param key
+	 * @param subkey
+	 * @param value
+	 */
+	public void set(String key,String subkey, Object value);
 
 	/**
 	 * 方法注释
@@ -45,6 +52,14 @@ public interface RedisClient {
 	 * @param timeout
 	 */
 	public void set(String key, Object value, int timeout);
+	
+    /**
+     * @param key
+     * @param subkey
+     * @param value
+     * @param timeout
+     */
+    public void set(String key,String subkey, Object value, int timeout);
 
 	/**
 	 * 方法注释
@@ -85,6 +100,15 @@ public interface RedisClient {
 	 * @return
 	 */
 	public <T> T get(String key, Class<T> c);
+	
+	/** hash get
+	 * @param <T>
+	 * @param key
+	 * @param subkey
+	 * @param c
+	 * @return
+	 */
+	public <T> T get(String key, String subkey, Class<T> c);
 
 	/**
 	 * 方法注释
@@ -93,6 +117,8 @@ public interface RedisClient {
 	 * @param key
 	 */
 	public void delete(String key);
+	
+	public void delete(String key,String subkey);
 
 	/**
 	 * 方法注释

+ 41 - 5
examcloud-web/src/main/java/cn/com/qmth/examcloud/web/redis/SimpleRedisClient.java

@@ -54,8 +54,7 @@ public final class SimpleRedisClient implements RedisClient {
 	public void set(String key, Object value, int timeout) {
 		long s = System.currentTimeMillis();
 		beforeMethod();
-		set(key, value);
-		expire(key, timeout);
+		redisTemplate.opsForValue().set(key, value,timeout,TimeUnit.SECONDS);
 		afterMethod("set(String key, Object value, int timeout)", s);
 	}
 
@@ -105,12 +104,22 @@ public final class SimpleRedisClient implements RedisClient {
 		return t;
 	}
 
+
+    @Override
+    public <T> T get(String key,String hashKey, Class<T> c) {
+        long s = System.currentTimeMillis();
+        beforeMethod();
+        Object object = redisTemplate.opsForHash().get(key,hashKey);
+        @SuppressWarnings("unchecked")
+        T t = (T) object;
+        afterMethod("get(String key, Class<T> c)", s);
+        return t;
+    }
 	@Override
 	public void delete(String key) {
 		long s = System.currentTimeMillis();
 		beforeMethod();
-		redisTemplate.opsForValue().set(key, null);
-		expire(key, 0);
+		redisTemplate.expire(key,0,TimeUnit.SECONDS);
 		afterMethod("delete(String key)", s);
 	}
 
@@ -146,4 +155,31 @@ public final class SimpleRedisClient implements RedisClient {
 		return redisTemplate.getExpire(key, timeUnit);
 	}
 
-}
+    @Override
+    public void set(String key, String subkey, Object value, int timeout) {
+        long s = System.currentTimeMillis();
+        beforeMethod();
+		expire(key, timeout);
+		set(key,subkey, value);
+        afterMethod("set(String key,String subkey,Object value, int timeout)", s);
+
+    }
+
+    @Override
+    public void delete(String key, String subkey) {
+        long s = System.currentTimeMillis();
+        beforeMethod();
+        redisTemplate.opsForHash().delete(key, subkey);
+        afterMethod("delete(String key,String subkey)", s);
+    }
+
+    @Override
+    public void set(String key, String subkey, Object value) {
+        long s = System.currentTimeMillis();
+        beforeMethod();
+        redisTemplate.opsForHash().put(key,subkey, value);
+        afterMethod("set(String key,String subkey, Object value)", s);
+    }
+
+
+}