更新了多个文件以支持数据同步功能,包括配置文件和数据库操作类的修改。
This commit is contained in:
parent
0eb25bcbb0
commit
8327a0c06f
|
@ -1,14 +1,24 @@
|
||||||
package com.io.yutian.elementoriginlib;
|
package com.io.yutian.elementoriginlib;
|
||||||
|
|
||||||
|
import com.io.yutian.elementoriginlib.config.MongodbConfig;
|
||||||
import com.io.yutian.elementoriginlib.config.OriginLibConfig;
|
import com.io.yutian.elementoriginlib.config.OriginLibConfig;
|
||||||
|
import com.io.yutian.elementoriginlib.config.RedisConfig;
|
||||||
import com.io.yutian.elementoriginlib.lang.Lang;
|
import com.io.yutian.elementoriginlib.lang.Lang;
|
||||||
import com.io.yutian.elementoriginlib.listener.GuiHandlerListener;
|
import com.io.yutian.elementoriginlib.listener.GuiHandlerListener;
|
||||||
import com.io.yutian.elementoriginlib.listener.PlayerChatInputListener;
|
import com.io.yutian.elementoriginlib.listener.PlayerChatInputListener;
|
||||||
import com.io.yutian.elementoriginlib.logger.Logger;
|
import com.io.yutian.elementoriginlib.logger.Logger;
|
||||||
import com.io.yutian.elementoriginlib.manager.CommandManager;
|
import com.io.yutian.elementoriginlib.manager.CommandManager;
|
||||||
import com.io.yutian.elementoriginlib.redis.RedisIO;
|
import com.io.yutian.elementoriginlib.redis.RedisIO;
|
||||||
|
import com.io.yutian.elementoriginlib.util.FileUtil;
|
||||||
|
import com.io.yutian.elementoriginlib.ztsd.ZstdDictionaryManager;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
public final class ElementOriginLib extends JavaPlugin {
|
public final class ElementOriginLib extends JavaPlugin {
|
||||||
|
|
||||||
public static final Logger LOGGER = Logger.getLogger(ElementOriginLib.class);
|
public static final Logger LOGGER = Logger.getLogger(ElementOriginLib.class);
|
||||||
|
@ -18,6 +28,9 @@ public final class ElementOriginLib extends JavaPlugin {
|
||||||
private RedisIO redisIO;
|
private RedisIO redisIO;
|
||||||
|
|
||||||
private OriginLibConfig config;
|
private OriginLibConfig config;
|
||||||
|
private RedisConfig redisConfig;
|
||||||
|
private MongodbConfig mongodbConfig;
|
||||||
|
private ZstdDictionaryManager zstdDictionaryManager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
|
@ -31,19 +44,51 @@ public final class ElementOriginLib extends JavaPlugin {
|
||||||
reload();
|
reload();
|
||||||
|
|
||||||
redisIO = new RedisIO();
|
redisIO = new RedisIO();
|
||||||
redisIO.init(this);
|
redisIO.init(redisConfig);
|
||||||
|
|
||||||
|
ZstdDictionaryManager.DictionaryConfig config = new ZstdDictionaryManager.DictionaryConfig.Builder()
|
||||||
|
.minSampleSize(1024) // 最小样本大小:1KB
|
||||||
|
.maxSampleSize(16 * 1024 * 1024) // 最大样本大小:16MB
|
||||||
|
.minQualityThreshold(0.5) // 最小样本质量阈值
|
||||||
|
.compressionLevel(3) // 压缩级别(1-22,3是平衡值)
|
||||||
|
.maxDictionarySize(128 * 1024) // 字典大小:128B
|
||||||
|
.trainingInterval(Duration.ofHours(1)) // 训练间隔:1小时
|
||||||
|
.maxSamples(1024) // 触发训练的样本数量
|
||||||
|
.maxSampleFileSize(512 * 1024 * 1024) // 样本文件大小限制:512MB
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try {
|
||||||
|
zstdDictionaryManager = new ZstdDictionaryManager("plugins/pixeldatasync/zstd_dictionary", config);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
redisIO.close();
|
redisIO.close();
|
||||||
|
zstdDictionaryManager.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reload() {
|
public void reload() {
|
||||||
config = new OriginLibConfig();
|
config = new OriginLibConfig();
|
||||||
saveDefaultConfig();
|
saveDefaultConfig();
|
||||||
reloadConfig();
|
reloadConfig();
|
||||||
|
redisConfig = new RedisConfig();
|
||||||
|
File redisFile = new File(getDataFolder(), "redis.yml");
|
||||||
|
if (!redisFile.exists()) {
|
||||||
|
saveResource("redis.yml", false);
|
||||||
|
}
|
||||||
|
FileConfiguration redisFileConfig = YamlConfiguration.loadConfiguration(redisFile);
|
||||||
|
redisConfig.load(redisFileConfig);
|
||||||
|
mongodbConfig = new MongodbConfig();
|
||||||
|
File mongodbFile = new File(getDataFolder(), "mongodb.yml");
|
||||||
|
if (!mongodbFile.exists()) {
|
||||||
|
saveResource("mongodb.yml", false);
|
||||||
|
}
|
||||||
|
FileConfiguration mongodbFileConfig = YamlConfiguration.loadConfiguration(mongodbFile);
|
||||||
|
mongodbConfig.load(mongodbFileConfig);
|
||||||
config.load(getConfig());
|
config.load(getConfig());
|
||||||
Lang.registerLangFile(this);
|
Lang.registerLangFile(this);
|
||||||
Lang.reload();
|
Lang.reload();
|
||||||
|
@ -53,6 +98,14 @@ public final class ElementOriginLib extends JavaPlugin {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RedisConfig getRedisConfig() {
|
||||||
|
return redisConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MongodbConfig getMongodbConfig() {
|
||||||
|
return mongodbConfig;
|
||||||
|
}
|
||||||
|
|
||||||
public RedisIO getRedisIO() {
|
public RedisIO getRedisIO() {
|
||||||
return redisIO;
|
return redisIO;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.io.yutian.elementoriginlib.config;
|
||||||
|
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
|
||||||
|
public class MongodbConfig {
|
||||||
|
|
||||||
|
private String host;
|
||||||
|
private int port;
|
||||||
|
private String database;
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
public void load(FileConfiguration config) {
|
||||||
|
host = config.getString("host", "127.0.0.1");
|
||||||
|
port = config.getInt("port", 27017);
|
||||||
|
database = config.getString("database", "admin");
|
||||||
|
username = config.getString("username", "root");
|
||||||
|
password = config.getString("password", "123456");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHost() {
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPort() {
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDatabase() {
|
||||||
|
return database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,13 +7,6 @@ public class OriginLibConfig {
|
||||||
private String redisBungeeNetworkId;
|
private String redisBungeeNetworkId;
|
||||||
private String redisBungeeProxyId;
|
private String redisBungeeProxyId;
|
||||||
|
|
||||||
private String mongoDbHost;
|
|
||||||
private int mongoDbPort;
|
|
||||||
private String mongoDbDatabase;
|
|
||||||
private String mongoDbUsername;
|
|
||||||
private String mongoDbPassword;
|
|
||||||
|
|
||||||
|
|
||||||
public void load(FileConfiguration config) {
|
public void load(FileConfiguration config) {
|
||||||
redisBungeeNetworkId = config.getString("redisBungeeNetworkId");
|
redisBungeeNetworkId = config.getString("redisBungeeNetworkId");
|
||||||
redisBungeeProxyId = config.getString("redisBungeeProxyId");
|
redisBungeeProxyId = config.getString("redisBungeeProxyId");
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.io.yutian.elementoriginlib.config;
|
||||||
|
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
|
||||||
|
public class RedisConfig {
|
||||||
|
|
||||||
|
private String server;
|
||||||
|
private int port;
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
public void load(FileConfiguration config) {
|
||||||
|
server = config.getString("server", "localhost");
|
||||||
|
port = config.getInt("port", 6379);
|
||||||
|
password = config.getString("password");
|
||||||
|
if (password != null && (password.isEmpty() || password.equals("none"))) {
|
||||||
|
password = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getServer() {
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPort() {
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,29 +1,11 @@
|
||||||
package com.io.yutian.elementoriginlib.datasync;
|
package com.io.yutian.elementoriginlib.datasync;
|
||||||
|
|
||||||
import com.io.yutian.elementoriginlib.ElementOriginLib;
|
import java.util.UUID;
|
||||||
import com.io.yutian.elementoriginlib.serialize.SerializeHelper;
|
|
||||||
import redis.clients.jedis.Jedis;
|
|
||||||
|
|
||||||
public class DataSyncHelper {
|
public class DataSyncHelper {
|
||||||
|
|
||||||
public static void saveData(String key, Object value) {
|
public static void sync(UUID uuid, Object obj) {
|
||||||
String strValue = SerializeHelper.serialize(value);
|
|
||||||
try (Jedis jedis = ElementOriginLib.inst().getRedisIO().getJedisPool().getResource()) {
|
|
||||||
jedis.set(key, strValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> T getData(String key, Class<T> clazz) {
|
|
||||||
try (Jedis jedis = ElementOriginLib.inst().getRedisIO().getJedisPool().getResource()) {
|
|
||||||
if (!jedis.exists(key)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String strValue = jedis.get(key);
|
|
||||||
if (strValue == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return SerializeHelper.deserialize(strValue, clazz);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.io.yutian.elementoriginlib.datasync;
|
||||||
|
|
||||||
|
public enum SyncDirection {
|
||||||
|
REDIS_TO_MONGO,
|
||||||
|
MONGO_TO_REDIS
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
package com.io.yutian.elementoriginlib.datasync;
|
||||||
|
|
||||||
|
import com.io.yutian.elementoriginlib.mongodb.MongoDBIO;
|
||||||
|
import com.mongodb.client.MongoCollection;
|
||||||
|
import com.mongodb.client.model.ReplaceOptions;
|
||||||
|
import org.bson.Document;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import redis.clients.jedis.Jedis;
|
||||||
|
import redis.clients.jedis.JedisPool;
|
||||||
|
import redis.clients.jedis.exceptions.JedisException;
|
||||||
|
|
||||||
|
public class SyncModule {
|
||||||
|
public static final String KEY_PREFIX = "playerdata";
|
||||||
|
private static final int DEFAULT_LOCK_EXPIRE = 15;
|
||||||
|
private static final String _ID = "_id";
|
||||||
|
|
||||||
|
private final String moduleName;
|
||||||
|
private final String databaseName;
|
||||||
|
private final String collectionName;
|
||||||
|
|
||||||
|
public SyncModule(String moduleName, String databaseName, String collectionName) {
|
||||||
|
this.moduleName = moduleName;
|
||||||
|
this.databaseName = databaseName;
|
||||||
|
this.collectionName = collectionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SyncResult syncToRedis(Player player, JedisPool jedisPool, MongoDBIO mongoDBIO) {
|
||||||
|
if (player == null || jedisPool == null || mongoDBIO == null) {
|
||||||
|
return SyncResult.INVALID_PARAMETERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
String playerId = player.getUniqueId().toString();
|
||||||
|
String redisKey = buildRedisKey(playerId);
|
||||||
|
String lockKey = redisKey + ":lock";
|
||||||
|
|
||||||
|
try (Jedis jedis = jedisPool.getResource()) {
|
||||||
|
if (!acquireLock(jedis, lockKey)) {
|
||||||
|
return SyncResult.LOCK_FAILED;
|
||||||
|
}
|
||||||
|
MongoCollection<Document> collection = mongoDBIO.getMongoDatabase()
|
||||||
|
.getCollection(collectionName);
|
||||||
|
Document query = new Document(_ID, player.getUniqueId());
|
||||||
|
Document mongoDoc = collection.find(query).first();
|
||||||
|
|
||||||
|
if (mongoDoc == null) {
|
||||||
|
return SyncResult.MONGO_DATA_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
String jsonData = mongoDoc.toJson();
|
||||||
|
jedis.set(redisKey, jsonData);
|
||||||
|
return SyncResult.SUCCESS;
|
||||||
|
} catch (JedisException e) {
|
||||||
|
return SyncResult.REDIS_ERROR;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return SyncResult.MONGO_ERROR;
|
||||||
|
} finally {
|
||||||
|
releaseLock(jedisPool, lockKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SyncResult syncToMongoDB(Player player, JedisPool jedisPool, MongoDBIO mongoDBIO) {
|
||||||
|
if (player == null || jedisPool == null || mongoDBIO == null) {
|
||||||
|
return SyncResult.INVALID_PARAMETERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
String playerId = player.getUniqueId().toString();
|
||||||
|
String redisKey = buildRedisKey(playerId);
|
||||||
|
String lockKey = redisKey + ":lock";
|
||||||
|
|
||||||
|
try (Jedis jedis = jedisPool.getResource()) {
|
||||||
|
if (!acquireLock(jedis, lockKey)) {
|
||||||
|
return SyncResult.LOCK_FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jedis.exists(redisKey)) {
|
||||||
|
return SyncResult.NOT_FOUND_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
String redisValue = jedis.get(redisKey);
|
||||||
|
if (redisValue == null || redisValue.isEmpty()) {
|
||||||
|
return SyncResult.EMPTY_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
MongoCollection<Document> collection = mongoDBIO.getMongoDatabase()
|
||||||
|
.getCollection(collectionName);
|
||||||
|
|
||||||
|
Document doc = Document.parse(redisValue);
|
||||||
|
collection.replaceOne(
|
||||||
|
new Document(_ID, player.getUniqueId()),
|
||||||
|
doc,
|
||||||
|
new ReplaceOptions().upsert(true)
|
||||||
|
);
|
||||||
|
|
||||||
|
jedis.del(redisKey);
|
||||||
|
return SyncResult.SUCCESS;
|
||||||
|
} catch (JedisException e) {
|
||||||
|
return SyncResult.REDIS_ERROR;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return SyncResult.MONGO_ERROR;
|
||||||
|
} finally {
|
||||||
|
releaseLock(jedisPool, lockKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean checkMongoDBConnection(MongoDBIO mongoDBIO) {
|
||||||
|
try {
|
||||||
|
Document pingResult = mongoDBIO.getMongoDatabase()
|
||||||
|
.runCommand(new Document("ping", 1));
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean checkRedisConnection(JedisPool jedisPool) {
|
||||||
|
try (Jedis jedis = jedisPool.getResource()) {
|
||||||
|
String result = jedis.ping();
|
||||||
|
return "PONG".equals(result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildRedisKey(String playerId) {
|
||||||
|
return String.format("%s:%s:%s", KEY_PREFIX, collectionName, playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean acquireLock(Jedis jedis, String lockKey) {
|
||||||
|
long result = jedis.setnx(lockKey, "1");
|
||||||
|
if (result == 1) {
|
||||||
|
jedis.expire(lockKey, DEFAULT_LOCK_EXPIRE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseLock(JedisPool jedisPool, String lockKey) {
|
||||||
|
try (Jedis jedis = jedisPool.getResource()) {
|
||||||
|
jedis.del(lockKey);
|
||||||
|
} catch (JedisException e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getModuleName() {
|
||||||
|
return moduleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDatabaseName() {
|
||||||
|
return databaseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCollectionName() {
|
||||||
|
return collectionName;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.io.yutian.elementoriginlib.datasync;
|
||||||
|
|
||||||
|
public enum SyncResult {
|
||||||
|
|
||||||
|
SUCCESS("同步成功", false),
|
||||||
|
REDIS_CONNECTION_FAILED("Redis连接失败", true),
|
||||||
|
MONGO_CONNECTION_FAILED("MongoDB连接失败", true),
|
||||||
|
LOCK_FAILED("获取锁失败", true),
|
||||||
|
NOT_FOUND_KEY("Redis键不存在", false),
|
||||||
|
MONGO_DATA_NOT_FOUND("MongoDB数据不存在", false),
|
||||||
|
EMPTY_DATA("Redis数据为空", false),
|
||||||
|
REDIS_ERROR("Redis操作异常", true),
|
||||||
|
MONGO_ERROR("MongoDB操作异常", true),
|
||||||
|
INVALID_PARAMETERS("参数无效", true),
|
||||||
|
UNKNOWN_ERROR("未知错误", true);
|
||||||
|
|
||||||
|
private final String message;
|
||||||
|
private final boolean error;
|
||||||
|
|
||||||
|
SyncResult(String message, boolean isError) {
|
||||||
|
this.message = message;
|
||||||
|
this.error = isError;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isError() {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.io.yutian.elementoriginlib.mongodb;
|
||||||
|
|
||||||
|
import com.io.yutian.elementoriginlib.ElementOriginLib;
|
||||||
|
import com.io.yutian.elementoriginlib.config.MongodbConfig;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class MongoDBFactory {
|
||||||
|
|
||||||
|
private static Map<String, MongoDBIO> mongoDBIOs = new HashMap<>();
|
||||||
|
|
||||||
|
public static MongoDBIO getMongoDBIO(String dbName) {
|
||||||
|
return getMongoDBIO(ElementOriginLib.inst().getMongodbConfig(), dbName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MongoDBIO getMongoDBIO(MongodbConfig config, String dbName) {
|
||||||
|
MongoDBIO mongoDBIO = mongoDBIOs.get(dbName);
|
||||||
|
if (mongoDBIO == null) {
|
||||||
|
mongoDBIO = new MongoDBIO(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(), dbName);
|
||||||
|
mongoDBIOs.put(dbName, mongoDBIO);
|
||||||
|
}
|
||||||
|
return mongoDBIO;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package com.io.yutian.elementoriginlib.mongodb;
|
||||||
|
|
||||||
|
import com.mongodb.ConnectionString;
|
||||||
|
import com.mongodb.MongoClientSettings;
|
||||||
|
import com.mongodb.client.MongoClient;
|
||||||
|
import com.mongodb.client.MongoClients;
|
||||||
|
import com.mongodb.client.MongoDatabase;
|
||||||
|
import org.bson.UuidRepresentation;
|
||||||
|
import org.bson.codecs.UuidCodec;
|
||||||
|
import org.bson.codecs.configuration.CodecRegistries;
|
||||||
|
import org.bson.codecs.configuration.CodecRegistry;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
public class MongoDBIO {
|
||||||
|
|
||||||
|
private final static Logger LOG = LoggerFactory.getLogger(MongoDBIO.class);
|
||||||
|
|
||||||
|
private static final CodecRegistry CODEC_REGISTRY = CodecRegistries.fromRegistries(
|
||||||
|
CodecRegistries.fromCodecs(new UuidCodec(UuidRepresentation.STANDARD)),
|
||||||
|
MongoClientSettings.getDefaultCodecRegistry()
|
||||||
|
);
|
||||||
|
|
||||||
|
private MongoDatabase mongoDatabase;
|
||||||
|
|
||||||
|
public MongoDBIO(String host, int port, String username, String password, String dbname) {
|
||||||
|
String connectionString = String.format(
|
||||||
|
"mongodb://%s:%s@%s:%d/?authSource=admin",
|
||||||
|
URLEncoder.encode(username, StandardCharsets.UTF_8),
|
||||||
|
URLEncoder.encode(password, StandardCharsets.UTF_8),
|
||||||
|
host, port
|
||||||
|
);
|
||||||
|
MongoClientSettings settings = MongoClientSettings.builder()
|
||||||
|
.applyConnectionString(new ConnectionString(connectionString))
|
||||||
|
.codecRegistry(CODEC_REGISTRY)
|
||||||
|
.uuidRepresentation(UuidRepresentation.STANDARD)
|
||||||
|
.build();
|
||||||
|
MongoClient client = MongoClients.create(settings);
|
||||||
|
mongoDatabase = client.getDatabase(dbname);
|
||||||
|
if (mongoDatabase == null) {
|
||||||
|
throw new IllegalArgumentException("获取数据库实例失败:" + dbname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public MongoDBIO(MongoClient mongo, String dbname) {
|
||||||
|
mongoDatabase = mongo.getDatabase(dbname);
|
||||||
|
if (mongoDatabase == null) {
|
||||||
|
throw new IllegalArgumentException("获取数据库实例失败:" + dbname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public MongoDatabase getMongoDatabase() {
|
||||||
|
return mongoDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,34 +1,19 @@
|
||||||
package com.io.yutian.elementoriginlib.redis;
|
package com.io.yutian.elementoriginlib.redis;
|
||||||
|
|
||||||
import com.io.yutian.elementoriginlib.util.FileUtil;
|
import com.io.yutian.elementoriginlib.config.RedisConfig;
|
||||||
import org.bukkit.configuration.file.FileConfiguration;
|
|
||||||
import org.bukkit.configuration.file.YamlConfiguration;
|
|
||||||
import org.bukkit.plugin.Plugin;
|
|
||||||
import redis.clients.jedis.Jedis;
|
import redis.clients.jedis.Jedis;
|
||||||
import redis.clients.jedis.JedisPool;
|
import redis.clients.jedis.JedisPool;
|
||||||
import redis.clients.jedis.JedisPoolConfig;
|
import redis.clients.jedis.JedisPoolConfig;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class RedisIO {
|
public class RedisIO {
|
||||||
|
|
||||||
private JedisPool jedisPool;
|
private JedisPool jedisPool;
|
||||||
|
|
||||||
public void init(Plugin plugin) {
|
public void init(RedisConfig redisConfig) {
|
||||||
File file = FileUtil.getFile(plugin, "", "redis.yml");
|
|
||||||
if (!file.exists()) {
|
|
||||||
plugin.saveResource("redis.yml", false);
|
|
||||||
}
|
|
||||||
FileConfiguration configuration = YamlConfiguration.loadConfiguration(file);
|
|
||||||
String redisServer = configuration.getString("server", "localhost");
|
|
||||||
int redisPort = configuration.getInt("port", 6379);
|
|
||||||
String redisPassword = configuration.getString("password");
|
|
||||||
if (redisPassword != null && (redisPassword.isEmpty() || redisPassword.equals("none"))) {
|
|
||||||
redisPassword = null;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
String finalRedisPassword = redisPassword;
|
String finalRedisPassword = redisConfig.getPassword();
|
||||||
JedisPoolConfig config = new JedisPoolConfig();
|
JedisPoolConfig config = new JedisPoolConfig();
|
||||||
config.setMaxTotal(256);
|
config.setMaxTotal(256);
|
||||||
config.setMaxIdle(64);
|
config.setMaxIdle(64);
|
||||||
|
@ -39,7 +24,7 @@ public class RedisIO {
|
||||||
config.setMinEvictableIdleTimeMillis(600_000);
|
config.setMinEvictableIdleTimeMillis(600_000);
|
||||||
config.setBlockWhenExhausted(true);
|
config.setBlockWhenExhausted(true);
|
||||||
config.setMaxWaitMillis(2000);
|
config.setMaxWaitMillis(2000);
|
||||||
jedisPool = new JedisPool(config, redisServer, redisPort, 0, finalRedisPassword);
|
jedisPool = new JedisPool(config, redisConfig.getServer(), redisConfig.getPort(), 0, finalRedisPassword);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,888 @@
|
||||||
|
package com.io.yutian.elementoriginlib.ztsd;
|
||||||
|
|
||||||
|
import com.github.luben.zstd.Zstd;
|
||||||
|
import com.github.luben.zstd.ZstdDictTrainer;
|
||||||
|
import com.github.luben.zstd.ZstdInputStream;
|
||||||
|
import com.io.yutian.elementoriginlib.ElementOriginLib;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class ZstdDictionaryManager {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(ZstdDictionaryManager.class);
|
||||||
|
|
||||||
|
private final DictionaryConfig config;
|
||||||
|
private final BlockingQueue<Sample> sampleQueue;
|
||||||
|
private final Map<String, DictionaryVersion> dictionaryVersions;
|
||||||
|
private final CompressionStats stats;
|
||||||
|
private final AtomicBoolean isTraining;
|
||||||
|
private final ScheduledExecutorService scheduler;
|
||||||
|
private final Path baseDir;
|
||||||
|
private final Path samplesDir;
|
||||||
|
private final Path statsDir;
|
||||||
|
private final AtomicInteger fileCounter;
|
||||||
|
|
||||||
|
public ZstdDictionaryManager(String storageDir, DictionaryConfig config) throws IOException {
|
||||||
|
this.config = config;
|
||||||
|
this.sampleQueue = new LinkedBlockingQueue<>();
|
||||||
|
this.dictionaryVersions = new ConcurrentHashMap<>();
|
||||||
|
this.stats = new CompressionStats();
|
||||||
|
this.isTraining = new AtomicBoolean(false);
|
||||||
|
this.scheduler = Executors.newScheduledThreadPool(1);
|
||||||
|
this.baseDir = Paths.get(storageDir).toAbsolutePath();
|
||||||
|
this.samplesDir = baseDir.resolve("samples");
|
||||||
|
this.statsDir = baseDir.resolve("stats");
|
||||||
|
this.fileCounter = new AtomicInteger(0);
|
||||||
|
|
||||||
|
Files.createDirectories(this.samplesDir);
|
||||||
|
Files.createDirectories(this.statsDir);
|
||||||
|
|
||||||
|
loadLatestSamples();
|
||||||
|
loadStats();
|
||||||
|
if (config.scheduledTraining) {
|
||||||
|
startScheduledTraining();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startScheduledTraining() {
|
||||||
|
scheduler.scheduleAtFixedRate(
|
||||||
|
this::trainIfNeeded,
|
||||||
|
config.getTrainingInterval().toMillis(),
|
||||||
|
config.getTrainingInterval().toMillis(),
|
||||||
|
TimeUnit.MILLISECONDS
|
||||||
|
);
|
||||||
|
LOGGER.info("已启动定时训练任务,间隔: {} 毫秒", config.getTrainingInterval().toMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSample(String text, String source) throws IOException {
|
||||||
|
try {
|
||||||
|
Sample sample = new Sample(text.getBytes("UTF-8"), source);
|
||||||
|
evaluateSampleQuality(sample);
|
||||||
|
if (sample.getQuality() >= config.getMinQualityThreshold()) {
|
||||||
|
sampleQueue.offer(sample);
|
||||||
|
if (sampleQueue.size() >= config.getMaxSamples()) {
|
||||||
|
trainIfNeeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
stats.recordError("ADD_SAMPLE_ERROR");
|
||||||
|
throw new IOException("添加样本失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void evaluateSampleQuality(Sample sample) {
|
||||||
|
double quality = 1.0;
|
||||||
|
byte[] data = sample.getData();
|
||||||
|
|
||||||
|
LOGGER.debug("评估样本质量 - 来源: {}, 大小: {} 字节",
|
||||||
|
sample.getSource(), data.length);
|
||||||
|
|
||||||
|
if (data.length == 0) {
|
||||||
|
sample.setQuality(0.0);
|
||||||
|
LOGGER.debug("样本为空,质量设为0");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length < config.getMinSampleSize()) {
|
||||||
|
quality *= 0.5;
|
||||||
|
LOGGER.debug("样本过小,质量降低 - 当前大小: {}, 最小要求: {}",
|
||||||
|
data.length, config.getMinSampleSize());
|
||||||
|
} else if (data.length > config.getMaxSampleSize()) {
|
||||||
|
quality *= 0.3;
|
||||||
|
LOGGER.debug("样本过大,质量降低 - 当前大小: {}, 最大限制: {}",
|
||||||
|
data.length, config.getMaxSampleSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
SampleMetrics metrics = calculateSampleMetrics(data);
|
||||||
|
|
||||||
|
if (metrics.entropy < 0.5) {
|
||||||
|
quality *= 0.7;
|
||||||
|
LOGGER.debug("样本熵值过低,质量降低 - 熵值: {}", metrics.entropy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metrics.uniqueRatio < 0.3) {
|
||||||
|
quality *= 0.8;
|
||||||
|
LOGGER.debug("样本多样性不足,质量降低 - 唯一字节比例: {}", metrics.uniqueRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metrics.repetitionRatio > 0.7) {
|
||||||
|
quality *= 0.6;
|
||||||
|
LOGGER.debug("样本重复性过高,质量降低 - 重复比例: {}", metrics.repetitionRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metrics.distributionScore < 0.4) {
|
||||||
|
quality *= 0.9;
|
||||||
|
LOGGER.debug("样本字节分布不均匀,质量降低 - 分布得分: {}", metrics.distributionScore);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metrics.compressionPotential < 0.3) {
|
||||||
|
quality *= 0.85;
|
||||||
|
LOGGER.debug("样本压缩潜力较低,质量降低 - 压缩潜力: {}", metrics.compressionPotential);
|
||||||
|
}
|
||||||
|
|
||||||
|
sample.setQuality(quality);
|
||||||
|
LOGGER.debug("样本质量评估完成 - 最终质量: {}, 熵值: {}, 唯一字节比例: {}, 重复比例: {}, 分布得分: {}, 压缩潜力: {}",
|
||||||
|
quality, metrics.entropy, metrics.uniqueRatio, metrics.repetitionRatio, metrics.distributionScore, metrics.compressionPotential);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SampleMetrics calculateSampleMetrics(byte[] data) {
|
||||||
|
int[] frequencies = new int[256];
|
||||||
|
int[] runLengths = new int[256];
|
||||||
|
int currentRun = 1;
|
||||||
|
byte lastByte = data[0];
|
||||||
|
|
||||||
|
for (int i = 1; i < data.length; i++) {
|
||||||
|
byte currentByte = data[i];
|
||||||
|
frequencies[currentByte & 0xFF]++;
|
||||||
|
|
||||||
|
if (currentByte == lastByte) {
|
||||||
|
currentRun++;
|
||||||
|
} else {
|
||||||
|
if (currentRun > runLengths[lastByte & 0xFF]) {
|
||||||
|
runLengths[lastByte & 0xFF] = currentRun;
|
||||||
|
}
|
||||||
|
currentRun = 1;
|
||||||
|
lastByte = currentByte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentRun > runLengths[lastByte & 0xFF]) {
|
||||||
|
runLengths[lastByte & 0xFF] = currentRun;
|
||||||
|
}
|
||||||
|
|
||||||
|
int uniqueBytes = 0;
|
||||||
|
for (int freq : frequencies) {
|
||||||
|
if (freq > 0) uniqueBytes++;
|
||||||
|
}
|
||||||
|
|
||||||
|
double entropy = 0.0;
|
||||||
|
for (int freq : frequencies) {
|
||||||
|
if (freq > 0) {
|
||||||
|
double probability = (double) freq / data.length;
|
||||||
|
entropy -= probability * (Math.log(probability) / Math.log(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entropy /= 8.0;
|
||||||
|
|
||||||
|
double repetitionRatio = 0.0;
|
||||||
|
for (int runLength : runLengths) {
|
||||||
|
repetitionRatio += (double) runLength / data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
double distributionScore = 0.0;
|
||||||
|
double expectedFreq = 1.0 / 256;
|
||||||
|
for (int freq : frequencies) {
|
||||||
|
if (freq > 0) {
|
||||||
|
double actualFreq = (double) freq / data.length;
|
||||||
|
distributionScore += 1.0 - Math.abs(actualFreq - expectedFreq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
distributionScore /= 256;
|
||||||
|
|
||||||
|
double compressionPotential = 1.0 - (entropy * 0.4 + repetitionRatio * 0.3 + (1.0 - distributionScore) * 0.3);
|
||||||
|
|
||||||
|
return new SampleMetrics(
|
||||||
|
entropy,
|
||||||
|
(double) uniqueBytes / 256,
|
||||||
|
repetitionRatio,
|
||||||
|
distributionScore,
|
||||||
|
compressionPotential
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void trainIfNeeded() {
|
||||||
|
if (isTraining.compareAndSet(false, true)) {
|
||||||
|
CompletableFuture.runAsync(this::trainAndRotate)
|
||||||
|
.whenComplete((v, e) -> isTraining.set(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void trainAndRotate() {
|
||||||
|
try {
|
||||||
|
List<Sample> samples = new ArrayList<>();
|
||||||
|
sampleQueue.drainTo(samples, config.getMaxSamples());
|
||||||
|
|
||||||
|
if (samples.isEmpty()) {
|
||||||
|
LOGGER.info("没有样本需要训练");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("开始训练字典,样本数量: {}", samples.size());
|
||||||
|
ZstdDictTrainer trainer = new ZstdDictTrainer(config.getMaxDictionarySize(), config.getMaxDictionarySize());
|
||||||
|
for (Sample sample : samples) {
|
||||||
|
trainer.addSample(sample.getData());
|
||||||
|
}
|
||||||
|
byte[] newDictionary = trainer.trainSamples();
|
||||||
|
LOGGER.info("字典训练完成,大小: {} 字节", newDictionary.length);
|
||||||
|
|
||||||
|
String versionId = Instant.now().toString().replace(":", "-");
|
||||||
|
DictionaryVersion version = new DictionaryVersion(versionId, newDictionary);
|
||||||
|
dictionaryVersions.put(versionId, version);
|
||||||
|
LOGGER.info("创建新字典版本: {}", versionId);
|
||||||
|
|
||||||
|
saveSamples(samples);
|
||||||
|
cleanOldVersions();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("训练字典失败: {}", e.getMessage());
|
||||||
|
stats.recordError("TRAINING_ERROR");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] compress(String text) throws IOException {
|
||||||
|
if (dictionaryVersions.isEmpty()) {
|
||||||
|
throw new IllegalStateException("未初始化字典,请先添加样本");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
long startTime = System.nanoTime();
|
||||||
|
byte[] original = text.getBytes("UTF-8");
|
||||||
|
byte[] compressed = Zstd.compressUsingDict(
|
||||||
|
original,
|
||||||
|
getLatestDictionary().getDictionary(),
|
||||||
|
config.getCompressionLevel()
|
||||||
|
);
|
||||||
|
long endTime = System.nanoTime();
|
||||||
|
|
||||||
|
stats.recordCompression(original.length, compressed.length, endTime - startTime);
|
||||||
|
DictionaryVersion latestDict = getLatestDictionary();
|
||||||
|
latestDict.incrementUseCount();
|
||||||
|
latestDict.updatePerformanceMetrics(
|
||||||
|
(double) compressed.length / original.length,
|
||||||
|
endTime - startTime,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
return compressed;
|
||||||
|
} catch (Exception e) {
|
||||||
|
stats.recordError("COMPRESSION_ERROR");
|
||||||
|
throw new IOException("压缩失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String decompress(byte[] compressed) throws IOException {
|
||||||
|
if (dictionaryVersions.isEmpty()) {
|
||||||
|
throw new IllegalStateException("未初始化字典,请先添加样本");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
long startTime = System.nanoTime();
|
||||||
|
try (ByteArrayInputStream bis = new ByteArrayInputStream(compressed);
|
||||||
|
ZstdInputStream zis = new ZstdInputStream(bis)) {
|
||||||
|
zis.setDict(getLatestDictionary().getDictionary());
|
||||||
|
byte[] result = zis.readAllBytes();
|
||||||
|
long endTime = System.nanoTime();
|
||||||
|
|
||||||
|
stats.recordDecompression(endTime - startTime);
|
||||||
|
return new String(result, "UTF-8");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
stats.recordError("DECOMPRESSION_ERROR");
|
||||||
|
throw new IOException("解压失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DictionaryVersion getLatestDictionary() {
|
||||||
|
return dictionaryVersions.values().stream()
|
||||||
|
.max(Comparator.comparing(DictionaryVersion::getCreateTime))
|
||||||
|
.orElseThrow(() -> new IllegalStateException("没有可用的字典"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveSamples(List<Sample> samples) throws IOException {
|
||||||
|
if (samples.isEmpty()) {
|
||||||
|
LOGGER.info("没有样本需要保存");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String timestamp = Instant.now().toString().replace(":", "-");
|
||||||
|
String sampleFileName = String.format("samples_%s_%03d.bin",
|
||||||
|
timestamp, fileCounter.incrementAndGet());
|
||||||
|
Path sampleFile = samplesDir.resolve(sampleFileName);
|
||||||
|
|
||||||
|
LOGGER.info("开始保存样本文件: {}, 样本数量: {}", sampleFileName, samples.size());
|
||||||
|
|
||||||
|
try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(sampleFile))) {
|
||||||
|
out.writeObject(samples);
|
||||||
|
LOGGER.info("样本文件保存成功: {}", sampleFile);
|
||||||
|
|
||||||
|
if (Files.exists(sampleFile)) {
|
||||||
|
long fileSize = Files.size(sampleFile);
|
||||||
|
LOGGER.info("样本文件大小: {} 字节", fileSize);
|
||||||
|
} else {
|
||||||
|
LOGGER.error("样本文件保存失败: 文件不存在");
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("保存样本文件失败: {}, {}", sampleFile, e.getMessage());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanOldVersions() {
|
||||||
|
if (dictionaryVersions.size() > 3) {
|
||||||
|
LOGGER.info("开始清理旧版本字典,当前版本数: {}", dictionaryVersions.size());
|
||||||
|
dictionaryVersions.entrySet().stream()
|
||||||
|
.sorted(Comparator.comparing(e -> e.getValue().getCreateTime()))
|
||||||
|
.limit(dictionaryVersions.size() - 3)
|
||||||
|
.forEach(e -> {
|
||||||
|
dictionaryVersions.remove(e.getKey());
|
||||||
|
LOGGER.info("已删除旧版本字典: {}", e.getKey());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void exportDictionary(String versionId, Path targetPath) throws IOException {
|
||||||
|
DictionaryVersion version = dictionaryVersions.get(versionId);
|
||||||
|
if (version == null) {
|
||||||
|
throw new IllegalArgumentException("字典版本不存在: " + versionId);
|
||||||
|
}
|
||||||
|
Files.write(targetPath, version.getDictionary());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void importDictionary(Path sourcePath, String versionId) throws IOException {
|
||||||
|
byte[] dictionary = Files.readAllBytes(sourcePath);
|
||||||
|
DictionaryVersion version = new DictionaryVersion(versionId, dictionary);
|
||||||
|
dictionaryVersions.put(versionId, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompressionStats getStats() {
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, DictionaryVersion> getDictionaryVersions() {
|
||||||
|
return Collections.unmodifiableMap(dictionaryVersions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
LOGGER.info("开始关闭字典管理器");
|
||||||
|
saveStats();
|
||||||
|
scheduler.shutdown();
|
||||||
|
try {
|
||||||
|
if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) {
|
||||||
|
LOGGER.warn("调度器未能在60秒内正常关闭,强制关闭");
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LOGGER.error("关闭调度器时被中断");
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
LOGGER.info("字典管理器已关闭");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadLatestSamples() throws IOException {
|
||||||
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(samplesDir, "samples_*.bin")) {
|
||||||
|
List<Path> files = new ArrayList<>();
|
||||||
|
stream.forEach(files::add);
|
||||||
|
LOGGER.info("样本文件数量: {}", files.size());
|
||||||
|
|
||||||
|
Optional<Path> latestFile = files.stream()
|
||||||
|
.max(Comparator.comparingLong(p -> {
|
||||||
|
try {
|
||||||
|
return Files.getLastModifiedTime(p).toMillis();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("获取文件修改时间失败: {}, {}", p, e.getMessage());
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (latestFile.isPresent()) {
|
||||||
|
try (ObjectInputStream in = new ObjectInputStream(Files.newInputStream(latestFile.get()))) {
|
||||||
|
List<Sample> loadedSamples = (List<Sample>) in.readObject();
|
||||||
|
LOGGER.info("加载样本文件: {}, 样本数量: {}", latestFile.get().getFileName(), loadedSamples.size());
|
||||||
|
|
||||||
|
for (Sample sample : loadedSamples) {
|
||||||
|
if (sample.getQuality() >= config.getMinQualityThreshold()) {
|
||||||
|
sampleQueue.offer(sample);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sampleQueue.size() >= config.getMaxSamples()) {
|
||||||
|
LOGGER.info("样本数量达到阈值,开始训练字典");
|
||||||
|
trainAndRotate();
|
||||||
|
} else {
|
||||||
|
LOGGER.info("样本数量不足,当前样本数: {}, 需要样本数: {}",
|
||||||
|
sampleQueue.size(), config.getMaxSamples());
|
||||||
|
}
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
throw new IOException("加载样本失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOGGER.info("未找到历史样本文件");
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("加载样本文件失败: {}", e.getMessage());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveStats() {
|
||||||
|
try {
|
||||||
|
String timestamp = Instant.now().toString().replace(":", "-");
|
||||||
|
String statsFileName = String.format("compression_stats_%s_%03d.json",
|
||||||
|
timestamp, fileCounter.incrementAndGet());
|
||||||
|
Path timestampedStatsFile = statsDir.resolve(statsFileName);
|
||||||
|
|
||||||
|
Map<String, Object> statsData = new HashMap<>();
|
||||||
|
statsData.put("totalCompressedSize", stats.getTotalCompressedSize());
|
||||||
|
statsData.put("totalOriginalSize", stats.getTotalOriginalSize());
|
||||||
|
statsData.put("totalCompressionTime", stats.getTotalCompressionTime());
|
||||||
|
statsData.put("totalDecompressionTime", stats.getTotalDecompressionTime());
|
||||||
|
statsData.put("errorCounts", stats.getErrorCounts());
|
||||||
|
statsData.put("averageCompressionRatio", stats.getAverageCompressionRatio());
|
||||||
|
statsData.put("averageCompressionTime", stats.getAverageCompressionTime());
|
||||||
|
statsData.put("averageDecompressionTime", stats.getAverageDecompressionTime());
|
||||||
|
statsData.put("timestamp", timestamp);
|
||||||
|
|
||||||
|
Map<String, Object> timeStats = new HashMap<>();
|
||||||
|
timeStats.put("lastHour", calculateTimeStats(Duration.ofHours(1)));
|
||||||
|
timeStats.put("lastDay", calculateTimeStats(Duration.ofDays(1)));
|
||||||
|
timeStats.put("lastWeek", calculateTimeStats(Duration.ofDays(7)));
|
||||||
|
statsData.put("timeStats", timeStats);
|
||||||
|
|
||||||
|
Map<String, Object> versionInfo = new HashMap<>();
|
||||||
|
dictionaryVersions.forEach((id, version) -> {
|
||||||
|
Map<String, Object> vInfo = new HashMap<>();
|
||||||
|
vInfo.put("createTime", version.getCreateTime().toString());
|
||||||
|
vInfo.put("useCount", version.getUseCount());
|
||||||
|
vInfo.put("compressionRatio", version.getCompressionRatio());
|
||||||
|
vInfo.put("compressionTime", version.getCompressionTime());
|
||||||
|
vInfo.put("decompressionTime", version.getDecompressionTime());
|
||||||
|
versionInfo.put(id, vInfo);
|
||||||
|
});
|
||||||
|
statsData.put("dictionaryVersions", versionInfo);
|
||||||
|
|
||||||
|
Files.writeString(timestampedStatsFile, new com.google.gson.Gson().toJson(statsData));
|
||||||
|
LOGGER.info("已保存统计文件: {}", timestampedStatsFile.getFileName());
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("保存统计数据失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> calculateTimeStats(Duration duration) {
|
||||||
|
Map<String, Object> timeStats = new HashMap<>();
|
||||||
|
Instant cutoff = Instant.now().minus(duration);
|
||||||
|
|
||||||
|
List<DictionaryVersion> recentVersions = dictionaryVersions.values().stream()
|
||||||
|
.filter(v -> v.getCreateTime().isAfter(cutoff))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!recentVersions.isEmpty()) {
|
||||||
|
double avgRatio = recentVersions.stream()
|
||||||
|
.mapToDouble(DictionaryVersion::getCompressionRatio)
|
||||||
|
.average()
|
||||||
|
.orElse(0.0);
|
||||||
|
|
||||||
|
double avgCompTime = recentVersions.stream()
|
||||||
|
.mapToLong(DictionaryVersion::getCompressionTime)
|
||||||
|
.average()
|
||||||
|
.orElse(0.0);
|
||||||
|
|
||||||
|
double avgDecompTime = recentVersions.stream()
|
||||||
|
.mapToLong(DictionaryVersion::getDecompressionTime)
|
||||||
|
.average()
|
||||||
|
.orElse(0.0);
|
||||||
|
|
||||||
|
timeStats.put("averageCompressionRatio", avgRatio);
|
||||||
|
timeStats.put("averageCompressionTime", avgCompTime);
|
||||||
|
timeStats.put("averageDecompressionTime", avgDecompTime);
|
||||||
|
timeStats.put("versionCount", recentVersions.size());
|
||||||
|
timeStats.put("totalUseCount", recentVersions.stream()
|
||||||
|
.mapToLong(DictionaryVersion::getUseCount)
|
||||||
|
.sum());
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadStats() {
|
||||||
|
try {
|
||||||
|
List<Path> statsFiles = Files.list(statsDir)
|
||||||
|
.filter(p -> p.getFileName().toString().startsWith("compression_stats_"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
LOGGER.info("统计文件数量: {}", statsFiles.size());
|
||||||
|
|
||||||
|
Optional<Path> latestStatsFile = statsFiles.stream()
|
||||||
|
.max(Comparator.comparing(p -> {
|
||||||
|
try {
|
||||||
|
return Files.getLastModifiedTime(p).toMillis();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("获取文件修改时间失败: {}, {}", p, e.getMessage());
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (latestStatsFile.isPresent()) {
|
||||||
|
Path statsPath = latestStatsFile.get();
|
||||||
|
LOGGER.info("加载统计文件: {}", statsPath.getFileName());
|
||||||
|
|
||||||
|
try {
|
||||||
|
String json = Files.readString(statsPath);
|
||||||
|
Map<String, Object> statsData = new com.google.gson.Gson().fromJson(json, Map.class);
|
||||||
|
|
||||||
|
// 更新统计数据
|
||||||
|
if (statsData.containsKey("totalCompressedSize")) {
|
||||||
|
long originalSize = ((Number) statsData.get("totalOriginalSize")).longValue();
|
||||||
|
long compressedSize = ((Number) statsData.get("totalCompressedSize")).longValue();
|
||||||
|
long compressionTime = ((Number) statsData.get("totalCompressionTime")).longValue();
|
||||||
|
|
||||||
|
if (originalSize > 0 && compressedSize > 0) {
|
||||||
|
stats.recordCompression(originalSize, compressedSize, compressionTime);
|
||||||
|
LOGGER.info("已加载压缩统计: 原始大小={}, 压缩后大小={}, 压缩时间={}ns",
|
||||||
|
originalSize, compressedSize, compressionTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statsData.containsKey("totalDecompressionTime")) {
|
||||||
|
long decompressionTime = ((Number) statsData.get("totalDecompressionTime")).longValue();
|
||||||
|
if (decompressionTime > 0) {
|
||||||
|
stats.recordDecompression(decompressionTime);
|
||||||
|
LOGGER.info("已加载解压统计: 解压时间={}ns", decompressionTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statsData.containsKey("errorCounts")) {
|
||||||
|
Map<String, Number> errorCounts = (Map<String, Number>) statsData.get("errorCounts");
|
||||||
|
errorCounts.forEach((error, count) -> {
|
||||||
|
long errorCount = count.longValue();
|
||||||
|
if (errorCount > 0) {
|
||||||
|
for (int i = 0; i < errorCount; i++) {
|
||||||
|
stats.recordError(error.toString());
|
||||||
|
}
|
||||||
|
LOGGER.info("已加载错误统计: {}={}", error, errorCount);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("解析统计文件失败: {}, {}", statsPath, e.getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOGGER.info("未找到历史统计文件,将使用新的统计记录");
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("加载统计数据失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SampleMetrics {
|
||||||
|
final double entropy;
|
||||||
|
final double uniqueRatio;
|
||||||
|
final double repetitionRatio;
|
||||||
|
final double distributionScore;
|
||||||
|
final double compressionPotential;
|
||||||
|
|
||||||
|
SampleMetrics(double entropy, double uniqueRatio, double repetitionRatio,
|
||||||
|
double distributionScore, double compressionPotential) {
|
||||||
|
this.entropy = entropy;
|
||||||
|
this.uniqueRatio = uniqueRatio;
|
||||||
|
this.repetitionRatio = repetitionRatio;
|
||||||
|
this.distributionScore = distributionScore;
|
||||||
|
this.compressionPotential = compressionPotential;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DictionaryConfig {
|
||||||
|
private final int minSampleSize;
|
||||||
|
private final int maxSampleSize;
|
||||||
|
private final double minQualityThreshold;
|
||||||
|
private final int compressionLevel;
|
||||||
|
private final int maxDictionarySize;
|
||||||
|
private final Duration trainingInterval;
|
||||||
|
private final int maxSamples;
|
||||||
|
private final int maxSampleFileSize;
|
||||||
|
private final boolean scheduledTraining;
|
||||||
|
|
||||||
|
private DictionaryConfig(Builder builder) {
|
||||||
|
this.minSampleSize = builder.minSampleSize;
|
||||||
|
this.maxSampleSize = builder.maxSampleSize;
|
||||||
|
this.minQualityThreshold = builder.minQualityThreshold;
|
||||||
|
this.compressionLevel = builder.compressionLevel;
|
||||||
|
this.maxDictionarySize = builder.maxDictionarySize;
|
||||||
|
this.trainingInterval = builder.trainingInterval;
|
||||||
|
this.maxSamples = builder.maxSamples;
|
||||||
|
this.maxSampleFileSize = builder.maxSampleFileSize;
|
||||||
|
this.scheduledTraining = builder.scheduledTraining;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMinSampleSize() {
|
||||||
|
return minSampleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxSampleSize() {
|
||||||
|
return maxSampleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getMinQualityThreshold() {
|
||||||
|
return minQualityThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCompressionLevel() {
|
||||||
|
return compressionLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxDictionarySize() {
|
||||||
|
return maxDictionarySize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Duration getTrainingInterval() {
|
||||||
|
return trainingInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxSamples() {
|
||||||
|
return maxSamples;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxSampleFileSize() {
|
||||||
|
return maxSampleFileSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isScheduledTraining() {
|
||||||
|
return scheduledTraining;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private int minSampleSize = 1024;
|
||||||
|
private int maxSampleSize = 10 * 1024 * 1024;
|
||||||
|
private double minQualityThreshold = 0.5;
|
||||||
|
private int compressionLevel = 3;
|
||||||
|
private int maxDictionarySize = 10 * 1024;
|
||||||
|
private Duration trainingInterval = Duration.ofMinutes(30);
|
||||||
|
private int maxSamples = 100;
|
||||||
|
private int maxSampleFileSize = 10 * 1024 * 1024;
|
||||||
|
private boolean scheduledTraining = false;
|
||||||
|
|
||||||
|
public Builder minSampleSize(int size) {
|
||||||
|
this.minSampleSize = size;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder maxSampleSize(int size) {
|
||||||
|
this.maxSampleSize = size;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder minQualityThreshold(double threshold) {
|
||||||
|
this.minQualityThreshold = threshold;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder compressionLevel(int level) {
|
||||||
|
this.compressionLevel = level;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder maxDictionarySize(int size) {
|
||||||
|
this.maxDictionarySize = size;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder trainingInterval(Duration interval) {
|
||||||
|
this.trainingInterval = interval;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder maxSamples(int count) {
|
||||||
|
this.maxSamples = count;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder maxSampleFileSize(int size) {
|
||||||
|
this.maxSampleFileSize = size;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder scheduledTraining(boolean scheduledTraining) {
|
||||||
|
this.scheduledTraining = scheduledTraining;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DictionaryConfig build() {
|
||||||
|
return new DictionaryConfig(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Sample implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final byte[] data;
|
||||||
|
private final Instant timestamp;
|
||||||
|
private final String source;
|
||||||
|
private final Map<String, Object> metadata;
|
||||||
|
private double quality;
|
||||||
|
|
||||||
|
public Sample(byte[] data, String source) {
|
||||||
|
this.data = data;
|
||||||
|
this.timestamp = Instant.now();
|
||||||
|
this.source = source;
|
||||||
|
this.quality = 1.0;
|
||||||
|
this.metadata = new ConcurrentHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSource() {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getQuality() {
|
||||||
|
return quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQuality(double quality) {
|
||||||
|
this.quality = Math.max(0.0, Math.min(1.0, quality));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getMetadata() {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CompressionStats {
|
||||||
|
private final AtomicLong totalCompressedSize;
|
||||||
|
private final AtomicLong totalOriginalSize;
|
||||||
|
private final AtomicLong totalCompressionTime;
|
||||||
|
private final AtomicLong totalDecompressionTime;
|
||||||
|
private final Map<String, AtomicLong> errorCounts;
|
||||||
|
|
||||||
|
public CompressionStats() {
|
||||||
|
this.totalCompressedSize = new AtomicLong(0);
|
||||||
|
this.totalOriginalSize = new AtomicLong(0);
|
||||||
|
this.totalCompressionTime = new AtomicLong(0);
|
||||||
|
this.totalDecompressionTime = new AtomicLong(0);
|
||||||
|
this.errorCounts = new ConcurrentHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordCompression(long originalSize, long compressedSize, long time) {
|
||||||
|
totalOriginalSize.addAndGet(originalSize);
|
||||||
|
totalCompressedSize.addAndGet(compressedSize);
|
||||||
|
totalCompressionTime.addAndGet(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordDecompression(long time) {
|
||||||
|
totalDecompressionTime.addAndGet(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recordError(String errorType) {
|
||||||
|
errorCounts.computeIfAbsent(errorType, k -> new AtomicLong(0)).incrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAverageCompressionRatio() {
|
||||||
|
long original = totalOriginalSize.get();
|
||||||
|
return original > 0 ? (double) totalCompressedSize.get() / original : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAverageCompressionTime() {
|
||||||
|
long count = totalOriginalSize.get();
|
||||||
|
return count > 0 ? (double) totalCompressionTime.get() / count : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAverageDecompressionTime() {
|
||||||
|
long count = totalOriginalSize.get();
|
||||||
|
return count > 0 ? (double) totalDecompressionTime.get() / count : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalCompressedSize() {
|
||||||
|
return totalCompressedSize.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalOriginalSize() {
|
||||||
|
return totalOriginalSize.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalCompressionTime() {
|
||||||
|
return totalCompressionTime.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalDecompressionTime() {
|
||||||
|
return totalDecompressionTime.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Long> getErrorCounts() {
|
||||||
|
Map<String, Long> result = new ConcurrentHashMap<>();
|
||||||
|
errorCounts.forEach((k, v) -> result.put(k, v.get()));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DictionaryVersion implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final String versionId;
|
||||||
|
private final Instant createTime;
|
||||||
|
private final AtomicLong useCount;
|
||||||
|
private final byte[] dictionary;
|
||||||
|
private final Map<String, Object> metadata;
|
||||||
|
private double compressionRatio;
|
||||||
|
private long compressionTime;
|
||||||
|
private long decompressionTime;
|
||||||
|
|
||||||
|
public DictionaryVersion(String versionId, byte[] dictionary) {
|
||||||
|
this.versionId = versionId;
|
||||||
|
this.createTime = Instant.now();
|
||||||
|
this.useCount = new AtomicLong(0);
|
||||||
|
this.dictionary = dictionary;
|
||||||
|
this.metadata = new ConcurrentHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void incrementUseCount() {
|
||||||
|
useCount.incrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updatePerformanceMetrics(double ratio, long compTime, long decompTime) {
|
||||||
|
this.compressionRatio = ratio;
|
||||||
|
this.compressionTime = compTime;
|
||||||
|
this.decompressionTime = decompTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVersionId() {
|
||||||
|
return versionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getCreateTime() {
|
||||||
|
return createTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getUseCount() {
|
||||||
|
return useCount.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getDictionary() {
|
||||||
|
return dictionary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getMetadata() {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getCompressionRatio() {
|
||||||
|
return compressionRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCompressionTime() {
|
||||||
|
return compressionTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDecompressionTime() {
|
||||||
|
return decompressionTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
src/main/resources/mongodb.yml
Normal file
5
src/main/resources/mongodb.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
host: "localhost"
|
||||||
|
port: 27017
|
||||||
|
database: "admin"
|
||||||
|
username: "root"
|
||||||
|
password: "123456"
|
|
@ -1,3 +1,3 @@
|
||||||
redis-server: 127.0.0.1
|
server: 127.0.0.1
|
||||||
redis-port: 6379
|
port: 6379
|
||||||
redis-password: "none"
|
password: "none"
|
Loading…
Reference in New Issue
Block a user