синхронизировано по ключу карты

#java #multithreading #kotlin #telegram #synchronized

Вопрос:

Я использую следующий метод в коде моего телеграмм-бота:

 fun save(tUser: org.telegram.telegrambots.meta.api.objects.User): User {
        val userId = tUser.id;
        synchronized(this) {
            val user = userRepository.findById(userId).orElse(User(userId))
            user.apply {
                username = tUser.userName
                firstName = tUser.firstName
                lastName = tUser.lastName
                languageCode = tUser.languageCode
                lastMessageReceived = Instant.now()
            }
            return userRepository.save(user)
        }
    }
 

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

Таким образом, поиск и вставка в базу данных пользователей выполняется строго в одном потоке. Но нет необходимости синхронизировать весь объект контроллера(это) (обработка разных пользователей может идти параллельно), т. Е. мне нужно синхронизировать, скажем, по ключу ConcurrentHashMap, например, так:

     private val users: ConcurrentHashMap<Long, Long> = ConcurrentHashMap()

    fun save(tUser: org.telegram.telegrambots.meta.api.objects.User): User {
        val userId = users.putIfAbsent(tUser.id, tUser.id)!!
        synchronized(userId) {
            val user = userRepository.findById(userId).orElse(User(userId))
            user.apply {
                username = tUser.userName
                firstName = tUser.firstName
                lastName = tUser.lastName
                languageCode = tUser.languageCode
                lastMessageReceived = Instant.now()
            }
            return userRepository.save(user)
        }
    }
 

было бы это правильно?

Здесь я вижу еще одну проблему — потоки замедляются на ConcurrentHashMap, хотя и не так сильно, как в первом случае, когда доступ к БД осуществляется внутри синхронизированного блока(это)

Комментарии:

1. Я считаю, что синхронизация на Long самом деле не очень хорошая идея. Вы можете использовать ту же технику, но хранить ReentrantLock объекты на карте (или просто Any() ) и синхронизировать их. Недостатком является то, что карта будет расти со временем, и удалить неиспользуемые блокировки нетривиально.

2. Какова фактическая реализация userRepository ? Любая разумная реляционная БД не будет иметь проблем с одновременным выбором и обеспечит точный контроль над тем, что происходит при вставках и обновлениях

3. @DavidSoroko, я использую postgres. Если я не использую блокировку: Может возникнуть ситуация, когда один и тот же пользователь (отсутствующий в базе данных) входит в два потока. И тогда первый запрос будет выполнен успешно, но второй завершится сбоем с нарушением первичного ключа

4. Синхронизация на стороне сервера не решит проблему, если вы запустите несколько экземпляров. Вы могли бы рассмотреть вопрос О КОНФЛИКТЕ «НИЧЕГО не ДЕЛАЙ» в качестве альтернативы: postgresql.org/docs/9.5/sql-insert.html

5. или просто проигнорируйте/проглотите уникальное исключение нарушения ограничений при вставке в ваш код. Это почти одно и то же.