Add Redis cache store for distributed deployment (#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>
pull/1760/head
luoxx 2022-03-18 11:07:51 +08:00 committed by GitHub
parent 8c9499ceaf
commit 55887343f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 297 additions and 4 deletions

View File

@ -73,6 +73,7 @@ ext {
huaweiObsVersion = '3.21.8.1'
templateInheritanceVersion = "0.4.RELEASE"
jsoupVersion = '1.14.3'
embeddedRedisVersion = '0.6'
diffUtilsVersion = '4.11'
}
@ -82,7 +83,8 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-jetty"
implementation "org.springframework.boot:spring-boot-starter-freemarker"
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation "org.springframework.boot:spring-boot-starter-validation"
implementation "org.springframework.boot:spring-boot-starter-data-redis"
implementation "com.sun.mail:jakarta.mail"
implementation "com.google.guava:guava:$guavaVersion"
@ -139,6 +141,10 @@ dependencies {
}
testImplementation "org.jsoup:jsoup:$jsoupVersion"
testImplementation ("com.github.kstyrc:embedded-redis:$embeddedRedisVersion") {
exclude group: 'org.slf4j', module: 'slf4j-simple'
}
developmentOnly "org.springframework.boot:spring-boot-devtools"
}

View File

@ -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
}
}

View File

@ -8,10 +8,12 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.scheduling.annotation.EnableAsync;
@ -20,6 +22,7 @@ import org.springframework.web.client.RestTemplate;
import run.halo.app.cache.AbstractStringCacheStore;
import run.halo.app.cache.InMemoryCacheStore;
import run.halo.app.cache.LevelCacheStore;
import run.halo.app.cache.RedisCacheStore;
import run.halo.app.config.attributeconverter.AttributeConverterAutoGenerateConfiguration;
import run.halo.app.config.properties.HaloProperties;
import run.halo.app.repository.base.BaseRepositoryImpl;
@ -32,6 +35,7 @@ import run.halo.app.utils.HttpClientUtils;
*/
@Slf4j
@EnableAsync
@EnableCaching
@EnableScheduling
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(HaloProperties.class)
@ -42,8 +46,12 @@ public class HaloConfiguration {
private final HaloProperties haloProperties;
public HaloConfiguration(HaloProperties haloProperties) {
private final StringRedisTemplate stringRedisTemplate;
public HaloConfiguration(HaloProperties haloProperties,
StringRedisTemplate stringRedisTemplate) {
this.haloProperties = haloProperties;
this.stringRedisTemplate = stringRedisTemplate;
}
@Bean
@ -70,14 +78,15 @@ public class HaloConfiguration {
case "level":
stringCacheStore = new LevelCacheStore(this.haloProperties);
break;
case "redis":
stringCacheStore = new RedisCacheStore(stringRedisTemplate);
break;
case "memory":
default:
//memory or default
stringCacheStore = new InMemoryCacheStore();
break;
}
log.info("Halo cache store load impl : [{}]", stringCacheStore.getClass());
return stringCacheStore;
}
}

View File

@ -46,6 +46,10 @@ spring:
max-request-size: 10240MB
cache:
type: none
redis:
port: 6379
database: 0
host: 127.0.0.1
management:
endpoints:
web:

View File

@ -42,12 +42,19 @@ spring:
- file:///${halo.work-dir}/templates/
- classpath:/templates/
expose-spring-macro-helpers: false
data:
redis:
repositories:
enabled: false
management:
endpoints:
web:
base-path: /api/admin/actuator
exposure:
include: [ 'httptrace', 'metrics', 'env', 'logfile', 'health' ]
health:
redis:
enabled: false
logging:
level:
run.halo.app: INFO

View File

@ -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();
}
}