Skip to content

Commit

Permalink
Add Redis cache store for distributed deployment (halo-dev#1751)
Browse files Browse the repository at this point in the history
* 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
2 people authored and winar-jin committed Mar 24, 2022
1 parent bd38e69 commit 7c5fda5
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 4 deletions.
8 changes: 7 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ ext {
huaweiObsVersion = '3.21.8.1'
templateInheritanceVersion = "0.4.RELEASE"
jsoupVersion = '1.14.3'
embeddedRedisVersion = '0.6'
diffUtilsVersion = '4.11'
}

Expand All @@ -81,7 +82,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"
Expand Down Expand Up @@ -137,6 +139,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"
}

Expand Down
106 changes: 106 additions & 0 deletions src/main/java/run/halo/app/cache/RedisCacheStore.java
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
}

}
15 changes: 12 additions & 3 deletions src/main/java/run/halo/app/config/HaloConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
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;
Expand All @@ -20,6 +22,7 @@
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;
Expand All @@ -32,6 +35,7 @@
*/
@Slf4j
@EnableAsync
@EnableCaching
@EnableScheduling
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(HaloProperties.class)
Expand All @@ -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
Expand All @@ -70,14 +78,15 @@ AbstractStringCacheStore stringCacheStore() {
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;

}
}
4 changes: 4 additions & 0 deletions src/main/resources/application-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,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
Expand Down
161 changes: 161 additions & 0 deletions src/test/java/run/halo/app/cache/RedisCacheStoreTest.java
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();
}

}

0 comments on commit 7c5fda5

Please sign in to comment.