Иногда вы просто не можете избежать этого: пессимистическая блокировка через SQL. На самом деле, это потрясающий инструмент, когда вы хотите синхронизировать несколько приложений с помощью общей глобальной блокировки.
Некоторые могут подумать, что это злоупотребляет базой данных. Мы думаем использовать инструменты, которые у вас есть, если они могут решить вашу проблему. Например, СУБД может быть идеальной реализацией для очереди сообщений .
Предположим, у вас есть сценарий использования пессимистической блокировки и вы хотите выбрать СУБД. Теперь, как сделать это правильно? Потому что действительно легко завести тупик. Представьте себе следующую настройку (для этого я использую Oracle):
|
1
2
3
4
5
6
|
CREATE TABLE locks (v NUMBER(18));INSERT INTO locksSELECT levelFROM dualCONNECT 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 countersList<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 threadlist.forEach(tuple -> tuple.v1.start());// Printing execution countsfor (;;) { 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:0Thread-1:941 Thread-2:966 Thread-3:978 Thread-4:979Thread-1:2215 Thread-2:2206 Thread-3:2244 Thread-4:2253Thread-1:3422 Thread-2:3400 Thread-3:3466 Thread-4:3418Thread-1:4756 Thread-2:4720 Thread-3:4855 Thread-4:4847Thread-1:6095 Thread-2:5987 Thread-3:6250 Thread-4:6173Thread-1:7537 Thread-2:7377 Thread-3:7644 Thread-4:7503Thread-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 resourceThread-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 resourceThread-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 resourceThread-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, eventFROM v$sessionWHERE username = 'TEST' |
Приведенный выше запрос возвращает катастрофический результат, здесь:
|
1
2
3
4
5
6
|
BLOCKING_SESSION EVENT-------------------------------------48 enq: TX - row lock contention54 enq: TX - row lock contention11 enq: TX - row lock contention11 enq: TX - row lock contention |
Вывод
Вывод может быть только следующим: используйте пессимистическую блокировку экономно и всегда ожидайте неожиданного . При выполнении пессимистической блокировки, как взаимоблокировки, так и сильные конфликты — вполне возможные проблемы, с которыми вы можете столкнуться. Как правило, следуйте этим правилам (по порядку):
- Избегайте пессимистичной блокировки, если можете
- Избегайте блокировки более одной строки за сеанс, если можете
- Избегайте блокировки строк в случайном порядке, если можете
- Избегайте ходить на работу, чтобы увидеть, что случилось
| Ссылка: | Как избежать страшной мертвой блокировки при пессимистической блокировке — и немного удивительного использования Java 8! от нашего партнера JCG Лукаса Эдера в блоге JAVA, SQL и JOOQ . |