Иногда вы просто не можете избежать этого: пессимистическая блокировка через SQL. На самом деле, это потрясающий инструмент, когда вы хотите синхронизировать несколько приложений с помощью общей глобальной блокировки.
Некоторые могут подумать, что это злоупотребляет базой данных. Мы думаем использовать инструменты, которые у вас есть, если они могут решить вашу проблему. Например, СУБД может быть идеальной реализацией для очереди сообщений .
Предположим, у вас есть сценарий использования пессимистической блокировки и вы хотите выбрать СУБД. Теперь, как сделать это правильно? Потому что действительно легко завести тупик. Представьте себе следующую настройку (для этого я использую Oracle):
CREATE TABLE locks (v NUMBER(18));
INSERT INTO locks
SELECT level
FROM dual
CONNECT BY level <= 10;
Это создает 10 записей, которые мы будем использовать как 10 различных блокировок на уровне строк.
Теперь давайте подключимся к базе данных из двух клиентов sqlplus:
Экземпляр 1
SQL> SELECT *
2 FROM locks
3 WHERE v = 1
4 FOR UPDATE;
V
----------
1
Экземпляр 2
SQL> SELECT *
2 FROM locks
3 WHERE v = 2
4 FOR UPDATE;
V
----------
2
Теперь мы получили две разные блокировки из двух разных сессий.
А потом, давайте обратные вещи:
Экземпляр 1
SQL> SELECT *
2 FROM locks
3 WHERE v = 2
4 FOR UPDATE;
Экземпляр 2
SQL> SELECT *
2 FROM locks
3 WHERE v = 1
4 FOR UPDATE;
Оба сеанса теперь заблокированы, и, к счастью, Oracle обнаружит это и провалит один из сеансов:
ORA-00060: deadlock detected while waiting for resource
Как избежать тупиков
Это очень явный пример, когда легко понять, почему это происходит, и, возможно, как этого избежать. Простой способ избежать взаимоблокировок — это установить правило, что все блокировки всегда должны быть получены в порядке возрастания. Если вы знаете, что вам нужны блокировки № 1 и 2, вы должны приобрести их в этом порядке. Таким образом, вы все равно будете производить блокировку и, следовательно, конфликт, но, по крайней мере, конфликт, в конце концов (вероятно) будет разрешен после уменьшения нагрузки. Вот пример, который показывает, что происходит, когда у вас больше клиентов. На этот раз написано как потоки Java.
В этом примере мы используем jOOλ для более простых лямбда-выражений (например, лямбда-выражения, выбрасывающие проверенные исключения). И, конечно же, мы будем сильно злоупотреблять Java 8!
Class.forName("oracle.jdbc.OracleDriver");
// We want a collection of 4 threads and their
// associated execution counters
List<Tuple2<Thread, AtomicLong>> list =
IntStream
.range(0, 4)
// Let's use jOOλ here to wrap checked exceptions
// we'll map the thread index to the actual tuple
.mapToObj(Unchecked.intFunction(i -> {
final Connection con = DriverManager.getConnection(
"jdbc:oracle:thin:@localhost:1521:xe",
"TEST", "TEST");
final AtomicLong counter = new AtomicLong();
final Random rnd = new Random();
return Tuple.tuple(
// Each thread acquires a random number of
// locks in ascending order
new Thread(Unchecked.runnable(() -> {
for (;;) {
String sql =
" SELECT *"
+ " FROM locks"
+ " WHERE v BETWEEN ? AND ?"
+ " ORDER BY v"
+ " FOR UPDATE";
try (PreparedStatement stmt =
con.prepareStatement(sql)) {
stmt.setInt(1, rnd.nextInt(10));
stmt.setInt(2, rnd.nextInt(10));
stmt.executeUpdate();
counter.incrementAndGet();
con.commit();
}
}
})),
counter
);
}))
.collect(Collectors.toList());
// Starting each thread
list.forEach(tuple -> tuple.v1.start());
// Printing execution counts
for (;;) {
list.forEach(tuple -> {
System.out.print(String.format(
"%1s:%2$-10s",
tuple.v1.getName(),
tuple.v2.get()
));
});
System.out.println();
Thread.sleep(1000);
}
Когда программа запускается, вы можете видеть, что она продолжается постепенно, причем каждый поток принимает примерно ту же нагрузку, что и другие потоки:
Thread-1:0 Thread-2:0 Thread-3:0 Thread-4:0
Thread-1:941 Thread-2:966 Thread-3:978 Thread-4:979
Thread-1:2215 Thread-2:2206 Thread-3:2244 Thread-4:2253
Thread-1:3422 Thread-2:3400 Thread-3:3466 Thread-4:3418
Thread-1:4756 Thread-2:4720 Thread-3:4855 Thread-4:4847
Thread-1:6095 Thread-2:5987 Thread-3:6250 Thread-4:6173
Thread-1:7537 Thread-2:7377 Thread-3:7644 Thread-4:7503
Thread-1:9122 Thread-2:8884 Thread-3:9176 Thread-4:9155
Теперь, ради аргумента, давайте сделаем запретную вещь иORDER BY DBMS_RANDOM.VALUE
String sql =
" SELECT *"
+ " FROM locks"
+ " WHERE v BETWEEN ? AND ?"
+ " ORDER BY DBMS_RANDOM.VALUE"
+ " FOR UPDATE";
Это не займет много времени, и ваше приложение взорвется:
Thread-1:0 Thread-2:0 Thread-3:0 Thread-4:0
Thread-1:72 Thread-2:79 Thread-3:79 Thread-4:90
Thread-1:72 Thread-2:79 Thread-3:79 Thread-4:90
Thread-1:72 Thread-2:79 Thread-3:79 Thread-4:90
Exception in thread "Thread-3" org.jooq.lambda.UncheckedException:
java.sql.SQLException: ORA-00060: deadlock detected while waiting for resource
Thread-1:72 Thread-2:79 Thread-3:79 Thread-4:93
Thread-1:72 Thread-2:79 Thread-3:79 Thread-4:93
Thread-1:72 Thread-2:79 Thread-3:79 Thread-4:93
Exception in thread "Thread-1" org.jooq.lambda.UncheckedException:
java.sql.SQLException: ORA-00060: deadlock detected while waiting for resource
Thread-1:72 Thread-2:1268 Thread-3:79 Thread-4:1330
Thread-1:72 Thread-2:3332 Thread-3:79 Thread-4:3455
Thread-1:72 Thread-2:5691 Thread-3:79 Thread-4:5841
Thread-1:72 Thread-2:8663 Thread-3:79 Thread-4:8811
Thread-1:72 Thread-2:11307 Thread-3:79 Thread-4:11426
Thread-1:72 Thread-2:12231 Thread-3:79 Thread-4:12348
Thread-1:72 Thread-2:12231 Thread-3:79 Thread-4:12348
Thread-1:72 Thread-2:12231 Thread-3:79 Thread-4:12348
Exception in thread "Thread-4" org.jooq.lambda.UncheckedException:
java.sql.SQLException: ORA-00060: deadlock detected while waiting for resource
Thread-1:72 Thread-2:13888 Thread-3:79 Thread-4:12348
Thread-1:72 Thread-2:17037 Thread-3:79 Thread-4:12348
Thread-1:72 Thread-2:20234 Thread-3:79 Thread-4:12348
Thread-1:72 Thread-2:23495 Thread-3:79 Thread-4:12348
И, наконец, все ваши потоки, кроме одного, были уничтожены (по крайней мере, в нашем примере) из-за исключений взаимоблокировок.
Остерегайтесь раздоров, хотя
Приведенные выше примеры также были впечатляющими с точки зрения отображения других негативных побочных эффектов пессимистической блокировки (или блокировки в целом): Конфликт. Единственный поток, который продолжал выполняться в «плохом примере», был почти таким же быстрым, как и четыре потока ранее. Наш глупый пример, в котором мы использовали случайные диапазоны блокировок, привел к тому, что в среднем почти каждая попытка получения блокировок имела, по крайней мере, некоторую блокировку . Как вы можете понять это? Посмотрев на enq: TX — конфликтные события блокировки строк в ваших сессиях. Например:
SELECT blocking_session, event
FROM v$session
WHERE username = 'TEST'
Приведенный выше запрос возвращает катастрофический результат, здесь:
BLOCKING_SESSION EVENT
-------------------------------------
48 enq: TX - row lock contention
54 enq: TX - row lock contention
11 enq: TX - row lock contention
11 enq: TX - row lock contention
Вывод
Вывод может быть только следующим: используйте пессимистическую блокировку экономно и всегда ожидайте неожиданного . При выполнении пессимистической блокировки, как взаимоблокировки, так и сильные конфликты — вполне возможные проблемы, с которыми вы можете столкнуться. Как правило, следуйте этим правилам (по порядку):
- Избегайте пессимистичной блокировки, если можете
- Избегайте блокировки более одной строки за сеанс, если можете
- Избегайте блокировки строк в случайном порядке, если можете
- Избегайте ходить на работу, чтобы увидеть, что случилось