mirror of https://github.com/halo-dev/halo
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
parent
8c9499ceaf
commit
55887343f9
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue