Как вы это сделаете: Эффективный и оптимизированный способ отслеживания непрочитанных сообщений для каждого пользователя

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

Поэтому, проведя диагностику, мы пришли к 3 решениям, которые мы можем реализовать
Убрать сложный пункт where или оптимизировать таблицу, из которой берется информация для чтения, или и то, и другое, вам должно быть интересно, насколько сложным является этот пункт where, он выглядел примерно так

WHERE receiver ->'$.permission' = ${permissionValue}
      AND (CASE
        WHEN receiver->'$.userType' = ${userType}
        THEN receiver->'$.sendOfficeUser' = true
        WHEN receiver->'$.moduleType' = 'reviewApproval'
        THEN JSON_UNQUOTE(receiver->'$.pLevel') IN ('${permList.review.join("','")}')
        WHEN receiver->'$.moduleType' = 'actions'
        THEN JSON_UNQUOTE(receiver->'$.pLevel') IN ('${permList.action.join("','")}')
        WHEN receiver->'$.moduleType' = ''
        THEN JSON_UNQUOTE(receiver->'$.pLevel') = ''
        ELSE true
        END)
      AND (CASE
        WHEN receiver->'$.grant' IS NOT NULL
        THEN receiver->'$.grant' = '${grant}'
        ELSE true
        END)`
Войти в полноэкранный режим Выйти из полноэкранного режима
  1. Изменить текущую реализацию того, как информация хранится в NotificationRead — таблице, используемой для отслеживания всех статусов чтения.

  2. Отказаться от таблицы NotificationRead вообще и, следовательно, от запроса Join также и ввести два новых столбца, Owner и read, которые будут содержать массив идентификаторов пользователей, этот метод сократит не только join, но и сложное предложение WHERE, которое полагалось на JSON столбец reciever, как показано выше.

  3. И, наконец, гибрид двух предыдущих решений, у нас будет колонка owner для распознавания того, кто может ее видеть, но информация о чтении будет запрашиваться из таблицы NotificationRead.

Я подробно расскажу о первой реализации, а оставшиеся две, которые являются простыми, будут описаны вкратце.

Первая реализация

Колонки

  • идентификатор уведомления

    • Тип данных — bigint
    • Внешний ключ Ограничение — Notification.notification_id
    • Составной ключ
  • пользователь_id

    • Тип данных — varchar(50)
    • Внешний ключ Constraint — Users.username
    • Составной ключ
  • mark_all

    • Тип данных — bit(1)
    • Может быть Null
  • last_id_marked

    • Тип данных — bigint
    • Внешний ключ Ограничение — Notification.notification_id
    • Может быть Null

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

Составные ключи в этой таблице просты: _notification_id_ — прочитанное уведомление и _user_id_ — пользователь, который его прочитал.

_mark_all_ будет использоваться как своеобразный флаг, показывающий, что уведомление было отмечено всем, начиная с этого Id и далее, то есть, допустим, пользователь с именем пользователя ‘anoop’ нажимает mark all, и, допустим, последнее уведомление _notification_id_ равно 800.
Запись NotificationRead будет выглядеть следующим образом

{
"NotificationRead": [
    {
        "notification_id" : 800,
        "user_id" : "anoop",
        "mark_all" : 1,
        "last_id_marked" : null
    }
]}
Вход в полноэкранный режим Выход из полноэкранного режима

Это означает следующее:

  • 1. что при запросе таблицы Notification мы будем считать, что все уведомления ниже 800 прочитаны пользователем, это возможно, потому что _notification_id _ в таблице Notification имеет автоинкремент.
  • 2. Это означает, что нам нужно будет хранить только одну запись в случае, когда выполняется отметка всех, вместо того, чтобы хранить все записи

last_read_id (возможно, это не совсем точное название) будет использоваться для хранения last_id в последовательном списке id, помеченных как прочитанные, позвольте мне объяснить.
если пользователи пометят набор последовательных отметок, похожих на флажок в Gmail, который показывает 50 за раз, скажем, notification_id’s 851 to 801 и попытаются пометить их как прочитанные, наша база данных не будет хранить 50 записей, а только одну запись. и это будет выглядеть так

{
"NotificationRead": [
    {
        "notification_id" : 851,
        "user_id" : "anoop",
        "mark_all" : null,
        "last_id_marked" : 801
    }
]}
Вход в полноэкранный режим Выйти из полноэкранного режима

Что это значит:

  • что при запросе таблицы Notification мы будем считать, что все уведомления между 851 и 801 прочитаны пользователем.
  • Это означает, что нам нужно будет хранить только одну запись вместо 50.

Запрос

Давайте посмотрим на пример запроса, полученного из этого шаблона данных, я сделал демонстрационную таблицу и протестировал, и он работает нормально

SELECT n.notification_id , (CASE WHEN nr.notification_id IS NOT NULL THEN true ELSE false END) AS hasRead
FROM Notification n
LEFT JOIN NotificationRead nr ON nr.user_id = 'anoop' AND (nr.mark_all = 1 AND nr.notification_id >= n.notification_id) 
OR (n.notification_id = nr.notification_id) 
OR (nr.last_id_marked IS NOT NULL AND n.notification_id BETWEEN nr.last_id_marked AND nr.notification_id) 
WHERE {condition}
Вход в полноэкранный режим Выйти из полноэкранного режима

И теперь мы переходим к последнему кусочку головоломки (на самом деле первому)…

Вставка данных

  • Запросите все идентификаторы уведомлений, применимые к пользователю с текущим статусом чтения, как показано в примере запроса в предыдущем разделе

  • отобразите результат и обновите статус чтения для предоставленного FE идентификатора (идентификаторов) на true, пока что все просто.

const newlyReadData = currentReadData.map((currentRead) => {
    return {
             ...currentRead,
             isRead: currentRead.isRead ? true: ids.includes(currentRead.id)
           }
})
Вход в полноэкранный режим Выйти из полноэкранного режима
  • Теперь начинается основная часть логики вставки данных. Мы уменьшим полученный массив после отображения и разобьем его на новый массив всех идентификаторов, которые имеют статус isRead true, разбивка массива массивов (мы можем назвать это группировкой, пакетной обработкой и т.д.) будет выглядеть следующим образом:

  • Допустим, у пользователя было 10 уведомлений 1-10, и результирующая карта массива isRead выглядит так = 1,2,3,5,8,9, уменьшенный массив будет выглядеть так [[1,2,3],[5],[8,9] мы будем группировать все последовательные уведомления о прочтении в один массив, как вы уже поняли из примера ниже. чтобы продемонстрировать дальше, давайте рассмотрим другой пример

  • тот же сценарий, что и в предыдущем примере, но количество прочтений 1,2,5,6,7,8,9,10, то пакетный массив будет выглядеть как [[1,2],[5,6,7,8,9,10]]

const batchingNotificationsRead = (notification) => {
    const batchArray = []
    let individualBatch = []
    for (const [index,notified] of notification.entries()) {
        if (notified.isRead){
            individualBatch.push(notified.id)
        }
        else if (!notified.isRead && individualBatch.length > 0) {
            batchArray.push(individualBatch)
            individualBatch = []
        }
        if (index === notification.length -1 && individualBatch.length > 0){
            batchArray.push(individualBatch)
            individualBatch = []
        }
    }
    return batchArray
}
Вход в полноэкранный режим Выход из полноэкранного режима

наконец, допустим, у нас есть все 10, как прочитанный пакетный массив будет выглядеть как [[1,2,3,4,5,6,7,8,9,10]] .
Теперь из массива batch подготовим данные для вставки в данные для batching [[1,2,3],[5],[8,9] у нас будет три записи в DB следующим образом

[

{   
    "notification_id" : 1,
    "user_id" : "anoop",
    "mark_all" : null,
    "last_id_marked" : 3
},
{  
     "notification_id" : 5,
    "user_id" : "anoop",
    "mark_all" : null,
    "last_id_marked" : null
},
{
    "notification_id" : 8,
    "user_id" : "anoop",
    "mark_all" : null,
    "last_id_marked" : 9
}
]
Вход в полноэкранный режим Выйти из полноэкранного режима

для пакетной обработки [[1,2],[5,6,7,8,9,10] мы будем иметь две записи в БД следующим образом

[

{   
    "notification_id" : 1,
    "user_id" : "anoop",
    "mark_all" : null,
    "last_id_marked" : 2
},
{  
     "notification_id" : 5,
    "user_id" : "anoop",
    "mark_all" : 1,
    "last_id_marked" : null
}
]
Вход в полноэкранный режим Выйти из полноэкранного режима

для пакетной обработки [[1,2,3,4,5,6,7,8,9,10] мы будем иметь только одну запись в БД следующим образом

[
    {  
         "notification_id" : 1,
        "user_id" : "anoop",
        "mark_all" : 1,
        "last_id_marked" : null
    }
]
Войдите в полноэкранный режим Выйти из полноэкранного режима
const prepareNotificationReadForDb = (data ) => {
    let dbData = []
    for (let j = data.length - 1; j>=0; j--) {
        const batch = data[j]
        if (batch[batch.length - 1] === notification[notification.length - 1].id && batch.length > 1) {
            let insertData = {
                "notification_id" : data[j][0],
                "user_id" : "anoop",
                "mark_all" : 1,
                "last_id_marked" : null
            }
            dbData.push(insertData)
        }else if (batch.length > 1) {
            let insertData = {
                "notification_id" : batch[0],
                "user_id" : "anoop",
                "mark_all" : null,
                "last_id_marked" : batch[batch.length - 1]
            }
            dbData.push(insertData)
        }else {
            let insertData = {
                "notification_id" : data[j][0],
                "user_id" : "anoop",
                "mark_all" : null,
                "last_id_marked" : null
            }
            dbData.push(insertData)
        }
    }
    return dbData
}
Войти в полноэкранный режим Выход из полноэкранного режима

Последний шаг — удаление всех предыдущих записей под именем пользователя и вставка вновь сформированных записей, так как мы делаем удаление перед обновлением, мы должны сделать unitofworktransaction method, так как будет очень опасно удалить и мы не сможем обновить.

Это означает, что у нас будет максимум n записей для 2n-1 уведомлений пользователя (если пользователь читает все уведомления альтернативно), а если он нажимает отметить все, то уменьшается до 1 записи на пользователя, такое уменьшение записей в таблице ускоряет запрос.

Итак, теперь я хотел бы узнать, как бы вы это сделали, выбрали ли вы какой-либо из вариантов, упомянутых выше, или у вас есть собственное решение. Пожалуйста, поделитесь…

Оцените статью
devanswers.ru
Добавить комментарий