[Add][Update] CacheStore-redis 添加 Redis 缓存存储容器的实现模块;

[Add] META-INF/services/* 支持 SPI 机制;
[Add] RedisCacheStore, RedisCacheStoreFactory, RedisConnectionPool, RedisUtils 添加相关父类和辅助类;
[Add] RedisMapCacheStore, RedisMapCacheStoreTest 添加映射表缓存相关实现及完整单元测试;
[Add] RedisSingleCacheStore, RedisSingleCacheStoreTest 添加单项缓存相关实现及完整单元测试;
[Update] redis.clients:jedis 更新依赖项版本(3.2.0 -> 3.3.0);
This commit is contained in:
2020-09-12 03:15:37 +08:00
parent 862ddc871e
commit c856d94384
10 changed files with 883 additions and 1 deletions

View File

@ -0,0 +1,102 @@
/*
* Copyright (C) 2020 LamGC
*
* ContentGrabbingJi is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* ContentGrabbingJi is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.lamgc.cgj.bot.cache.redis;
import net.lamgc.cgj.bot.cache.CacheKey;
import net.lamgc.cgj.bot.cache.CacheStore;
import redis.clients.jedis.Jedis;
import java.util.HashSet;
import java.util.Set;
/**
*
* @author LamGC
*/
public abstract class RedisCacheStore<V> implements CacheStore<V> {
/**
* 获取 Key 前缀.
* <p>key = getKeyPrefix() + key
* @param cacheKey CacheKey 对象.
* @return 返回 Key 前缀.
*/
protected String getKeyString(CacheKey cacheKey) {
return getKeyPrefix() + cacheKey.join(RedisUtils.KEY_SEPARATOR);
}
/**
* 获取 Key 的完整前缀.
* @return 返回完整前缀.
*/
protected abstract String getKeyPrefix();
@Override
public boolean setTimeToLive(CacheKey key, long ttl) {
String keyString = getKeyString(key);
return RedisConnectionPool.executeRedis(jedis -> {
Long result;
if (ttl >= 0) {
result = jedis.pexpire(keyString, ttl);
} else {
result = jedis.persist(keyString);
}
return result == RedisUtils.RETURN_CODE_OK;
});
}
@Override
public long getTimeToLive(CacheKey key) {
return RedisConnectionPool.executeRedis(jedis -> {
Long ttl = jedis.pttl(getKeyString(key));
return ttl < 0 ? -1 : ttl;
});
}
@Override
public long size() {
return RedisConnectionPool.executeRedis(Jedis::dbSize);
}
@Override
public boolean clear() {
return RedisConnectionPool.executeRedis(jedis -> RedisUtils.isOk(jedis.flushDB()));
}
@Override
public boolean exists(CacheKey key) {
return RedisConnectionPool.executeRedis(jedis -> jedis.exists(getKeyString(key)));
}
@Override
public boolean remove(CacheKey key) {
return RedisConnectionPool.executeRedis(jedis -> jedis.del(getKeyString(key)) == RedisUtils.RETURN_CODE_OK);
}
@Override
public Set<String> keySet() {
Set<String> keys = RedisConnectionPool.executeRedis(jedis -> jedis.keys(RedisUtils.KEY_PATTERN_ALL));
final int prefixLength = getKeyPrefix().length();
Set<String> newKeys = new HashSet<>();
for (String key : keys) {
newKeys.add(key.substring(prefixLength));
}
return newKeys;
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (C) 2020 LamGC
*
* ContentGrabbingJi is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* ContentGrabbingJi is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.lamgc.cgj.bot.cache.redis;
import net.lamgc.cgj.bot.cache.*;
import net.lamgc.cgj.bot.cache.convert.StringConverter;
/**
*
* @author LamGC
*/
@Factory(name = "Redis")
public class RedisCacheStoreFactory implements CacheStoreFactory {
@Override
public <V> SingleCacheStore<V> newSingleCacheStore(String identify, StringConverter<V> converter) {
return new RedisSingleCacheStore<>(identify, converter);
}
@Override
public <E> ListCacheStore<E> newListCacheStore(String identify, StringConverter<E> converter) {
return null;
}
@Override
public <E> SetCacheStore<E> newSetCacheStore(String identify, StringConverter<E> converter) {
return null;
}
@Override
public <V> MapCacheStore<V> newMapCacheStore(String identify, StringConverter<V> converter) {
return new RedisMapCacheStore<>(identify, converter);
}
@Override
public boolean canGetCacheStore() {
return RedisConnectionPool.available();
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright (C) 2020 LamGC
*
* ContentGrabbingJi is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* ContentGrabbingJi is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.lamgc.cgj.bot.cache.redis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
/**
* 统一的 Redis 连接池.
* @author LamGC
*/
public class RedisConnectionPool {
private final static Logger log = LoggerFactory.getLogger(RedisConnectionPool.class);
private final static AtomicReference<JedisPool> POOL = new AtomicReference<>();
public static synchronized void reconnectRedis() {
JedisPool jedisPool = POOL.get();
if (jedisPool != null && !jedisPool.isClosed()) {
return;
}
JedisPoolConfig config = new JedisPoolConfig();
config.setTestOnBorrow(true);
config.setTestOnReturn(true);
jedisPool = new JedisPool(config);
POOL.set(jedisPool);
}
/**
* 获取一个 Jedis 对象.
* <p>注意, 需回收 Jedis 对象, 否则可能会耗尽连接池导致后续操作受到影响.
* @return 返回可用的 Jedis 连接.
*/
public static Jedis getConnection() {
JedisPool pool = POOL.get();
if (pool == null || pool.isClosed()) {
reconnectRedis();
pool = POOL.get();
if (pool == null) {
throw new IllegalStateException("Redis connection lost");
}
}
return pool.getResource();
}
/**
* 执行 Redis 操作并返回相关值.
* <p>本方法会自动回收 Jedis.
* @param function 待运行的操作.
* @param <R> 返回值类型.
* @return 返回 function 返回的内容.
*/
public static <R> R executeRedis(Function<Jedis, R> function) {
try (Jedis jedis = getConnection()) {
return function.apply(jedis);
}
}
/**
* 检查 Redis 连接池是否有可用的资源.
* @return 如果连接池依然活跃, 返回 true.
*/
public static boolean available() {
JedisPool jedisPool = POOL.get();
if (jedisPool == null || jedisPool.isClosed()) {
reconnectRedis();
if (jedisPool == null || jedisPool.isClosed()) {
return false;
}
}
if (jedisPool.getNumIdle() == 0) {
try (Jedis jedis = jedisPool.getResource()) {
return "pong".equalsIgnoreCase(jedis.ping());
} catch (Exception e) {
log.error("Redis 连接测试时发生异常", e);
}
return false;
}
return true;
}
}

View File

@ -0,0 +1,175 @@
/*
* Copyright (C) 2020 LamGC
*
* ContentGrabbingJi is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* ContentGrabbingJi is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.lamgc.cgj.bot.cache.redis;
import com.google.common.base.Strings;
import net.lamgc.cgj.bot.cache.CacheKey;
import net.lamgc.cgj.bot.cache.MapCacheStore;
import net.lamgc.cgj.bot.cache.convert.StringConverter;
import java.util.*;
/**
* Redis Map缓存存储容器.
* @param <V> 值类型.
* @author LamGC
*/
public class RedisMapCacheStore<V> extends RedisCacheStore<Map<String, V>> implements MapCacheStore<V> {
private final String keyPrefix;
private final StringConverter<V> converter;
public RedisMapCacheStore(String keyPrefix, StringConverter<V> converter) {
keyPrefix = Strings.nullToEmpty(keyPrefix).trim();
if (!keyPrefix.isEmpty() && keyPrefix.endsWith(RedisUtils.KEY_SEPARATOR)) {
this.keyPrefix = keyPrefix;
} else {
this.keyPrefix = keyPrefix + RedisUtils.KEY_SEPARATOR;
}
this.converter = Objects.requireNonNull(converter);
}
@Override
protected String getKeyPrefix() {
return this.keyPrefix;
}
@Override
public int mapSize(CacheKey key) {
return RedisConnectionPool.executeRedis(jedis -> {
String keyString = getKeyString(key);
if (jedis.exists(keyString)) {
return jedis.hlen(keyString).intValue();
}
return 0;
});
}
@Override
public Set<String> mapFieldSet(CacheKey key) {
return RedisConnectionPool.executeRedis(jedis -> {
String keyString = getKeyString(key);
if (jedis.exists(keyString)) {
return jedis.hkeys(keyString);
}
return null;
});
}
@Override
public Set<V> mapValueSet(CacheKey key) {
List<String> rawValueSet = RedisConnectionPool.executeRedis(jedis -> {
String keyString = getKeyString(key);
if (jedis.exists(keyString)) {
return jedis.hvals(keyString);
}
return null;
});
if (rawValueSet == null) {
return null;
}
Set<V> result = new HashSet<>();
for (String rawValue : rawValueSet) {
result.add(converter.from(rawValue));
}
return result;
}
@Override
public boolean put(CacheKey key, String field, V value) {
Objects.requireNonNull(field);
Objects.requireNonNull(value);
return RedisConnectionPool.executeRedis(jedis -> {
String keyString = getKeyString(key);
return jedis.hset(keyString, field, converter.to(value)) == RedisUtils.RETURN_CODE_OK;
});
}
@Override
public boolean putAll(CacheKey key, Map<String, V> map) {
Objects.requireNonNull(key);
Objects.requireNonNull(map);
if (map.size() == 0 && exists(key)) {
return true;
}
final Map<String, String> targetMap = new HashMap<>(map.size());
map.forEach((k, v) -> targetMap.put(k, converter.to(v)));
return RedisConnectionPool.executeRedis(jedis -> {
String keyString = getKeyString(key);
return RedisUtils.isOk(jedis.hmset(keyString, targetMap));
});
}
@Override
public boolean putIfNotExist(CacheKey key, String field, V value) {
Objects.requireNonNull(field);
Objects.requireNonNull(value);
return RedisConnectionPool.executeRedis(jedis -> {
String keyString = getKeyString(key);
return jedis.hsetnx(keyString, field, converter.to(value)) == RedisUtils.RETURN_CODE_OK;
});
}
@Override
public V get(CacheKey key, String field) {
Objects.requireNonNull(field);
String value = RedisConnectionPool.executeRedis(jedis -> jedis.hget(getKeyString(key), field));
if (value == null) {
return null;
}
return converter.from(value);
}
@Override
public boolean removeField(CacheKey key, String field) {
Objects.requireNonNull(field);
return RedisConnectionPool.executeRedis(jedis ->
jedis.hdel(getKeyString(key), field) == RedisUtils.RETURN_CODE_OK);
}
@Override
public boolean containsField(CacheKey key, String field) {
Objects.requireNonNull(field);
return RedisConnectionPool.executeRedis(jedis -> jedis.hexists(getKeyString(key), field));
}
@Override
public boolean mapIsEmpty(CacheKey key) {
return mapSize(key) == 0;
}
@Override
public boolean clearMap(CacheKey key) {
Set<String> fields = mapFieldSet(key);
if (fields == null) {
return false;
} else if (fields.size() == 0) {
return true;
}
String[] fieldsArray = new String[fields.size()];
fields.toArray(fieldsArray);
return RedisConnectionPool.executeRedis(jedis ->
jedis.hdel(getKeyString(key), fieldsArray) != RedisUtils.RETURN_CODE_FAILED);
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright (C) 2020 LamGC
*
* ContentGrabbingJi is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* ContentGrabbingJi is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.lamgc.cgj.bot.cache.redis;
import com.google.common.base.Strings;
import net.lamgc.cgj.bot.cache.CacheKey;
import net.lamgc.cgj.bot.cache.SingleCacheStore;
import net.lamgc.cgj.bot.cache.convert.StringConverter;
import java.util.Objects;
/**
* Redis 单项缓存存储容器.
* @param <V> 值类型.
* @see net.lamgc.cgj.bot.cache.CacheStore
* @see net.lamgc.cgj.bot.cache.SingleCacheStore
* @see net.lamgc.cgj.bot.cache.redis.RedisCacheStore
* @author LamGC
*/
public class RedisSingleCacheStore<V> extends RedisCacheStore<V> implements SingleCacheStore<V> {
private final String keyPrefix;
private final StringConverter<V> converter;
public RedisSingleCacheStore(String keyPrefix, StringConverter<V> converter) {
keyPrefix = Strings.nullToEmpty(keyPrefix).trim();
if (!keyPrefix.isEmpty() && keyPrefix.endsWith(RedisUtils.KEY_SEPARATOR)) {
this.keyPrefix = keyPrefix;
} else {
this.keyPrefix = keyPrefix + RedisUtils.KEY_SEPARATOR;
}
this.converter = Objects.requireNonNull(converter);
}
@Override
public boolean set(CacheKey key, V value) {
return RedisConnectionPool.executeRedis(jedis ->
RedisUtils.isOk(jedis.set(getKeyString(key), converter.to(Objects.requireNonNull(value)))));
}
@Override
public boolean setIfNotExist(CacheKey key, V value) {
return RedisConnectionPool.executeRedis(jedis ->
jedis.setnx(getKeyString(key), converter.to(Objects.requireNonNull(value)))
== RedisUtils.RETURN_CODE_OK);
}
@Override
public V get(CacheKey key) {
String value = RedisConnectionPool.executeRedis(jedis -> jedis.get(getKeyString(key)));
if (value == null) {
return null;
}
return converter.from(value);
}
@Override
protected String getKeyPrefix() {
return this.keyPrefix;
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (C) 2020 LamGC
*
* ContentGrabbingJi is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* ContentGrabbingJi is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.lamgc.cgj.bot.cache.redis;
/**
* @author LamGC
*/
public class RedisUtils {
/**
* 返回码 - 成功
*/
public final static int RETURN_CODE_OK = 1;
/**
* 返回码 - 失败
*/
public final static int RETURN_CODE_FAILED = 0;
/**
* Key匹配规则 - 所有Key
*/
public final static String KEY_PATTERN_ALL = "*";
/**
* Key 分隔符
*/
public final static String KEY_SEPARATOR = ":";
/**
* 检查字符串返回结果是否为操作成功.
* @param result 字符串返回结果.
* @return 如果为操作成功, 返回 true.
*/
public static boolean isOk(String result) {
return "OK".equalsIgnoreCase(result);
}
}

View File

@ -0,0 +1,18 @@
#
# Copyright (C) 2020 LamGC
#
# ContentGrabbingJi is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# ContentGrabbingJi is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
net.lamgc.cgj.bot.cache.redis.RedisCacheStoreFactory