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"
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
rootProject.name = 'halo'
|
||||
rootProject.name = 'halo'
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.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
|
||||
|
|
|
@ -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 = "";
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -143,5 +143,4 @@ public class HaloConst {
|
|||
* Version constant.
|
||||
*/
|
||||
public static String HALO_VERSION = null;
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
#
|
||||
|
|
Loading…
Reference in New Issue