Skip to content

Databases

github-actions[bot] edited this page Nov 13, 2026 · 18 revisions

MySQL, MariaDB и Redis

HLib включает HikariCP, MySQL Connector/J, MariaDB driver, Jedis и Commons Pool. Эти библиотеки уже находятся в HLib.jar, поэтому отдельное встраивание в ваш plugin JAR не требуется.

Библиотека Версия в сборке
HikariCP 4.0.3
MySQL Connector/J 8.0.31
MariaDB Connector/J 3.1.4
Jedis 4.2.2
Commons Pool 2.11.1

Все DB-вызовы HLib синхронные. Планирование вне main thread — ответственность вызывающего кода.

HLib сам не открывает SQL- или Redis-подключения. Их создаёт и настраивает использующий библиотеку плагин; для обычных утилит база данных не нужна.

MySQL и MariaDB

Создание пула

MySQL mysql = new MySQL(
	"127.0.0.1:3306",
	"minecraft",
	"plugin",
	"password",
	null,
	"mariadb",
	300,
	5,
	1800,
	10
);

if (!mysql.isEnabled()) {
	throw new IllegalStateException("Подключение к MariaDB не создано");
}

Аргументы конструктора:

Аргумент Значение Default при null/пустом значении
address host:port обязателен
database имя базы обязателен
username, password учётные данные передаются как есть
url JDBC template с {driver}, {address}, {database} стандартный JDBC URL
driver mysql или mariadb mysql
keepaliveTime секунды 300
connectionTimeout секунды 5
maxLifetime секунды 1800
poolSize maximum pool size 10

Отсутствующие параметры JDBC URL дополняются connectTimeout=5000 и socketTimeout=60000 в миллисекундах для MySQL и MariaDB. Первый ограничивает подключение TCP, второй — ожидание данных из socket, включая ответ SQL и commit. Это отдельные настройки от connectionTimeout конструктора, который ограничивает только ожидание свободного connection в HikariCP. Socket timeout не задаёт общий deadline всей транзакции и не отменяет блокирующие callbacks вызывающего кода.

Явные параметры URL сохраняются, включая повторения и значение 0, отключающее соответствующий timeout. Имена сопоставляются по правилам используемого драйвера: MariaDB без учёта регистра, MySQL с точным написанием connectTimeout/socketTimeout; исходный URL не переписывается. Для пользовательского URL без таймаутов действуют те же defaults. Запросы и транзакции автоматически не повторяются: при обрыве во время commit результат может быть неопределённым. Если требуются длительные операции, задайте подходящий socketTimeout явно; значение по умолчанию оставляет запас для поддерживаемого GET_LOCK до 30 секунд. Сетевые ограничения описаны в MariaDB Connector/J и MySQL Connector/J.

Если HLib-компоненты должны использовать этот pool, зарегистрируйте его:

Tools.setMysqlInstance(mysql);

Это один глобальный пул HLib. Не заменяйте чужое подключение, если нескольким плагинам нужен общий доступ. Для собственных таблиц можно хранить MySQL локально и вообще не регистрировать его в Tools.

Prepared query

Scheduler.doAsync(() -> {
	AtomicReference<String> name = new AtomicReference<>();

	boolean success = mysql.query("SELECT name FROM players WHERE id = ?")
		.setArgs(playerId)
		.setResult(resultSet -> {
			if (resultSet.next()) {
				name.set(resultSet.getString("name"));
			}
		})
		.execute();

	Scheduler.Bukkit.doSync(() -> {
		String result = success ? name.get() : null;
		Informer.send(player, Informer.component("&7Результат: &f" + result));
	});
});

Callback setResult выполняется в том же потоке, что и запрос, пока ResultSet открыт. Не сохраняйте ResultSet для последующего использования.

INSERT и generated key

Optional<Integer> id = mysql.query("INSERT INTO players (name) VALUES (?)")
	.setArgs(name)
	.executeInsert();

Пустой Optional означает либо отсутствие generated key, либо ошибку. Эти состояния API не различает.

Batch

int[] result = mysql.query("INSERT INTO player_tags (player_id, tag) VALUES (?, ?)")
	.addBatch(10, "builder")
	.addBatch(11, "admin")
	.executeBatch();

Batch выполняется без явной транзакции. Пустой массив означает и пустой batch, и ошибку.

Транзакции

