mirror of https://github.com/halo-dev/halo
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
parent
f0f354af07
commit
9ec3088f1e
|
@ -72,6 +72,7 @@ ext {
|
||||||
jsonVersion = "20190722"
|
jsonVersion = "20190722"
|
||||||
fastJsonVersion = "1.2.68"
|
fastJsonVersion = "1.2.68"
|
||||||
annotationsVersion = "3.0.1u2"
|
annotationsVersion = "3.0.1u2"
|
||||||
|
jedisVersion= '3.2.0'
|
||||||
zxingVersion = "3.4.0"
|
zxingVersion = "3.4.0"
|
||||||
huaweiObsVersion = "3.19.7"
|
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-data-jpa"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-web"
|
implementation "org.springframework.boot:spring-boot-starter-web"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-undertow"
|
implementation "org.springframework.boot:spring-boot-starter-undertow"
|
||||||
|
implementation "redis.clients:jedis:$jedisVersion"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-freemarker"
|
implementation "org.springframework.boot:spring-boot-starter-freemarker"
|
||||||
implementation "com.sun.mail:jakarta.mail"
|
implementation "com.sun.mail:jakarta.mail"
|
||||||
|
|
||||||
|
@ -140,4 +142,4 @@ dependencies {
|
||||||
testImplementation "org.springframework.boot:spring-boot-starter-test"
|
testImplementation "org.springframework.boot:spring-boot-starter-test"
|
||||||
|
|
||||||
developmentOnly "org.springframework.boot:spring-boot-devtools"
|
developmentOnly "org.springframework.boot:spring-boot-devtools"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
rootProject.name = 'halo'
|
rootProject.name = 'halo'
|
||||||
|
|
|
@ -18,6 +18,17 @@ import java.util.concurrent.TimeUnit;
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public abstract class AbstractStringCacheStore extends AbstractCacheStore<String, String> {
|
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) {
|
public <T> void putAny(String key, T value) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -116,18 +116,6 @@ public class LevelCacheStore extends AbstractStringCacheStore {
|
||||||
return new String(bytes, Charset.defaultCharset());
|
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 {
|
private class CacheExpiryCleaner extends TimerTask {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -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() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,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.properties.HaloProperties;
|
import run.halo.app.config.properties.HaloProperties;
|
||||||
import run.halo.app.model.support.HaloConst;
|
import run.halo.app.model.support.HaloConst;
|
||||||
import run.halo.app.utils.HttpClientUtils;
|
import run.halo.app.utils.HttpClientUtils;
|
||||||
|
@ -63,7 +64,9 @@ public class HaloConfiguration {
|
||||||
case "level":
|
case "level":
|
||||||
stringCacheStore = new LevelCacheStore();
|
stringCacheStore = new LevelCacheStore();
|
||||||
break;
|
break;
|
||||||
|
case "redis":
|
||||||
|
stringCacheStore = new RedisCacheStore(this.haloProperties);
|
||||||
|
break;
|
||||||
case "memory":
|
case "memory":
|
||||||
default:
|
default:
|
||||||
//memory or default
|
//memory or default
|
||||||
|
|
|
@ -4,6 +4,7 @@ import lombok.Data;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import run.halo.app.model.enums.Mode;
|
import run.halo.app.model.enums.Mode;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
import static run.halo.app.model.support.HaloConst.*;
|
import static run.halo.app.model.support.HaloConst.*;
|
||||||
|
@ -78,4 +79,9 @@ public class HaloProperties {
|
||||||
*/
|
*/
|
||||||
private String cache = "memory";
|
private String cache = "memory";
|
||||||
|
|
||||||
|
private ArrayList<String> cacheRedisNodes = new ArrayList<>();
|
||||||
|
|
||||||
|
private String cacheRedisPassword = "";
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,5 +143,4 @@ public class HaloConst {
|
||||||
* Version constant.
|
* Version constant.
|
||||||
*/
|
*/
|
||||||
public static String HALO_VERSION = null;
|
public static String HALO_VERSION = null;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,8 @@ import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import java.util.zip.ZipInputStream;
|
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;
|
import static run.halo.app.model.support.HaloConst.DEFAULT_THEME_ID;
|
||||||
|
|
||||||
|
@ -78,6 +80,11 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
|
|
||||||
private final ApplicationEventPublisher eventPublisher;
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* in seconds.
|
||||||
|
*/
|
||||||
|
protected static final long ACTIVATED_THEME_SYNC_INTERVAL = 5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activated theme id.
|
* Activated theme id.
|
||||||
*/
|
*/
|
||||||
|
@ -103,6 +110,17 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
|
|
||||||
themeWorkDir = Paths.get(haloProperties.getWorkDir(), THEME_FOLDER);
|
themeWorkDir = Paths.get(haloProperties.getWorkDir(), THEME_FOLDER);
|
||||||
this.eventPublisher = eventPublisher;
|
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
|
@Override
|
||||||
|
|
|
@ -62,4 +62,9 @@ halo:
|
||||||
auth-enabled: true
|
auth-enabled: true
|
||||||
mode: demo
|
mode: demo
|
||||||
workDir: ${user.home}/halo-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
|
||||||
|
#
|
||||||
|
|
Loading…
Reference in New Issue