Статьи

Как избежать страшной мертвой блокировки при пессимистической блокировке — и немного удивительного использования Java 8!

Иногда вы просто не можете избежать этого: пессимистическая блокировка через 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 *
  FROM locks
  WHERE v = 1
  FOR UPDATE;
 
         V
----------
         1

Экземпляр 2

1
2
3
4
5
6
7
8
SQL> SELECT *
  FROM locks
  WHERE v = 2
  FOR UPDATE;
 
         V
----------
         2

Теперь мы получили две разные блокировки из двух разных сессий.

А потом, давайте обратные вещи:

Экземпляр 1

1
2
3
4
SQL> SELECT *
  FROM locks
  WHERE v = 2
  FOR UPDATE;

Экземпляр 2

1
2
3
4
SQL> SELECT *
  FROM locks
  WHERE v = 1
  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

Вывод

Вывод может быть только следующим: используйте пессимистическую блокировку экономно и всегда ожидайте неожиданного . При выполнении пессимистической блокировки, как взаимоблокировки, так и сильные конфликты — вполне возможные проблемы, с которыми вы можете столкнуться. Как правило, следуйте этим правилам (по порядку):

  • Избегайте пессимистичной блокировки, если можете
  • Избегайте блокировки более одной строки за сеанс, если можете
  • Избегайте блокировки строк в случайном порядке, если можете
  • Избегайте ходить на работу, чтобы увидеть, что случилось