[Add] CacheStore-Redis 添加 List 组件;

[Add] RedisListCacheStore 添加完整的 List 组件;
[Add] RedisListCacheStoreTest 完整单元测试类;
[Update] RedisCacheStoreFactory 添加对 List 组件的提供;
This commit is contained in:
LamGC 2021-01-11 16:38:30 +08:00
parent 752cf907d6
commit 8d9debeb1b
Signed by: LamGC
GPG Key ID: 6C5AE2A913941E1D
3 changed files with 519 additions and 1 deletions

View File

@ -90,7 +90,7 @@ public class RedisCacheStoreFactory implements CacheStoreFactory {
@Override
public <E> ListCacheStore<E> newListCacheStore(String identify, StringConverter<E> converter) {
throw new GetCacheStoreException("No corresponding implementation");
return new RedisListCacheStore<>(connectionPool, identify, converter);
}
@Override

View File

@ -0,0 +1,167 @@
/*
* Copyright (C) 2021 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.
*
* 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.ListCacheStore;
import net.lamgc.cgj.bot.cache.convert.StringConverter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
/**
* Redis 列表缓存存储容器.
* @param <E>
* @author LamGC
*/
public class RedisListCacheStore<E> extends RedisCacheStore<List<E>> implements ListCacheStore<E> {
private final String keyPrefix;
private final StringConverter<E> converter;
private final RedisConnectionPool connectionPool;
public RedisListCacheStore(RedisConnectionPool connectionPool, String keyPrefix, StringConverter<E> converter) {
super(connectionPool);
this.connectionPool = connectionPool;
keyPrefix = Objects.requireNonNull(keyPrefix).trim();
if (keyPrefix.isEmpty()) {
throw new IllegalArgumentException("Key prefix cannot be empty.");
}
if (keyPrefix.endsWith(RedisUtils.KEY_SEPARATOR)) {
this.keyPrefix = keyPrefix;
} else {
this.keyPrefix = keyPrefix + RedisUtils.KEY_SEPARATOR;
}
this.converter = Objects.requireNonNull(converter);
}
@Override
public E getElement(CacheKey key, int index) {
return connectionPool.executeRedis(jedis -> converter.from(jedis.lindex(getKeyString(key), index)));
}
@Override
public List<E> getElementsByRange(CacheKey key, int index, int length) {
List<String> strings = connectionPool.executeRedis(jedis ->
// stop = start + length - 1
jedis.lrange(getKeyString(key), index, index + length - 1));
List<E> result = new ArrayList<>(strings.size());
strings.forEach(element -> result.add(converter.from(element)));
return result;
}
/**
* {@inheritDoc}
*
* <p>注意: Redis 实现中, 该功能通过一段 Lua 脚本实现,
* 由于 Redis 并没有原生支持该功能, 所以只能用脚本遍历查找.
* 如果 List 元素过多, 可能会导致执行缓慢且影响后续操作, 谨慎使用.
* @param key 待操作的缓存项键名.
* @param index 欲删除元素的索引, 0 开始.
* @return 如果元素存在且删除成功, 返回 true.
* @throws NullPointerException key null 时抛出.
*/
@Override
public boolean removeElement(CacheKey key, int index) {
List<String> keys = new ArrayList<>(1);
List<String> args = new ArrayList<>(1);
keys.add(getKeyString(key));
args.add(String.valueOf(index));
Number result = (Number) connectionPool.executeScript(LuaScript.LIST_REMOVE_ELEMENT_BY_INDEX, keys, args);
return result.intValue() == 1;
}
@Override
public boolean removeElement(CacheKey key, E element) {
return connectionPool.executeRedis(jedis ->
jedis.lrem(getKeyString(key), 1, converter.to(element)) != RedisUtils.RETURN_CODE_FAILED);
}
@Override
public boolean addElement(CacheKey key, E element) {
Objects.requireNonNull(element);
return connectionPool.executeRedis(jedis ->
jedis.lpush(getKeyString(key), converter.to(element)) != RedisUtils.RETURN_CODE_FAILED);
}
@Override
public boolean addElements(CacheKey key, Collection<E> elements) {
Objects.requireNonNull(elements);
if (elements.size() == 0) {
return exists(key);
}
List<E> values = new ArrayList<>(elements);
String[] valueStrings = new String[values.size()];
for (int i = 0; i < valueStrings.length; i++) {
valueStrings[i] = converter.to(values.get(i));
}
return connectionPool.executeRedis(jedis ->
jedis.lpush(getKeyString(key), valueStrings) != RedisUtils.RETURN_CODE_FAILED);
}
/**
* {@inheritDoc}
*
* <p>注意: Redis 实现中, 该功能通过一段 Lua 脚本实现,
* 由于 Redis 并没有原生支持该功能, 所以只能用脚本遍历查找.
* 如果 List 元素过多, 可能会导致执行缓慢且影响后续操作, 谨慎使用.
* @param key 待检查的缓存项键名.
* @param element 待查找的缓存值.
* @return 如果存在, 返回 true, 如果元素不存在, 或缓存项不存在, 返回 false.
* @throws NullPointerException key element null 时抛出; 本方法不允许存储 null , 因为 null 代表"没有/不存在".
*/
@Override
public boolean containsElement(CacheKey key, E element) {
List<String> keys = new ArrayList<>(1);
List<String> args = new ArrayList<>(1);
keys.add(getKeyString(key));
args.add(converter.to(element));
Number result = (Number) connectionPool.executeScript(LuaScript.LIST_CHECK_ELEMENT_CONTAINS, keys, args);
return result.intValue() != -1;
}
@Override
public boolean isEmpty(CacheKey key) {
return elementsLength(key) == -1;
}
@Override
public int elementsLength(CacheKey key) {
long result = connectionPool.executeRedis(jedis -> jedis.llen(getKeyString(key)));
if (result == 0) {
return -1;
} else {
return (int) result;
}
}
@Override
public boolean clearCollection(CacheKey key) {
return remove(key);
}
@Override
protected String getKeyPrefix() {
return this.keyPrefix;
}
}

View File

@ -0,0 +1,351 @@
/*
* Copyright (C) 2021 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.
*
* 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.ListCacheStore;
import net.lamgc.cgj.bot.cache.convert.StringConverter;
import net.lamgc.cgj.bot.cache.convert.StringToStringConverter;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import redis.clients.jedis.Jedis;
import java.lang.reflect.Field;
import java.util.*;
/**
* @see RedisListCacheStore
*/
public class RedisListCacheStoreTest {
private final Jedis jedis = new Jedis();
private final static StringConverter<String> CONVERTER = new StringToStringConverter();
private final static String IDENTIFY = "test:list";
private static RedisConnectionPool connectionPool;
@BeforeClass
public static void beforeAllTest() {
connectionPool = new RedisConnectionPool();
connectionPool.reconnectRedis();
Assert.assertTrue("Redis is not connected.", connectionPool.available());
}
private ListCacheStore<String> newListCacheStore() {
return new RedisListCacheStore<>(connectionPool, IDENTIFY, CONVERTER);
}
private Set<String> getListElements(String key) {
Set<String> actualElements = new HashSet<>();
for (long i = 0; i < jedis.llen(key); i++) {
actualElements.add(jedis.lindex(key, i));
}
return actualElements;
}
@Before
public void beforeTest() {
Set<String> keys = jedis.keys(RedisUtils.toRedisCacheKey(IDENTIFY, RedisUtils.CACHE_KEY_ALL));
for (String key : keys) {
jedis.del(key);
}
}
@Test
public void prefixCheck() throws NoSuchFieldException, IllegalAccessException {
final Field prefixField = RedisListCacheStore.class.getDeclaredField("keyPrefix");
prefixField.setAccessible(true);
String prefix = (String) prefixField.get(new RedisListCacheStore<>(connectionPool, IDENTIFY, CONVERTER));
Assert.assertTrue(prefix.endsWith(RedisUtils.KEY_SEPARATOR));
prefix = (String) prefixField.get(new RedisListCacheStore<>(connectionPool,
IDENTIFY + RedisUtils.KEY_SEPARATOR, CONVERTER));
Assert.assertTrue(prefix.endsWith(RedisUtils.KEY_SEPARATOR));
prefixField.setAccessible(false);
}
@Test(expected = IllegalArgumentException.class)
public void emptyPrefixTest() {
new RedisListCacheStore<>(connectionPool, "", CONVERTER);
}
@Test(expected = NullPointerException.class)
public void nullPrefixTest() {
new RedisListCacheStore<>(connectionPool, null, CONVERTER);
}
@Test
public void addElementTest() {
ListCacheStore<String> listCacheStore = newListCacheStore();
final CacheKey listKey = new CacheKey("list_add_element");
final String element = "test";
final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey);
Assert.assertTrue(listCacheStore.addElement(listKey, element));
Assert.assertEquals(1, jedis.llen(listKeyStr).intValue());
Assert.assertEquals(element, jedis.lpop(listKeyStr));
}
@Test
public void addElementsTest() {
ListCacheStore<String> listCacheStore = newListCacheStore();
final CacheKey listKey = new CacheKey("list_add_elements");
final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey);
final Set<String> expectedElements = new HashSet<>();
expectedElements.add("a");
expectedElements.add("b");
expectedElements.add("c");
expectedElements.add("d");
expectedElements.add("e");
Assert.assertEquals("The key does not exist, but the empty collection was added successfully.",
jedis.exists(listKeyStr),
listCacheStore.addElements(listKey, Collections.emptyList()));
Assert.assertTrue(listCacheStore.addElements(listKey, expectedElements));
Assert.assertEquals(expectedElements.size(), jedis.llen(listKeyStr).intValue());
Set<String> actualElements = getListElements(listKeyStr);
Assert.assertTrue(actualElements.containsAll(expectedElements));
Assert.assertEquals("Key does not exist, but adding empty collection failed.",
jedis.exists(listKeyStr),
listCacheStore.addElements(listKey, Collections.emptySet()));
}
@Test
public void removeElementByElementTest() {
ListCacheStore<String> listCacheStore = newListCacheStore();
final CacheKey listKey = new CacheKey("list_remove_element_by_element");
final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey);
final List<String> expectedElements = new ArrayList<>();
expectedElements.add("a");
expectedElements.add("b");
expectedElements.add("c");
expectedElements.add("d");
expectedElements.add("e");
final String[] expectedElementsArr = new String[expectedElements.size()];
expectedElements.toArray(expectedElementsArr);
jedis.del(listKeyStr);
Assert.assertFalse(listCacheStore.removeElement(listKey, "NoExistElement"));
Assert.assertNotEquals("The expected create operation failed.",
RedisUtils.RETURN_CODE_FAILED, jedis.lpush(listKeyStr, expectedElementsArr).intValue());
Random random = new Random();
final int deletedIndex = random.nextInt(expectedElements.size());
Assert.assertTrue("The operation to be tested failed.",
listCacheStore.removeElement(listKey, expectedElements.get(deletedIndex)));
expectedElements.remove(deletedIndex);
Assert.assertEquals(expectedElements.size(), jedis.llen(listKeyStr).intValue());
Set<String> actualElements = getListElements(listKeyStr);
Assert.assertTrue(actualElements.containsAll(expectedElements));
}
@Test
public void removeElementByIndexTest() {
ListCacheStore<String> listCacheStore = newListCacheStore();
final CacheKey listKey = new CacheKey("list_remove_element_by_index");
final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey);
final Set<String> expectedElements = new HashSet<>();
expectedElements.add("a");
expectedElements.add("b");
expectedElements.add("c");
expectedElements.add("d");
expectedElements.add("e");
final String[] expectedElementsArr = new String[expectedElements.size()];
expectedElements.toArray(expectedElementsArr);
jedis.del(listKeyStr);
// 尝试删除不存在的 Key
Assert.assertFalse(listCacheStore.removeElement(listKey, 0));
Assert.assertNotEquals("The expected create operation failed.",
RedisUtils.RETURN_CODE_FAILED, jedis.lpush(listKeyStr, expectedElementsArr).intValue());
Assert.assertFalse(
listCacheStore.removeElement(listKey, jedis.llen(listKeyStr).intValue()));
Random random = new Random();
final int deletedIndex = random.nextInt(expectedElements.size());
String deletedElement = jedis.lindex(listKeyStr, deletedIndex);
Assert.assertTrue("The operation to be tested failed.",
listCacheStore.removeElement(listKey, deletedIndex));
expectedElements.remove(deletedElement);
Assert.assertEquals(expectedElements.size(), jedis.llen(listKeyStr).intValue());
Set<String> actualElements = getListElements(listKeyStr);
Assert.assertTrue(actualElements.containsAll(expectedElements));
}
@Test
public void containsElementTest() {
ListCacheStore<String> listCacheStore = newListCacheStore();
final CacheKey listKey = new CacheKey("list_contains_element");
final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey);
final Set<String> expectedElements = new HashSet<>();
expectedElements.add("a");
expectedElements.add("b");
expectedElements.add("c");
expectedElements.add("d");
expectedElements.add("e");
final String[] expectedElementsArr = new String[expectedElements.size()];
expectedElements.toArray(expectedElementsArr);
Assert.assertNotEquals(-1, jedis.lpush(listKeyStr, expectedElementsArr).intValue());
Set<String> actualElements = getListElements(listKeyStr);
expectedElements.add("f");
expectedElements.add("g");
expectedElements.add("h");
expectedElements.add("i");
for (String expectedElement : expectedElements) {
Assert.assertEquals(String.format("Make a difference: '%s'", expectedElement),
actualElements.contains(expectedElement),
listCacheStore.containsElement(listKey, expectedElement));
}
}
@Test
public void isEmptyTest() {
ListCacheStore<String> listCacheStore = newListCacheStore();
final CacheKey listKey = new CacheKey("list_is_empty");
final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey);
Assert.assertEquals(!jedis.exists(listKeyStr), listCacheStore.isEmpty(listKey));
jedis.lpush(listKeyStr, "test");
Assert.assertEquals(jedis.exists(listKeyStr), !listCacheStore.isEmpty(listKey));
}
@Test
public void elementsLengthTest() {
ListCacheStore<String> listCacheStore = newListCacheStore();
final CacheKey listKey = new CacheKey("list_elements_length");
final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey);
final Set<String> expectedElements = new HashSet<>();
expectedElements.add("a");
expectedElements.add("b");
expectedElements.add("c");
expectedElements.add("d");
expectedElements.add("e");
final String[] expectedElementsArr = new String[expectedElements.size()];
expectedElements.toArray(expectedElementsArr);
long beforeLength = jedis.llen(listKeyStr);
if (jedis.llen(listKeyStr) == 0) {
Assert.assertEquals(-1, listCacheStore.elementsLength(listKey));
} else {
Assert.assertEquals(beforeLength, listCacheStore.elementsLength(listKey));
}
jedis.del(listKeyStr);
jedis.lpush(listKeyStr, expectedElementsArr);
Assert.assertEquals(jedis.llen(listKeyStr).intValue(), listCacheStore.elementsLength(listKey));
}
@Test
public void clearCollectionTest() {
ListCacheStore<String> listCacheStore = newListCacheStore();
final CacheKey listKey = new CacheKey("list_clear_collection");
final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey);
final Set<String> expectedElements = new HashSet<>();
expectedElements.add("a");
expectedElements.add("b");
expectedElements.add("c");
expectedElements.add("d");
expectedElements.add("e");
final String[] expectedElementsArr = new String[expectedElements.size()];
expectedElements.toArray(expectedElementsArr);
Assert.assertEquals(jedis.exists(listKeyStr), listCacheStore.clearCollection(listKey));
jedis.lpush(listKeyStr, expectedElementsArr);
Assert.assertTrue(listCacheStore.clearCollection(listKey));
Assert.assertEquals(0, jedis.llen(listKeyStr).intValue());
Assert.assertFalse(jedis.exists(listKeyStr));
}
@Test
public void getElementTest() {
ListCacheStore<String> listCacheStore = newListCacheStore();
final CacheKey listKey = new CacheKey("list_get_element");
final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey);
final List<String> expectedElements = new ArrayList<>();
expectedElements.add("a");
expectedElements.add("b");
expectedElements.add("c");
expectedElements.add("d");
expectedElements.add("e");
final String[] expectedElementsArr = new String[expectedElements.size()];
expectedElements.toArray(expectedElementsArr);
jedis.lpush(listKeyStr, expectedElementsArr);
Collections.reverse(expectedElements);
for (int i = 0; i < expectedElements.size(); i++) {
Assert.assertEquals("index: " + i, expectedElements.get(i),
listCacheStore.getElement(listKey, i));
}
}
@Test
public void getElementsByRangeTest() {
ListCacheStore<String> listCacheStore = newListCacheStore();
final CacheKey listKey = new CacheKey("list_get_elements_by_range");
final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey);
final List<String> expectedElements = new ArrayList<>();
expectedElements.add("a");
expectedElements.add("b");
expectedElements.add("c");
expectedElements.add("d");
expectedElements.add("e");
expectedElements.add("f");
expectedElements.add("g");
expectedElements.add("h");
expectedElements.add("i");
expectedElements.add("j");
expectedElements.add("k");
final String[] expectedElementsArr = new String[expectedElements.size()];
expectedElements.toArray(expectedElementsArr);
jedis.lpush(listKeyStr, expectedElementsArr);
Collections.reverse(expectedElements);
final int start = 2;
final int length = 4;
List<String> actualElements = listCacheStore.getElementsByRange(listKey, start, length);
for (int i = 0; i < length; i++) {
Assert.assertEquals(expectedElements.get(start + i), actualElements.get(i));
}
}
}