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'
|
huaweiObsVersion = '3.21.8.1'
|
||||||
templateInheritanceVersion = "0.4.RELEASE"
|
templateInheritanceVersion = "0.4.RELEASE"
|
||||||
jsoupVersion = '1.14.3'
|
jsoupVersion = '1.14.3'
|
||||||
|
embeddedRedisVersion = '0.6'
|
||||||
diffUtilsVersion = '4.11'
|
diffUtilsVersion = '4.11'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +83,8 @@ dependencies {
|
||||||
implementation "org.springframework.boot:spring-boot-starter-web"
|
implementation "org.springframework.boot:spring-boot-starter-web"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-jetty"
|
implementation "org.springframework.boot:spring-boot-starter-jetty"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-freemarker"
|
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.sun.mail:jakarta.mail"
|
||||||
implementation "com.google.guava:guava:$guavaVersion"
|
implementation "com.google.guava:guava:$guavaVersion"
|
||||||
|
@ -139,6 +141,10 @@ dependencies {
|
||||||
}
|
}
|
||||||
testImplementation "org.jsoup:jsoup:$jsoupVersion"
|
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"
|
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.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
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.client.HttpComponentsClientHttpRequestFactory;
|
||||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||||
import org.springframework.scheduling.annotation.EnableAsync;
|
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.AbstractStringCacheStore;
|
||||||
import run.halo.app.cache.InMemoryCacheStore;
|
import run.halo.app.cache.InMemoryCacheStore;
|
||||||
import run.halo.app.cache.LevelCacheStore;
|
import run.halo.app.cache.LevelCacheStore;
|
||||||
|
import run.halo.app.cache.RedisCacheStore;
|
||||||
import run.halo.app.config.attributeconverter.AttributeConverterAutoGenerateConfiguration;
|
import run.halo.app.config.attributeconverter.AttributeConverterAutoGenerateConfiguration;
|
||||||
import run.halo.app.config.properties.HaloProperties;
|
import run.halo.app.config.properties.HaloProperties;
|
||||||
import run.halo.app.repository.base.BaseRepositoryImpl;
|
import run.halo.app.repository.base.BaseRepositoryImpl;
|
||||||
|
@ -32,6 +35,7 @@ import run.halo.app.utils.HttpClientUtils;
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@EnableAsync
|
@EnableAsync
|
||||||
|
@EnableCaching
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@EnableConfigurationProperties(HaloProperties.class)
|
@EnableConfigurationProperties(HaloProperties.class)
|
||||||
|
@ -42,8 +46,12 @@ public class HaloConfiguration {
|
||||||
|
|
||||||
private final HaloProperties haloProperties;
|
private final HaloProperties haloProperties;
|
||||||
|
|
||||||
public HaloConfiguration(HaloProperties haloProperties) {
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
|
|
||||||
|
public HaloConfiguration(HaloProperties haloProperties,
|
||||||
|
StringRedisTemplate stringRedisTemplate) {
|
||||||
this.haloProperties = haloProperties;
|
this.haloProperties = haloProperties;
|
||||||
|
this.stringRedisTemplate = stringRedisTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@ -70,14 +78,15 @@ public class HaloConfiguration {
|
||||||
case "level":
|
case "level":
|
||||||
stringCacheStore = new LevelCacheStore(this.haloProperties);
|
stringCacheStore = new LevelCacheStore(this.haloProperties);
|
||||||
break;
|
break;
|
||||||
|
case "redis":
|
||||||
|
stringCacheStore = new RedisCacheStore(stringRedisTemplate);
|
||||||
|
break;
|
||||||
case "memory":
|
case "memory":
|
||||||
default:
|
default:
|
||||||
//memory or default
|
|
||||||
stringCacheStore = new InMemoryCacheStore();
|
stringCacheStore = new InMemoryCacheStore();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
log.info("Halo cache store load impl : [{}]", stringCacheStore.getClass());
|
log.info("Halo cache store load impl : [{}]", stringCacheStore.getClass());
|
||||||
return stringCacheStore;
|
return stringCacheStore;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,10 @@ spring:
|
||||||
max-request-size: 10240MB
|
max-request-size: 10240MB
|
||||||
cache:
|
cache:
|
||||||
type: none
|
type: none
|
||||||
|
redis:
|
||||||
|
port: 6379
|
||||||
|
database: 0
|
||||||
|
host: 127.0.0.1
|
||||||
management:
|
management:
|
||||||
endpoints:
|
endpoints:
|
||||||
web:
|
web:
|
||||||
|
|
|
@ -42,12 +42,19 @@ spring:
|
||||||
- file:///${halo.work-dir}/templates/
|
- file:///${halo.work-dir}/templates/
|
||||||
- classpath:/templates/
|
- classpath:/templates/
|
||||||
expose-spring-macro-helpers: false
|
expose-spring-macro-helpers: false
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
repositories:
|
||||||
|
enabled: false
|
||||||
management:
|
management:
|
||||||
endpoints:
|
endpoints:
|
||||||
web:
|
web:
|
||||||
base-path: /api/admin/actuator
|
base-path: /api/admin/actuator
|
||||||
exposure:
|
exposure:
|
||||||
include: [ 'httptrace', 'metrics', 'env', 'logfile', 'health' ]
|
include: [ 'httptrace', 'metrics', 'env', 'logfile', 'health' ]
|
||||||
|
health:
|
||||||
|
redis:
|
||||||
|
enabled: false
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
run.halo.app: INFO
|
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