Распределенная блокировка в Java Spring Boot

У меня есть 2 экземпляра контейнеров с запланированным заданием, и я хотел бы защититься от запуска одной и той же задачи в каждом контейнере докеров. Я решил сделать распределенную блокировку с помощью SQL Server.

я добавил конфигурацию

  @Bean
    public DefaultLockRepository DefaultLockRepository(DataSource dataSource) {
        return new DefaultLockRepository(dataSource);
    }

    @Bean
    public JdbcLockRegistry jdbcLockRegistry(LockRepository lockRepository) {
        return new JdbcLockRegistry(lockRepository);
    }

И я пытаюсь получить блокировку следующим образом:

   public void doSomething() throws InterruptedException {
        boolean lockAcquired;
        var lock = lockRegistry.obtain("main");

        try {
            
            lockAcquired = lock.tryLock();
            if (lockAcquired == true) {
                logger.info("Locked");
            } else {
                logger.info("Not locked");
            }

        } catch (Exception ex) {
            lockAcquired = false;
            logger.info("Not locked");
        }

        Thread.sleep(3600000);

        lock.unlock();
    }

Я запустил 2 экземпляра приложения с одинаковым кодом. Когда я вошел в метод первого экземпляра приложения, была получена блокировка - и я получил сообщение "Заблокировано" - так что все в порядке.

В базе данных я увидел такую ​​запись:

LOCK_KEY    REGION  CLIENT_ID   CREATED_DATE
fad58de7-3664-35db-8650-cfefac2fcd61    DEFAULT c0314b87-0bee-4bdd-8567-a939fd21f0bf    2024-03-16 13:20:43.2559575I saw that in datatabase I've got new entry like this:

Когда я запустил второй экземпляр приложения, я ожидал получить сообщение «Не заблокировано», поскольку блокировка была получена в первом приложении, которое все еще работает, но нет - у меня снова есть журнал «Заблокировано», и моя запись в базе данных была обновлена ​​до:

LOCK_KEY    REGION  CLIENT_ID   CREATED_DATE
fad58de7-3664-35db-8650-cfefac2fcd61    DEFAULT ad58971d-c991-4709-a03b-9acb39a38c54    2024-03-16 13:24:42.3002365

Итак, моя распределенная блокировка не работает.

Что я делаю не так? Как я могу защититься от выполнения запланированной задачи с помощью распределенной блокировки во многих контейнерах докеров.

//ОБНОВЛЯТЬ

Когда я выполняю метод в двух экземплярах за короткий промежуток времени (например, менее 5 с), он работает, но когда я выполняю метод в первом экземпляре и пытаюсь выполнить его во втором экземпляре через 30 с, это не работает.

//ОБНОВЛЕНИЕ2

Как предположил Алекс, это была проблема с TTL в DefaultLockRepository. Блокировка была установлена ​​на время по умолчанию 10 секунд. После изменения кода следующим образом:

 @Bean
    public DefaultLockRepository DefaultLockRepository(DataSource dataSource) {
        var repo = new DefaultLockRepository(dataSource);
        repo.setTimeToLive(1000 * 100);
        return repo;
    }

Для блокировки установлено значение 100 секунд, и в течение следующих 100 секунд другой экземпляр службы не сможет заблокировать метод.

пожалуйста, проверьте, пытается ли lock.tryLock() вернуть true в обоих случаях.

devReddit 16.03.2024 14:56

Вам нужно проверить, что возвращает tryLock. docs.oracle.com/en/java/javase/17/docs/api/java.base/java/ut‌​il/…

D-Dᴙum 16.03.2024 15:00

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

santal 16.03.2024 16:30

@santal, когда ты говоришь, что использование большего временного интервала не работает, что происходит? Также ваш пример никогда не снимает блокировку?

D-Dᴙum 16.03.2024 17:31

Я думаю, вам следует изменить TTL в DefaultLockRepository, используя метод setTimeToLive, потому что его значение по умолчанию составляет 10 секунд.

Alex 16.03.2024 17:41

@ Алекс, да, это решение! После изменения TTL в DefaultLockRepository все работает! Можете ли вы опубликовать свой комментарий в качестве ответа на вопрос?

santal 16.03.2024 17:55

Если это проблема взаимоблокировки, это предполагает, что логика вашего приложения может быть причиной проблемы.

D-Dᴙum 16.03.2024 18:03

Я не буду публиковать это как ответ, потому что использовать spring lock support в вашем случае — не лучшая идея. Если вам просто нужно предотвратить дублирование сервисов, достаточно использовать собственный общий lock в БД, проверяя его наличие. Я ответил, почему ваш код не работает во временных случаях

Alex 16.03.2024 18:17
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
8
355
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я успешно использовал ShedLock. Добавление следующего в pom.xml (или соответствующий градиент)

        <dependency>
            <groupId>net.javacrumbs.shedlock</groupId>
            <artifactId>shedlock-spring</artifactId>
            <version>${shedlock.version}</version>
        </dependency>

Затем добавьте следующее в основной класс или класс конфигурации (пример в Kotlin, но та же концепция для Java):

@EnableSchedulerLock(defaultLockAtMostFor = "10m")
class MainClass //...

    @Bean
    fun lockProvider(dataSource: DataSource): LockProvider {
        return JdbcTemplateLockProvider(dataSource)
    }
//...

Затем добавьте к методу следующую аннотацию:

@Service
class NeedingLockService //...

@Scheduled(fixedDelay = 5000) // using it at a schedule interval in my case but not relevant for lock
@SchedulerLock(name = "nameOfTheLock")
fun dispatch() { // name doesn't matter
//...
}

В этом примере Kotlin сервис будет запускаться каждые 5 секунд, но только в одном контейнере/поде, если у вас их несколько.

Примечания:

  1. Нет никаких гарантий, что он будет работать на конкретном контейнере/поде, хотя обычно это так.
  2. Источник данных, очевидно, должен быть одинаковым во всех случаях, чтобы он работал.
  3. Вам необходимо создать необходимую таблицу. Вот файл Liquibase для него:
databaseChangeLog:
  - changeSet:
      id: 5
      author: Geoffrey
      changes:
        - createTable:
            tableName: shedlock
            columns:
              - column:
                  name: name
                  type: varchar(64)
                  constraints:
                    primaryKey: true
                    nullable: false
              - column:
                  name: lock_until
                  type: timestamp
                  constraints:
                    nullable: false
              - column:
                  name: locked_at
                  type: timestamp
                  defaultValueComputed: CURRENT_TIMESTAMP
                  constraints:
                    nullable: false
              - column:
                  name: locked_by
                  type: varchar(255)
                  constraints:
                    nullable: false

Другие вопросы по теме