Статьи

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

Иногда вы просто не можете избежать этого: пессимистическая блокировка через 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

Вывод

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

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