forked from halo-dev/halo
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Redis cache store for distributed deployment (halo-dev#1751)
* add new cache way - redis * Optimize redis operation * Remove public from CacheWrapper class * add redis cache unit test * refactor: test case for redis cache store Co-authored-by: guqing <1484563614@qq.com>
- Loading branch information
Showing
6 changed files
with
297 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package run.halo.app.cache; | ||
|
||
import java.util.LinkedHashMap; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.concurrent.TimeUnit; | ||
import javax.annotation.PreDestroy; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.data.redis.core.StringRedisTemplate; | ||
import org.springframework.lang.NonNull; | ||
import org.springframework.util.Assert; | ||
import org.springframework.util.CollectionUtils; | ||
|
||
/** | ||
* redis cache store. | ||
* | ||
* @author luoxx | ||
*/ | ||
@Slf4j | ||
public class RedisCacheStore extends AbstractStringCacheStore { | ||
|
||
private static final String REDIS_PREFIX = "halo.redis."; | ||
|
||
private final StringRedisTemplate redisTemplate; | ||
|
||
public RedisCacheStore(StringRedisTemplate redisTemplate) { | ||
this.redisTemplate = redisTemplate; | ||
} | ||
|
||
@Override | ||
@NonNull | ||
Optional<CacheWrapper<String>> getInternal(@NonNull String key) { | ||
Assert.hasText(key, "Cache key must not be blank"); | ||
String value = redisTemplate.opsForValue().get(REDIS_PREFIX + key); | ||
CacheWrapper<String> cacheStore = new CacheWrapper<>(); | ||
cacheStore.setData(value); | ||
return Optional.of(cacheStore); | ||
} | ||
|
||
@Override | ||
void putInternal(@NonNull String key, @NonNull CacheWrapper<String> cacheWrapper) { | ||
Assert.hasText(key, "Cache key must not be blank"); | ||
Assert.notNull(cacheWrapper, "Cache wrapper must not be null"); | ||
if (cacheWrapper.getExpireAt() != null) { | ||
long expire = cacheWrapper.getExpireAt().getTime() - System.currentTimeMillis(); | ||
redisTemplate.opsForValue().set( | ||
REDIS_PREFIX + key, cacheWrapper.getData(), expire, TimeUnit.MILLISECONDS); | ||
} else { | ||
redisTemplate.opsForValue().set(REDIS_PREFIX + key, cacheWrapper.getData()); | ||
} | ||
|
||
log.debug("Put [{}] cache : [{}]", key, cacheWrapper); | ||
} | ||
|
||
@Override | ||
Boolean putInternalIfAbsent(@NonNull String key, @NonNull CacheWrapper<String> cacheWrapper) { | ||
Assert.hasText(key, "Cache key must not be blank"); | ||
Assert.notNull(cacheWrapper, "Cache wrapper must not be null"); | ||
|
||
log.debug("Preparing to put key: [{}], value: [{}]", key, cacheWrapper); | ||
|
||
if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) { | ||
log.warn("Failed to put the cache, the key: [{}] has been present already", key); | ||
return false; | ||
} | ||
|
||
putInternal(key, cacheWrapper); | ||
log.debug("Put successfully"); | ||
return true; | ||
|
||
} | ||
|
||
@Override | ||
public Optional<String> get(String key) { | ||
Assert.notNull(key, "Cache key must not be blank"); | ||
|
||
return getInternal(key).map(CacheWrapper::getData); | ||
} | ||
|
||
@Override | ||
public void delete(@NonNull String key) { | ||
Assert.hasText(key, "Cache key must not be blank"); | ||
|
||
if (Boolean.TRUE.equals(redisTemplate.hasKey(REDIS_PREFIX + key))) { | ||
redisTemplate.delete(REDIS_PREFIX + key); | ||
log.debug("Removed key: [{}]", key); | ||
} | ||
} | ||
|
||
@Override | ||
public LinkedHashMap<String, String> toMap() { | ||
LinkedHashMap<String, String> map = new LinkedHashMap<>(); | ||
Set<String> keys = redisTemplate.keys(REDIS_PREFIX + "*"); | ||
if (CollectionUtils.isEmpty(keys)) { | ||
return map; | ||
} | ||
keys.forEach(key -> map.put(key, redisTemplate.opsForValue().get(key))); | ||
return map; | ||
} | ||
|
||
@PreDestroy | ||
public void preDestroy() { | ||
//do nothing | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
161 changes: 161 additions & 0 deletions
161
src/test/java/run/halo/app/cache/RedisCacheStoreTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
package run.halo.app.cache; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertFalse; | ||
import static org.junit.jupiter.api.Assertions.assertThrows; | ||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
||
import java.util.LinkedHashMap; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.concurrent.TimeUnit; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.junit.jupiter.api.AfterEach; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.boot.test.context.SpringBootTest; | ||
import org.springframework.data.redis.core.StringRedisTemplate; | ||
import redis.embedded.RedisServer; | ||
|
||
/** | ||
* RedisCacheStoreTest. | ||
* | ||
* @author luoxx | ||
* @author guqing | ||
* @date 3/16/22 | ||
*/ | ||
@Slf4j | ||
@SpringBootTest | ||
class RedisCacheStoreTest { | ||
|
||
@Autowired | ||
private StringRedisTemplate redisTemplate; | ||
|
||
RedisCacheStore cacheStore; | ||
|
||
RedisServer redisServer; | ||
|
||
@BeforeEach | ||
void startRedis() { | ||
redisServer = RedisServer.builder() | ||
.port(6379) | ||
.build(); | ||
redisServer.start(); | ||
cacheStore = new RedisCacheStore(redisTemplate); | ||
clearAllCache(); | ||
} | ||
|
||
|
||
@Test | ||
void putNullValueTest() { | ||
String key = "test_key"; | ||
|
||
assertThrows(IllegalArgumentException.class, () -> cacheStore.put(key, null)); | ||
} | ||
|
||
@Test | ||
void putNullKeyTest() { | ||
String value = "test_value"; | ||
|
||
assertThrows(IllegalArgumentException.class, () -> cacheStore.put(null, value)); | ||
} | ||
|
||
@Test | ||
void getByNullKeyTest() { | ||
assertThrows(IllegalArgumentException.class, () -> cacheStore.get(null)); | ||
} | ||
|
||
@Test | ||
void getNullTest() { | ||
String key = "test_key"; | ||
|
||
Optional<String> valueOptional = cacheStore.get(key); | ||
|
||
assertFalse(valueOptional.isPresent()); | ||
} | ||
|
||
@Test | ||
void expirationTest() throws InterruptedException { | ||
String key = "test_key"; | ||
String value = "test_value"; | ||
cacheStore.put(key, value, 500, TimeUnit.MILLISECONDS); | ||
|
||
Optional<String> valueOptional = cacheStore.get(key); | ||
|
||
assertTrue(valueOptional.isPresent()); | ||
assertEquals(value, valueOptional.get()); | ||
|
||
TimeUnit.SECONDS.sleep(1L); | ||
|
||
valueOptional = cacheStore.get(key); | ||
|
||
assertFalse(valueOptional.isPresent()); | ||
} | ||
|
||
@Test | ||
void deleteTest() { | ||
String key = "test_key"; | ||
String value = "test_value"; | ||
|
||
// Put the cache | ||
cacheStore.put(key, value); | ||
|
||
// Get the caceh | ||
Optional<String> valueOptional = cacheStore.get(key); | ||
|
||
// Assert | ||
assertTrue(valueOptional.isPresent()); | ||
assertEquals(value, valueOptional.get()); | ||
|
||
// Delete the cache | ||
cacheStore.delete(key); | ||
|
||
// Get the cache again | ||
valueOptional = cacheStore.get(key); | ||
|
||
// Assertion | ||
assertFalse(valueOptional.isPresent()); | ||
} | ||
|
||
@Test | ||
void toMapTest() { | ||
String key1 = "test_key_1"; | ||
String value1 = "test_value_1"; | ||
|
||
// Put the cache | ||
cacheStore.put(key1, value1); | ||
LinkedHashMap<String, String> map = cacheStore.toMap(); | ||
assertThat(map).isNotNull(); | ||
assertThat(map.size()).isEqualTo(1); | ||
assertThat(map.get("halo.redis.test_key_1")).isEqualTo("test_value_1"); | ||
|
||
String key2 = "test_key_2"; | ||
String value2 = "test_value_2"; | ||
|
||
// Put the cache | ||
cacheStore.put(key2, value2); | ||
|
||
map = cacheStore.toMap(); | ||
assertThat(map).isNotNull(); | ||
assertThat(map.size()).isEqualTo(2); | ||
assertThat(map.get("halo.redis.test_key_1")).isEqualTo("test_value_1"); | ||
assertThat(map.get("halo.redis.test_key_1")).isEqualTo("test_value_1"); | ||
} | ||
|
||
public void clearAllCache() { | ||
Set<String> keys = redisTemplate.keys("*"); | ||
if (keys == null) { | ||
return; | ||
} | ||
log.debug("Clear all cache."); | ||
redisTemplate.delete(keys); | ||
} | ||
|
||
@AfterEach | ||
void stopRedis() { | ||
redisServer.stop(); | ||
} | ||
|
||
} |