Для атомарного изменения нескольких записей используйте MySQL.transaction(...). Callback получает один MySQL.Transaction, связанный с одним JDBC connection и текущим потоком:

long purchaseId = mysql.transaction(transaction -> {
	int changed = transaction.update(
		"UPDATE wallets SET balance = balance - ? WHERE name = ? AND balance >= ?",
		price, name, price
	);
	if (changed != 1) throw new java.sql.SQLException("Недостаточный баланс или неизвестный игрок");
	return transaction.insert(
		"INSERT INTO purchases (name, price) VALUES (?, ?)",
		name, price
	);
});
Метод Результат
transaction.update(sql, args...) Количество изменённых строк как int
transaction.insert(sql, args...) Generated key как long; требует ровно одну вставленную строку и непустой ключ
transaction.query(sql, reader, args...) Значение, возвращённое reader.apply(ResultSet)
mysql.transaction(action) Значение callback после успешного commit

Оба callback используют MySQL.ThrowingFunction<T, R> и могут выбрасывать SQLException. ResultSet открыт только во время reader; сохраняйте извлечённые значения, а не JDBC-объекты. При выходе из основного callback транзакция закрывается для последующих вызовов, даже если её объект был сохранён. Использование из другого потока запрещено.

При нормальном возврате callback выполняется commit. SQLException, RuntimeException и Error приводят к попытке rollback и передаются вызывающему коду. Ошибка rollback добавляется к исходной как suppressed; statements, result sets и connection закрываются автоматически. Автоматических повторов нет. Ошибка связи во время commit может означать неопределённый результат на стороне сервера: не повторяйте денежную операцию вслепую.

Все вызовы остаются синхронными; выполняйте их через async Scheduler. Не вызывайте команды Bukkit, внешние HTTP API или выдачу предметов внутри callback: SQL rollback не отменяет эти действия. Транзакции гарантируют атомарность только для transactional таблиц (например, InnoDB); DDL и другие команды с неявным commit в callback не допускаются. Вложенный mysql.query(...) открывает отдельное соединение и не входит в текущую транзакцию — используйте только переданный объект Transaction.

Новое API не подавляет и не логирует SQL-ошибки: обработка и безопасное сообщение об ошибке остаются ответственностью вызывающего кода. Старый builder MySQL.Query сохраняет прежнее поведение.

Транзакция под именованной блокировкой

HLib 7.9 добавляет mysql.lockedTransaction(lockName, timeoutSeconds, callback). API получает MySQL/MariaDB GET_LOCK на том же выделенном connection до callback и освобождает RELEASE_LOCK во внешнем finally после commit либо rollback. Это подходит для общей квоты, которую одновременно применяют несколько сервисов.

mysql.lockedTransaction("example:registration", 5, transaction -> {
	long count = transaction.query("SELECT COUNT(*) FROM registration_event WHERE registered_at >= ?", result -> {
		result.next();
		return result.getLong(1);
	}, since);
	if (count >= maximum) throw new java.sql.SQLException("Registration quota reached");
	transaction.update("INSERT INTO registration_event (registered_at) VALUES (?)", now);
	return null;
});

Имя допускает 1–64 символа a-z A-Z 0-9 : _ -, ожидание — 0–30 секунд. Timeout либо ошибка получения lock не запускает callback. Ошибка освобождения закрывает физическое соединение через abort, чтобы блокировка не осталась в пуле. Первичная ошибка сохраняется; ошибки rollback/release/abort/close добавляются как suppressed. Нельзя вручную освобождать lock или выполнять COMMIT внутри callback. Обычный transaction сохраняет прежнюю семантику. HTTP-политику и другие внешние чтения готовьте до захвата блокировки, а актуальные SQL-счётчики читайте внутри неё.

Своя DDL-константа

public enum Tables implements MySQLTable {
	PLAYER_TAGS("CREATE TABLE IF NOT EXISTS player_tags (player_id INT NOT NULL, tag VARCHAR(64) NOT NULL)");

	private final String value;

	Tables(String value) {
		this.value = value;
	}

	@Override
	public String getValue() {
		return value;
	}
}
mysql.query(Tables.PLAYER_TAGS).setQuiet(false).execute();

Overload query(MySQLTable) включает quiet-режим автоматически. setQuiet(false) нужен, чтобы ошибка DDL была видна в логе и execute() вернул false.

