Иногда вы просто не можете избежать этого: пессимистическая блокировка через SQL. На самом деле, это потрясающий инструмент, когда вы хотите синхронизировать несколько приложений с помощью общей глобальной блокировки.
Некоторые могут подумать, что это злоупотребляет базой данных. Мы думаем использовать инструменты, которые у вас есть, если они могут решить вашу проблему. Например, СУБД может быть идеальной реализацией для очереди сообщений .
Предположим, у вас есть сценарий использования пессимистической блокировки и вы хотите выбрать СУБД. Теперь, как сделать это правильно? Потому что действительно легко завести тупик. Представьте себе следующую настройку (для этого я использую Oracle):
1
2
3
4
5
6
|
CREATE TABLE locks (v NUMBER(18)); INSERT INTO locks SELECT level FROM dual CONNECT BY level <= 10; |
Это создает 10 записей, которые мы будем использовать как 10 различных блокировок на уровне строк.
Теперь давайте подключимся к базе данных из двух клиентов sqlplus:
Экземпляр 1
1
2
3
4
5
6
7
8
|
SQL> SELECT * 2 FROM locks 3 WHERE v = 1 4 FOR UPDATE ; V ---------- 1 |
Экземпляр 2
1
2
3
4
5
6
7
8
|
SQL> SELECT * 2 FROM locks 3 WHERE v = 2 4 FOR UPDATE ; V ---------- 2 |
Теперь мы получили две разные блокировки из двух разных сессий.
А потом, давайте обратные вещи:
Экземпляр 1
1
2
3
4
|
SQL> SELECT * 2 FROM locks 3 WHERE v = 2 4 FOR UPDATE ; |
Экземпляр 2
1
2
3
4
|
SQL> SELECT * 2 FROM locks 3 WHERE v = 1 4 FOR UPDATE ; |
Оба сеанса теперь заблокированы, и, к счастью, Oracle обнаружит это и провалит один из сеансов:
1
|
ORA-00060: deadlock detected while waiting for resource |
Как избежать тупиков
Это очень явный пример, когда легко понять, почему это происходит, и, возможно, как этого избежать. Простой способ избежать взаимоблокировок — это установить правило, что все блокировки всегда должны быть получены в порядке возрастания. Если вы знаете, что вам нужны блокировки № 1 и 2, вы должны приобрести их в этом порядке. Таким образом, вы все равно будете производить блокировку и, следовательно, конфликт, но, по крайней мере, конфликт, в конце концов (вероятно) будет разрешен после уменьшения нагрузки. Вот пример, который показывает, что происходит, когда у вас больше клиентов. На этот раз написано как потоки Java.
В этом примере мы используем jOOλ для более простых лямбда-выражений (например, лямбда-выражения, выбрасывающие проверенные исключения). И, конечно же, мы будем сильно злоупотреблять Java 8!
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
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); } |
Когда программа запускается, вы можете видеть, что она продолжается постепенно, причем каждый поток принимает примерно ту же нагрузку, что и другие потоки:
1
2
3
4
5
6
7
8
|
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
1
2
3
4
5
6
|
String sql = " SELECT *" + " FROM locks" + " WHERE v BETWEEN ? AND ?" + " ORDER BY DBMS_RANDOM.VALUE" + " FOR UPDATE" ; |
Это не займет много времени, и ваше приложение взорвется:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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 — конфликтные события блокировки строк в ваших сессиях. Например:
1
2
3
|
SELECT blocking_session, event FROM v$session WHERE username = 'TEST' |
Приведенный выше запрос возвращает катастрофический результат, здесь:
1
2
3
4
5
6
|
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 |
Вывод
Вывод может быть только следующим: используйте пессимистическую блокировку экономно и всегда ожидайте неожиданного . При выполнении пессимистической блокировки, как взаимоблокировки, так и сильные конфликты — вполне возможные проблемы, с которыми вы можете столкнуться. Как правило, следуйте этим правилам (по порядку):
- Избегайте пессимистичной блокировки, если можете
- Избегайте блокировки более одной строки за сеанс, если можете
- Избегайте блокировки строк в случайном порядке, если можете
- Избегайте ходить на работу, чтобы увидеть, что случилось
Ссылка: | Как избежать страшной мертвой блокировки при пессимистической блокировке — и немного удивительного использования Java 8! от нашего партнера JCG Лукаса Эдера в блоге JAVA, SQL и JOOQ . |