| | |
| | | package com.zy.common.utils; |
| | | |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.data.redis.core.RedisTemplate; |
| | | import org.springframework.stereotype.Component; |
| | | import org.springframework.util.CollectionUtils; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.regex.Pattern; |
| | | |
| | | /** |
| | | * redisTemplate封装 |
| | | * |
| | | */ |
| | | //@Component |
| | | @Slf4j |
| | | @Component |
| | | public class RedisUtil { |
| | | |
| | | @Autowired |
| | | private RedisTemplate<String, Object> redisTemplate; |
| | | private static final long LOCAL_NO_EXPIRE = Long.MAX_VALUE; |
| | | /** 无 TTL 写入 Redis 失败时,内存条目默认保留时长(秒) */ |
| | | private static final long LOCAL_DEFAULT_TTL_SEC = 86400L; |
| | | |
| | | public RedisUtil(RedisTemplate<String, Object> redisTemplate) { |
| | | // @Autowired |
| | | // private RedisTemplate<String, Object> redisTemplate; |
| | | |
| | | @Autowired |
| | | private RedisTemplate redisTemplate; |
| | | |
| | | private final ConcurrentHashMap<String, LocalCacheEntry> localCache = new ConcurrentHashMap<>(); |
| | | private final Object fallbackLock = new Object(); |
| | | private volatile boolean redisFallbackMode = false; |
| | | private final Set<String> fallbackLocalKeys = ConcurrentHashMap.newKeySet(); |
| | | |
| | | public RedisTemplate getRedisTemplate() { |
| | | return redisTemplate; |
| | | } |
| | | |
| | | public RedisUtil(RedisTemplate redisTemplate) { |
| | | this.redisTemplate = redisTemplate; |
| | | } |
| | | |
| | |
| | | return redisTemplate.hasKey(key); |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | return false; |
| | | return localHasValid(key); |
| | | } |
| | | } |
| | | |
| | |
| | | @SuppressWarnings("unchecked") |
| | | public void del(String... key) { |
| | | if (key != null && key.length > 0) { |
| | | if (key.length == 1) { |
| | | redisTemplate.delete(key[0]); |
| | | } else { |
| | | redisTemplate.delete(CollectionUtils.arrayToList(key)); |
| | | try { |
| | | if (key.length == 1) { |
| | | redisTemplate.delete(key[0]); |
| | | } else { |
| | | redisTemplate.delete(CollectionUtils.arrayToList(key)); |
| | | } |
| | | } catch (Exception e) { |
| | | // Redis 不可用时仍清理本地条目 |
| | | } |
| | | for (String k : key) { |
| | | if (k != null) { |
| | | localCache.remove(k); |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | * @return 值 |
| | | */ |
| | | public Object get(String key) { |
| | | return key == null ? null : redisTemplate.opsForValue().get(key); |
| | | if (key == null) { |
| | | return null; |
| | | } |
| | | try { |
| | | Object v = redisTemplate.opsForValue().get(key); |
| | | notifyRedisAvailable(); |
| | | return v; |
| | | } catch (Exception e) { |
| | | enterFallbackIfNeeded(); |
| | | log.info("Redis 不可用,从 JVM 内存读取 key={},原因: {}", key, e.getMessage()); |
| | | return localGet(key); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取全部数据 |
| | | * @return |
| | | */ |
| | | public HashMap<Object, Object> getRedis() { |
| | | Set<String> keys = redisTemplate.keys("*"); |
| | | HashMap<Object, Object> map = new HashMap<>(); |
| | | for (String key : keys) { |
| | | Object value = redisTemplate.opsForValue().get(key); |
| | | map.put(key, value); |
| | | } |
| | | |
| | | return map;//返回全部数据集合 |
| | | } |
| | | |
| | | /** |
| | |
| | | public boolean set(String key, Object value) { |
| | | try { |
| | | redisTemplate.opsForValue().set(key, value); |
| | | long start = System.currentTimeMillis(); |
| | | while (System.currentTimeMillis() - start < 10000) {//有效期10s |
| | | Object o = redisTemplate.opsForValue().get(key); |
| | | if (o == null) { |
| | | continue; |
| | | } |
| | | |
| | | if (o.equals(value)) { |
| | | break; |
| | | } |
| | | } |
| | | localCache.remove(key); |
| | | notifyRedisAvailable(); |
| | | return true; |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | return false; |
| | | log.debug("Redis set 失败", e); |
| | | localPutFallback(key, value, LOCAL_DEFAULT_TTL_SEC); |
| | | return true; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 普通缓存放入-异步 |
| | | * |
| | | * @param key 键 |
| | | * @param value 值 |
| | | * @return true成功 false失败 |
| | | */ |
| | | public boolean setAsync(String key, Object value) { |
| | | try { |
| | | redisTemplate.opsForValue().set(key, value); |
| | | localCache.remove(key); |
| | | notifyRedisAvailable(); |
| | | return true; |
| | | } catch (Exception e) { |
| | | log.debug("Redis setAsync 失败", e); |
| | | localPutFallback(key, value, LOCAL_DEFAULT_TTL_SEC); |
| | | return true; |
| | | } |
| | | } |
| | | |
| | |
| | | redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); |
| | | } else { |
| | | set(key, value); |
| | | return true; |
| | | } |
| | | localCache.remove(key); |
| | | notifyRedisAvailable(); |
| | | return true; |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | return false; |
| | | log.debug("Redis set(expire) 失败", e); |
| | | long ttl = time > 0 ? time : LOCAL_DEFAULT_TTL_SEC; |
| | | localPutFallback(key, value, ttl); |
| | | return true; |
| | | } |
| | | } |
| | | |
| | |
| | | * @return |
| | | */ |
| | | public Set keys(String pattern) { |
| | | return redisTemplate.keys(pattern); |
| | | try { |
| | | Set<?> s = redisTemplate.keys(pattern); |
| | | notifyRedisAvailable(); |
| | | return s; |
| | | } catch (Exception e) { |
| | | enterFallbackIfNeeded(); |
| | | log.info("Redis 不可用,从 JVM 内存匹配 keys,pattern={},原因: {}", pattern, e.getMessage()); |
| | | return localKeysMatching(pattern); |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | redisTemplate.convertAndSend(channel, message); |
| | | } |
| | | |
| | | private void enterFallbackIfNeeded() { |
| | | synchronized (fallbackLock) { |
| | | if (!redisFallbackMode) { |
| | | redisFallbackMode = true; |
| | | log.warn("Redis 不可用,字符串相关缓存将暂存于 JVM 内存,待 Redis 恢复后自动切回 Redis 并清理上述键"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void notifyRedisAvailable() { |
| | | if (!redisFallbackMode && fallbackLocalKeys.isEmpty()) { |
| | | return; |
| | | } |
| | | synchronized (fallbackLock) { |
| | | if (!redisFallbackMode && fallbackLocalKeys.isEmpty()) { |
| | | return; |
| | | } |
| | | int n = fallbackLocalKeys.size(); |
| | | for (String k : new ArrayList<>(fallbackLocalKeys)) { |
| | | localCache.remove(k); |
| | | } |
| | | fallbackLocalKeys.clear(); |
| | | redisFallbackMode = false; |
| | | if (n > 0) { |
| | | log.info("Redis 已恢复,已清理 JVM 内存中 {} 个因 Redis 不可用而暂存的键", n); |
| | | } else { |
| | | log.info("Redis 已恢复"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void localPutFallback(String key, Object value, long ttlSeconds) { |
| | | if (key == null) { |
| | | return; |
| | | } |
| | | synchronized (fallbackLock) { |
| | | if (!redisFallbackMode) { |
| | | redisFallbackMode = true; |
| | | log.warn("Redis 不可用,字符串相关缓存将暂存于 JVM 内存,待 Redis 恢复后自动切回 Redis 并清理上述键"); |
| | | } |
| | | fallbackLocalKeys.add(key); |
| | | localPut(key, value, ttlSeconds); |
| | | } |
| | | log.info("Redis 不可用,键已写入 JVM 内存: {}", key); |
| | | } |
| | | |
| | | private void localPut(String key, Object value, long ttlSeconds) { |
| | | if (key == null) { |
| | | return; |
| | | } |
| | | long expireAt = ttlSeconds > 0 |
| | | ? System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(ttlSeconds) |
| | | : LOCAL_NO_EXPIRE; |
| | | localCache.put(key, new LocalCacheEntry(value, expireAt)); |
| | | } |
| | | |
| | | private Object localGet(String key) { |
| | | LocalCacheEntry e = localCache.get(key); |
| | | if (e == null) { |
| | | return null; |
| | | } |
| | | if (e.expireAtMillis != LOCAL_NO_EXPIRE && System.currentTimeMillis() > e.expireAtMillis) { |
| | | localCache.remove(key, e); |
| | | return null; |
| | | } |
| | | return e.value; |
| | | } |
| | | |
| | | private boolean localHasValid(String key) { |
| | | return localGet(key) != null; |
| | | } |
| | | |
| | | private Set<String> localKeysMatching(String pattern) { |
| | | if (pattern == null) { |
| | | return new HashSet<>(); |
| | | } |
| | | Pattern p = Pattern.compile(globToRegex(pattern)); |
| | | Set<String> out = new HashSet<>(); |
| | | for (String k : localCache.keySet()) { |
| | | if (localGet(k) == null) { |
| | | continue; |
| | | } |
| | | if (p.matcher(k).matches()) { |
| | | out.add(k); |
| | | } |
| | | } |
| | | return out; |
| | | } |
| | | |
| | | private static String globToRegex(String glob) { |
| | | StringBuilder sb = new StringBuilder("^"); |
| | | for (int i = 0; i < glob.length(); i++) { |
| | | char c = glob.charAt(i); |
| | | if (c == '*') { |
| | | sb.append(".*"); |
| | | } else if (c == '?') { |
| | | sb.append('.'); |
| | | } else if ("\\.[]{}()+-^$|".indexOf(c) >= 0) { |
| | | sb.append('\\').append(c); |
| | | } else { |
| | | sb.append(c); |
| | | } |
| | | } |
| | | sb.append('$'); |
| | | return sb.toString(); |
| | | } |
| | | |
| | | private static final class LocalCacheEntry { |
| | | final Object value; |
| | | final long expireAtMillis; |
| | | |
| | | LocalCacheEntry(Object value, long expireAtMillis) { |
| | | this.value = value; |
| | | this.expireAtMillis = expireAtMillis; |
| | | } |
| | | } |
| | | |
| | | } |
| | | |