В современных приложениях Java работа с базами данных является одним из ключевых аспектов разработки. Взаимодействие с базами данных может происходить с использованием различных технологий, таких как JDBC и ORM (Object-Relational Mapping) фреймворки (например, Hibernate).
Однако при работе с базами данных разработчикам нужно учитывать различные феномены чтения, которые могут возникнуть в процессе исполнения транзакций. В данной статье мы рассмотрим три основных феномена чтения:
Кроме того, мы обсудим методы предотвращения этих явлений в контексте разработки с использованием JDBС, как наиболее базового метода.
Грязное чтение
Феномен грязного чтения происходит, когда транзакция получает доступ к данным, измененным, но еще не зафиксированным в рамках другой параллельной транзакции. Вследствие этого, транзакция, осуществляющая чтение, может работать с неправильными данными, в случае если вторая транзакция, вносящая изменения, в конечном итоге будет отменена (ROLLBACK).
Предположим, что у нас есть таблица products
с двумя столбцами: id
и price
. Имеется следующая запись в таблице:
id | price
---------
1 | 100
Теперь предположим, что у нас есть две параллельные транзакции: Т1 и Т2. Т1 изменяет цену продукта, а Т2 читает данные.
BEGIN;
UPDATE products SET price = 120 WHERE id = 1;
-- Тут Т2 читает данные
ROLLBACK;
Транзакция Т2:
BEGIN; -- Тут Т1 изменяет цену продукта SELECT * FROM products WHERE id = 1; COMMIT;
Пошаговое описание:
- Транзакция Т1 начинается с изменения цены продукта с идентификатором 1 (с 100 на 120). Изменения пока не фиксированы.
- Транзакция Т2 начинается и читает данные продукта с идентификатором 1. Из-за того, что изменения Т1 еще не зафиксированы, Т2 читает грязные данные (цена 120, хотя она еще не была фиксирована).
- Транзакция Т1 откатывается (ROLLBACK), возвращая цену продукта к исходному значению (100). Однако Т2 уже получила некорректные данные (120) и будет работать с ними дальше.
- Транзакция Т2 завершается.
Чтобы избежать грязного чтения при работе с JDBC, вы должны установить уровень изоляции транзакции, который предотвращает чтение незафиксированных данных. Уровень изоляции READ_COMMITTED является одним из наиболее распространенных выборов для этой цели. Вот как вы можете установить уровень изоляции транзакции в JDBC:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class JDBCTransactionExample {
public static void main(String[] args) {
String url = "jdbc:your_database_url";
String username = "your_username";
String password = "your_password";
try (Connection connection = DriverManager.getConnection(url, username, password)) {
// Установка уровня изоляции транзакции на READ_COMMITTED
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
// Начало транзакции
connection.setAutoCommit(false);
// Выполнение SQL-запросов
// Фиксация транзакции
connection.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
В этом примере кода, уровень изоляции транзакции устанавливается на Connection.TRANSACTION_READ_COMMITTED
, который гарантирует, что ваша транзакция будет читать только зафиксированные данные, предотвращая грязное чтение. Обратите внимание, что уровни изоляции транзакций могут варьироваться в зависимости от вашей СУБД, и некоторые СУБД могут иметь дополнительные уровни изоляции для лучшего контроля над параллельными транзакциями.
Неповторяемое чтение
Неповторяемое чтение – это феномен, который возникает, когда одна транзакция неоднократно читает одни и те же данные, и в то время как другая параллельная транзакция изменяет эти данные и фиксирует изменения. В результате, читающая транзакция получает разные значения при повторном чтении.
Предположим, что у нас есть таблица accounts
с двумя столбцами: id
и balance
. Имеется следующая запись в таблице:
id | balance
---------
1 | 5000
Теперь предположим, что у нас есть две параллельные транзакции: Т1 и Т2. Т1 изменяет баланс, а Т2 читает данные дважды.
Транзакция Т1:
BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
-- Тут Т2 делает второе чтение
COMMIT;
Транзакция Т2:
BEGIN; SELECT * FROM accounts WHERE id = 1; -- Тут Т1 изменяет баланс и фиксирует транзакцию SELECT * FROM accounts WHERE id = 1; COMMIT;
Пошаговое описание:
- Транзакция Т2 начинается и читает данные аккаунта с идентификатором 1. Баланс равен 5000.
- Транзакция Т1 начинается и изменяет баланс аккаунта с идентификатором 1, вычитая 1000. Изменения пока не фиксированы.
- Транзакция Т1 фиксирует изменения, устанавливая баланс аккаунта с идентификатором 1 равным 4000.
- Транзакция Т2 делает повторное чтение данных аккаунта с идентификатором 1 и получает новый баланс, равный 4000, вместо исходных 5000.
- Транзакция Т2 завершается.
В данном примере, Т2 получила разные значения при повторном чтении данных из-за того, что Т1 изменила и зафиксировала данные между двумя чтениями Т2. Это явление называется неповторяемым чтением.
Чтобы избежать неповторяемого чтения при работе с JDBC, вам следует установить уровень изоляции транзакции на REPEATABLE_READ или выше. Уровень изоляции REPEATABLE_READ гарантирует, что ваша транзакция будет видеть одну и ту же версию данных в течение всей транзакции, даже если другие транзакции вносят изменения и фиксируют их. Вот как установить уровень изоляции транзакции в JDBC:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class JDBCTransactionExample {
public static void main(String[] args) {
String url = "jdbc:your_database_url";
String username = "your_username";
String password = "your_password";
try (Connection connection = DriverManager.getConnection(url, username, password)) {
// Установка уровня изоляции транзакции на REPEATABLE_READ
connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
// Начало транзакции
connection.setAutoCommit(false);
// Выполнение SQL-запросов
// Фиксация транзакции
connection.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
В этом примере кода, уровень изоляции транзакции устанавливается на Connection.TRANSACTION_REPEATABLE_READ
, который гарантирует, что ваша транзакция будет видеть одну и ту же версию данных в течение всей транзакции, предотвращая неповторяемое чтение. Обратите внимание, что уровни изоляции транзакций могут варьироваться в зависимости от вашей СУБД, и некоторые СУБД могут иметь дополнительные уровни изоляции для лучшего контроля над параллельными транзакциями.
Фантомное чтение
Фантомное чтение – это феномен, который возникает, когда одна транзакция неоднократно читает набор данных, соответствующий определенному условию, и в то время как другая параллельная транзакция вставляет или удаляет строки, удовлетворяющие этому условию, и фиксирует изменения. В результате, читающая транзакция получает разное количество строк при повторном чтении.
Пример с SQL-запросами:
Предположим, что у нас есть таблица orders
с двумя столбцами: id
и status
. Имеются следующие записи в таблице:
id | status
---------
1 | new
2 | new
Теперь предположим, что у нас есть две параллельные транзакции: Т1 и Т2. Т1 добавляет новый заказ, а Т2 читает заказы со статусом “new” дважды.
Транзакция Т1:
BEGIN;
INSERT INTO orders (id, status) VALUES (3, 'new');
-- Тут Т2 делает второе чтение
COMMIT;
Транзакция Т2:
BEGIN; SELECT * FROM orders WHERE status = 'new'; -- Тут Т1 добавляет новый заказ и фиксирует транзакцию SELECT * FROM orders WHERE status = 'new'; COMMIT;
Пошаговое описание:
- Транзакция Т2 начинается и читает заказы со статусом “new”. В результате выборки получены заказы с идентификаторами 1 и 2.
- Транзакция Т1 начинается и добавляет новый заказ с идентификатором 3 и статусом “new”. Изменения пока не фиксированы.
- Транзакция Т1 фиксирует изменения, сохраняя новый заказ в таблице.
- Транзакция Т2 делает повторное чтение заказов со статусом “new” и обнаруживает новую строку с идентификатором 3, которая не была видна при первом чтении.
- Транзакция Т2 завершается.
В данном примере, Т2 обнаружила “фантомную” строку при повторном чтении данных из-за того, что Т1 добавила новую строку и зафиксировала изменения между двумя чтениями Т2. Это явление называется фантомным чтением.
Чтобы избежать фантомного чтения при работе с JDBC, вам следует установить уровень изоляции транзакции на SERIALIZABLE. Уровень изоляции SERIALIZABLE предоставляет самый строгий контроль над параллельными транзакциями и гарантирует, что ваша транзакция не будет видеть никаких новых строк, добавленных другими транзакциями, во время своего выполнения. Вот как установить уровень изоляции транзакции в JDBC:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class JDBCTransactionExample {
public static void main(String[] args) {
String url = "jdbc:your_database_url";
String username = "your_username";
String password = "your_password";
try (Connection connection = DriverManager.getConnection(url, username, password)) {
// Установка уровня изоляции транзакции на SERIALIZABLE
connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
// Начало транзакции
connection.setAutoCommit(false);
// Выполнение SQL-запросов
// Фиксация транзакции
connection.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
В этом примере кода, уровень изоляции транзакции устанавливается на Connection.TRANSACTION_SERIALIZABLE
, который гарантирует, что ваша транзакция будет изолирована от изменений, сделанных другими параллельными транзакциями, предотвращая фантомное чтение. Обратите внимание, что уровни изоляции транзакций могут варьироваться в зависимости от вашей СУБД, и некоторые СУБД могут иметь дополнительные уровни изоляции для лучшего контроля над параллельными транзакциями.
В данной статье мы рассмотрели феномены чтения в контексте разработки ПО.