diff --git a/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisCacheStoreFactory.java b/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisCacheStoreFactory.java index 4032b52..142496e 100644 --- a/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisCacheStoreFactory.java +++ b/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisCacheStoreFactory.java @@ -90,7 +90,7 @@ public class RedisCacheStoreFactory implements CacheStoreFactory { @Override public ListCacheStore newListCacheStore(String identify, StringConverter converter) { - throw new GetCacheStoreException("No corresponding implementation"); + return new RedisListCacheStore<>(connectionPool, identify, converter); } @Override diff --git a/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisListCacheStore.java b/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisListCacheStore.java new file mode 100644 index 0000000..e42f209 --- /dev/null +++ b/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisListCacheStore.java @@ -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 . + */ + +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 + * @author LamGC + */ +public class RedisListCacheStore extends RedisCacheStore> implements ListCacheStore { + + private final String keyPrefix; + private final StringConverter converter; + private final RedisConnectionPool connectionPool; + + public RedisListCacheStore(RedisConnectionPool connectionPool, String keyPrefix, StringConverter 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 getElementsByRange(CacheKey key, int index, int length) { + List strings = connectionPool.executeRedis(jedis -> + // stop = start + length - 1 + jedis.lrange(getKeyString(key), index, index + length - 1)); + List result = new ArrayList<>(strings.size()); + strings.forEach(element -> result.add(converter.from(element))); + return result; + } + + /** + * {@inheritDoc} + * + *

注意: 在 Redis 实现中, 该功能通过一段 Lua 脚本实现, + * 由于 Redis 并没有原生支持该功能, 所以只能用脚本遍历查找. + * 如果 List 元素过多, 可能会导致执行缓慢且影响后续操作, 谨慎使用. + * @param key 待操作的缓存项键名. + * @param index 欲删除元素的索引, 从 0 开始. + * @return 如果元素存在且删除成功, 返回 true. + * @throws NullPointerException 当 key 为 null 时抛出. + */ + @Override + public boolean removeElement(CacheKey key, int index) { + List keys = new ArrayList<>(1); + List 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 elements) { + Objects.requireNonNull(elements); + if (elements.size() == 0) { + return exists(key); + } + + List 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} + * + *

注意: 在 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 keys = new ArrayList<>(1); + List 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; + } +} diff --git a/ContentGrabbingJi-CacheStore-redis/src/test/java/net/lamgc/cgj/bot/cache/redis/RedisListCacheStoreTest.java b/ContentGrabbingJi-CacheStore-redis/src/test/java/net/lamgc/cgj/bot/cache/redis/RedisListCacheStoreTest.java new file mode 100644 index 0000000..3a72ad0 --- /dev/null +++ b/ContentGrabbingJi-CacheStore-redis/src/test/java/net/lamgc/cgj/bot/cache/redis/RedisListCacheStoreTest.java @@ -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 . + */ + +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 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 newListCacheStore() { + return new RedisListCacheStore<>(connectionPool, IDENTIFY, CONVERTER); + } + + private Set getListElements(String key) { + Set 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 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 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 listCacheStore = newListCacheStore(); + final CacheKey listKey = new CacheKey("list_add_elements"); + final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey); + final Set 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 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 listCacheStore = newListCacheStore(); + final CacheKey listKey = new CacheKey("list_remove_element_by_element"); + final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey); + final List 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 actualElements = getListElements(listKeyStr); + Assert.assertTrue(actualElements.containsAll(expectedElements)); + } + + @Test + public void removeElementByIndexTest() { + ListCacheStore listCacheStore = newListCacheStore(); + final CacheKey listKey = new CacheKey("list_remove_element_by_index"); + final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey); + final Set 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 actualElements = getListElements(listKeyStr); + Assert.assertTrue(actualElements.containsAll(expectedElements)); + } + + @Test + public void containsElementTest() { + ListCacheStore listCacheStore = newListCacheStore(); + final CacheKey listKey = new CacheKey("list_contains_element"); + final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey); + final Set 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 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 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 listCacheStore = newListCacheStore(); + final CacheKey listKey = new CacheKey("list_elements_length"); + final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey); + final Set 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 listCacheStore = newListCacheStore(); + final CacheKey listKey = new CacheKey("list_clear_collection"); + final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey); + final Set 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 listCacheStore = newListCacheStore(); + final CacheKey listKey = new CacheKey("list_get_element"); + final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey); + final List 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 listCacheStore = newListCacheStore(); + final CacheKey listKey = new CacheKey("list_get_elements_by_range"); + final String listKeyStr = RedisUtils.toRedisCacheKey(IDENTIFY, listKey); + final List 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 actualElements = listCacheStore.getElementsByRange(listKey, start, length); + + for (int i = 0; i < length; i++) { + Assert.assertEquals(expectedElements.get(start + i), actualElements.get(i)); + } + + } + + +} \ No newline at end of file