support distributed cache and customized halo home. (#754)

* support distributed cache with redis

* fix HOME_DIR; theme configurations auto update in distributed deployment; update redis cache configuration demo

* remove redundant const of WORK_DIR

Co-authored-by: John Niang <johnniang@riseup.net>
pull/821/head
chaos.su 2020-05-01 14:43:01 +08:00 committed by GitHub
parent f0f354af07
commit 9ec3088f1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 192 additions and 17 deletions

View File

@ -72,6 +72,7 @@ ext {
jsonVersion = "20190722"
fastJsonVersion = "1.2.68"
annotationsVersion = "3.0.1u2"
jedisVersion= '3.2.0'
zxingVersion = "3.4.0"
huaweiObsVersion = "3.19.7"
}
@ -81,6 +82,7 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-undertow"
implementation "redis.clients:jedis:$jedisVersion"
implementation "org.springframework.boot:spring-boot-starter-freemarker"
implementation "com.sun.mail:jakarta.mail"
@ -140,4 +142,4 @@ dependencies {
testImplementation "org.springframework.boot:spring-boot-starter-test"
developmentOnly "org.springframework.boot:spring-boot-devtools"
}
}

View File

@ -1 +1 @@
rootProject.name = 'halo'
rootProject.name = 'halo'

View File