SQL-модели HLib

При использовании встроенных моделей после регистрации глобального пула отдельно инициализируйте нужные таблицы:

Scheduler.doAsync(HPlayer::initMySQL);
Cooldowner.initMySQL();

HPlayer.initMySQL() синхронно создаёт и загружает hlib_player; поэтому в примере он запускается вне main thread. Cooldowner.initMySQL() сам планирует асинхронную загрузку hlib_cooldown. Не используйте эти подсистемы до завершения их загрузки; единого callback готовности нет.

HPlayer.createHPlayer(name) возвращает известный профиль из кэша; при промахе выполняет INSERT имени с no-op при дубликате и SELECT id/name FOR UPDATE в одной SQL-транзакции. Профиль, созданный сайтом или другим потоком, использует реальный ID и сохранённый регистр имени; незавершённая внешняя вставка сначала должна commit/rollback. Объект публикуется в кэше только после успешного commit. Ошибка SQL сохраняет исходную причину в IllegalStateException и не добавляет ложный профиль. getHPlayer(name) и getHPlayer(id) остаются cache-only и могут вернуть null.

Создание и публикация синхронизированы общим закрытым lock; одинаковая пара ID/имя при повторной загрузке принимается идемпотентно, фабрики возвращают уже известный объект. Public-конструктор также синхронизирует обе карты и может публиковать уже подтверждённую транзакцией пару без нового SQL. Конфликт ID с другим именем или имени с другим ID отклоняется без подмены кэша. Это не API переименования: уже закэшированный профиль автоматически не перечитывается при внешнем SQL rename/delete. HLib не регистрирует обработчики входа игроков для заполнения модели — это делает использующий её плагин. Все операции SQL синхронны; вызывающий код обязан использовать рабочий поток и не запускать parent INSERT отдельно от зависимых записей.

Ограничения MySQL API

  • MySQL.Query изменяемый и не должен использоваться одновременно из нескольких потоков.
  • SELECT требует setResult; иначе HLib вызывает executeUpdate.
  • Query с setResult всегда использует executeQuery, даже если SQL является UPDATE.
  • query(sql, true).execute() возвращает true и при SQLException; quiet-режим не подходит для контроля успеха.
  • Batch не открывает явную транзакцию.
  • Аргументы SQL-ошибки выводятся в лог. Не передавайте секреты как параметры запросов, если их нельзя логировать.
  • setEnabled(true) не восстанавливает уже закрытый pool.
  • Вызывайте mysql.shutdown() при остановке владельца подключения.

Redis

Подключение и проверка

Redis redis = new Redis("127.0.0.1:6379", "");

try (Jedis jedis = redis.getPool().getResource()) {
	String pong = jedis.ping();

	if (!"PONG".equals(pong)) {
		throw new IllegalStateException("Redis не ответил PONG");
	}
}

Tools.setRedisInstance(redis);

Constructor не проверяет соединение. Выполните PING самостоятельно. IPv6-адреса текущим parser не поддерживаются.

Publish

Scheduler.doAsync(() -> redis.publish("example:updates", "reload"));

При ошибке publish подавляет исключение и закрывает Redis. Вызывающий код не получает результат публикации.

Subscribe

Jedis.subscribe блокирует поток до unsubscribe. Используйте отдельный executor, а не main thread и не общий короткоживущий task.

ExecutorService subscriberExecutor = Executors.newSingleThreadExecutor();

subscriberExecutor.execute(() -> {
	try (Jedis jedis = redis.getPool().getResource()) {
		jedis.subscribe(new Redis.PubSub() {
			@Override
			public void onMessage(String channel, String message) {
				Informer.send(channel + ": " + message);
			}
		}, "example:updates");
	}
});

При shutdown вызовите:

redis.shutdown();
subscriberExecutor.shutdown();

Все Redis.PubSub хранятся в одном глобальном set и не удаляются автоматически. Shutdown одного Redis вызывает unsubscribe для активных subscribers всех экземпляров; предпочтительно иметь один общий Redis instance.

На Bukkit HLib закрывает зарегистрированные глобальные SQL/Redis-пулы при собственном отключении. На Velocity такого закрытия в entrypoint нет; владелец подключений должен закрывать их сам. Отдельные, не зарегистрированные в Tools экземпляры всегда закрывает их владелец.