@ -18,6 +18,17 @@ import java.util.concurrent.TimeUnit;
*/
@Slf4j
public abstract class AbstractStringCacheStore extends AbstractCacheStore<String, String> {
protected Optional<CacheWrapper<String>> jsonToCacheWrapper(String json) {
Assert.hasText(json, "json value must not be null");
CacheWrapper<String> cacheWrapper = null;
try {
cacheWrapper = JsonUtils.jsonToObject(json, CacheWrapper.class);
} catch (IOException e) {
e.printStackTrace();
log.debug("erro json to wrapper value bytes: [{}]", json, e);
}
return Optional.ofNullable(cacheWrapper);
}
public <T> void putAny(String key, T value) {
try {

View File

@ -116,18 +116,6 @@ public class LevelCacheStore extends AbstractStringCacheStore {
return new String(bytes, Charset.defaultCharset());
}
private Optional<CacheWrapper<String>> jsonToCacheWrapper(String json) {
Assert.hasText(json, "json value must not be null");
CacheWrapper<String> cacheWrapper = null;
try {
cacheWrapper = JsonUtils.jsonToObject(json, CacheWrapper.class);
} catch (IOException e) {
e.printStackTrace();
log.debug("erro json to wrapper value bytes: [{}]", json, e);
}
return Optional.ofNullable(cacheWrapper);
}
private class CacheExpiryCleaner extends TimerTask {
@Override

View File

@ -0,0 +1,143 @@
package run.halo.app.cache;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import javax.annotation.PreDestroy;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.springframework.util.StringUtils;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.HostAndPort;
import java.util.Set;
import java.util.HashSet;
import java.util.Date;
import run.halo.app.config.properties.HaloProperties;
import run.halo.app.utils.JsonUtils;
/**
* Redis cache store.
*
* @author chaos
*/
@Slf4j
public class RedisCacheStore extends AbstractStringCacheStore {
private volatile static JedisCluster REDIS;
protected HaloProperties haloProperties;
/**
* Cache container.
*/
private final static ConcurrentHashMap<String, CacheWrapper<String>> CACHE_CONTAINER = new ConcurrentHashMap<>();
/**
* Lock.
*/
private Lock lock = new ReentrantLock();
private void initRedis() {
JedisPoolConfig cfg = new JedisPoolConfig();
cfg.setMaxIdle(2);
cfg.setMaxTotal(30);
cfg.setMaxWaitMillis(5000);
Set<HostAndPort> nodes = new HashSet<>();
for (String hostPort : this.haloProperties.getCacheRedisNodes()) {
String[] temp = hostPort.split(":");
if (temp.length > 0) {
String host = temp[0];
int port = 6379;
if (temp.length > 1) {
try {
port = Integer.parseInt(temp[1]);
} catch (Exception ex) {
}
}
nodes.add(new HostAndPort(host, port));
}
}
if (nodes.isEmpty()) {
nodes.add(new HostAndPort("127.0.0.1", 6379));
}
REDIS = new JedisCluster(nodes, 5, 20, 3, this.haloProperties.getCacheRedisPassword(), cfg);
log.info("Initialized cache redis cluster: {}", REDIS.getClusterNodes());
}
protected JedisCluster redis() {
if (REDIS == null) {
synchronized (RedisCacheStore.class) {
if (REDIS != null) {
return REDIS;
}
initRedis();
return REDIS;
}
}
return REDIS;
}
public RedisCacheStore(HaloProperties haloProperties) {
this.haloProperties = haloProperties;
initRedis();
}
@Override
Optional<CacheWrapper<String>> getInternal(String key) {
Assert.hasText(key, "Cache key must not be blank");
String v = REDIS.get(key);
return StringUtils.isEmpty(v) ? Optional.empty() : jsonToCacheWrapper(v);
}
@Override
void putInternal(String key, CacheWrapper<String> cacheWrapper) {
putInternalIfAbsent(key, cacheWrapper);
try {
REDIS.set(key, JsonUtils.objectToJson(cacheWrapper));
Date ttl = cacheWrapper.getExpireAt();
if (ttl != null) {
REDIS.pexpireAt(key, ttl.getTime());
}
} catch (Exception e) {
log.warn("Put cache fail json2object key: [{}] value:[{}]", key, cacheWrapper);
}
}
@Override
Boolean putInternalIfAbsent(String key, CacheWrapper<String> cacheWrapper) {
Assert.hasText(key, "Cache key must not be blank");
Assert.notNull(cacheWrapper, "Cache wrapper must not be null");
try {
if (REDIS.setnx(key, JsonUtils.objectToJson(cacheWrapper)) <= 0) {
log.warn("Failed to put the cache, because the key: [{}] has been present already", key);
return false;
}
Date ttl = cacheWrapper.getExpireAt();
if (ttl != null) {
REDIS.pexpireAt(key, ttl.getTime());
}
return true;
} catch (JsonProcessingException e) {
log.warn("Put cache fail json2object key: [{}] value:[{}]", key, cacheWrapper);
}
log.debug("Cache key: [{}], original cache wrapper: [{}]", key, cacheWrapper);
return false;
}
@Override
public void delete(String key) {
Assert.hasText(key, "Cache key must not be blank");
REDIS.del(key);
log.debug("Removed key: [{}]", key);
}
@PreDestroy
public void preDestroy() {
}
}

View File

@ -15,6 +15,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.properties.HaloProperties;
import run.halo.app.model.support.HaloConst;
import run.halo.app.utils.HttpClientUtils;
@ -63,7 +64,9 @@ public class HaloConfiguration {
case "level":
stringCacheStore = new LevelCacheStore();
break;
case "redis":
stringCacheStore = new RedisCacheStore(this.haloProperties);
break;
case "memory":
default:
//memory or default

View File

@ -4,6 +4,7 @@ import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import run.halo.app.model.enums.Mode;
import java.util.ArrayList;
import java.time.Duration;
import static run.halo.app.model.support.HaloConst.*;
@ -78,4 +79,9 @@ public class HaloProperties {
*/
private String cache = "memory";
private ArrayList<String> cacheRedisNodes = new ArrayList<>();
private String cacheRedisPassword = "";
}

View File

@ -143,5 +143,4 @@ public class HaloConst {
* Version constant.
*/
public static String HALO_VERSION = null;
}

View File

@ -48,6 +48,8 @@ import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipInputStream;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static run.halo.app.model.support.HaloConst.DEFAULT_THEME_ID;
@ -78,6 +80,11 @@ public class ThemeServiceImpl implements ThemeService {
private final ApplicationEventPublisher eventPublisher;
/**
* in seconds.
*/
protected static final long ACTIVATED_THEME_SYNC_INTERVAL = 5;
/**
* Activated theme id.
*/
@ -103,6 +110,17 @@ public class ThemeServiceImpl implements ThemeService {
themeWorkDir = Paths.get(haloProperties.getWorkDir(), THEME_FOLDER);
this.eventPublisher = eventPublisher;
// check activated theme option changes every 5 seconds.
Executors.newSingleThreadScheduledExecutor().scheduleWithFixedDelay(() -> {
try {
String newActivatedThemeId = optionService.getByPropertyOrDefault(PrimaryProperties.THEME, String.class, DEFAULT_THEME_ID);
if (newActivatedThemeId != activatedThemeId) {
activateTheme(newActivatedThemeId);
}
} catch (Exception e) {
log.warn("theme option sync exception: {}", e);
}
}, ACTIVATED_THEME_SYNC_INTERVAL, ACTIVATED_THEME_SYNC_INTERVAL, TimeUnit.SECONDS);
}
@Override

View File

@ -62,4 +62,9 @@ halo:
auth-enabled: true
mode: demo
workDir: ${user.home}/halo-demo/
cache: level
cache: level
# use redis as cache to support halo deployment
# cache: redis
# cache-redis-nodes: ['127.0.0.1:6380', '127.0.0.1:6379']
# cache-redis-password: 123456
#