Ніколи більше не надсилайте дубльовані електронні листи: ганьба, якої я уникнув, використовуючи блокування та READPAST для високопродуктивних email-сповіщень

У розподілених системах забезпечити високу пропускну здатність при збереженні консистентності може бути складно, особливо коли кілька інстансів програми мають обробляти один і той самий джерело даних. У цій статті я поділюсь тим, як я реалізував мікросервіс для надсилання email-сповіщень, використовуючи блокування та SQL-підказку READPAST для ефективного керування конкурентністю. Цей підхід гарантує, що жодне повідомлення не буде оброблено двічі, навіть у багатозадачному середовищі.

Кейс використання: маркетингові email-кампанії

Уявіть, що ви створюєте мікросервіс для надсилання маркетингових email-сповіщень. Процес включає:

  1. Споживання подій Kafka, згенерованих спеціальним мікросервісом, відповідальним за визначення користувачів, яких потрібно залучити до email-кампаній.
  2. Зберігання цих подій у таблиці бази даних (email_messages) із стовпцем date_to_be_sent для запланованих кампаній, наприклад, акцій до Хелловіну.
  3. Використання запланованого завдання для запиту повідомлень, які готові до надсилання.
  4. Надсилання email через Azure Email Communication Service за допомогою їх SDK.

Задача? Досягти високої пропускної здатності при гарантуванні, що жоден email не буде надісланий більше одного разу, навіть коли працюють кілька інстансів сервісу.

Рішення: блокування та READPAST

Для вирішення цього завдання я спроектував систему, яка використовує SQL-підказку READPAST у поєднанні з блокуваннями для керування конкурентністю.

  • Блокування: забезпечують, щоб записи, які обробляються одним інстансом, були недоступні для інших.
  • READPAST: пропускає заблоковані рядки під час запитів, дозволяючи іншим інстансам обробляти різні записи одночасно.

Це гарантує, що кілька інстансів сервісу можуть працювати одночасно, не перетинаючись на тих самих записах, забезпечуючи як масштабованість, так і консистентність.

Дизайн таблиці

Ось спрощена версія таблиці email_messages:

CREATE TABLE email_messages (  
 id BIGINT PRIMARY KEY,  
 user_id BIGINT NOT NULL,  
 email VARCHAR(255) NOT NULL,  
 date_to_be_sent TIMESTAMP NOT NULL,  
 status VARCHAR(50) DEFAULT 'PENDING',  
 locked_at DATETIME NULL  
);
  • id: Унікальний ідентифікатор для кожного email-повідомлення.
  • date_to_be_sent: Часова мітка, коли email має бути надісланий.
  • status: Стан email (PENDING, SENT тощо).
  • locked_at: Вказує, коли запис обробляється.

Запит: Безпечно отримуємо записи

Ось SQL-запит, що використовується для отримання записів для обробки:

WITH cte AS (  
 SELECT TOP 1000 id  
 FROM email_messages WITH (ROWLOCK, READPAST)  
 WHERE status = 'PENDING' AND date_to_be_sent <= GETUTCDATE()  
 ORDER BY date_to_be_sent ASC  
)  
UPDATE email_messages  
SET locked_at = GETUTCDATE(), status = 'PROCESSING'  
OUTPUT INSERTED.*  
FROM cte  
WHERE email_messages.id = cte.id;
  • ROWLOCK: Затримує блокування на рівні рядка, щоб інші транзакції не могли змінювати ці рядки.
  • READPAST: Пропускає рядки, заблоковані іншими транзакціями.
  • TOP 1000: Обмежує кількість записів, які обробляються в кожній партії для масштабованості.

Заплановане завдання та виконання на кількох інстансах

Кожен інстанс програми виконує заплановане завдання для виконання вищезгаданого запиту та отримання записів для обробки.
Оскільки READPAST пропускає заблоковані рядки, кожен інстанс працює з унікальним набором записів, що гарантує відсутність перекриттів.

Надсилання Email

Після отримання записів, сервіс надсилає email через Azure Email Communication SDK або будь-який інший відповідний інструмент для доставки email, залежно від конкретних вимог системи:

@Service  
public class EmailSenderService {  

 @Autowired  
 private AzureCommunicationEmailClient emailClient;  

 public void sendEmails(List emailMessages) {  
 for (EmailMessage message : emailMessages) {  
 try {  
 EmailMessage email = new EmailMessage()  
 .setTo(message.getEmail())  
 .setSubject("Your Campaign")  
 .setBody("Your personalized email content here");  

 emailClient.send(email);  

 // Update status to SENT  
 message.setStatus("SENT");  
 emailMessagesRepository.save(message);  
 } catch (Exception e) {  
 log.error("Failed to send email", e);  
 }  
 }  
 }  
}

Переваги цього підходу

  • Висока пропускна здатність: Кілька інстансів одночасно обробляють записи.
  • Консистентність: Блокування та READPAST запобігають дублюванню обробки.
  • Масштабованість: Система може обробляти велику кількість email, додаючи нові інстанси.

Виклики та отримані уроки

  • Взаємні блокування (Deadlocks): Потрібен уважний підхід до блокувань та дизайну запитів, щоб уникнути взаємних блокувань.
  • Моніторинг: Необхідно впровадити надійний моніторинг для відстеження заблокованих записів та спроб повторної обробки.

Висновок

Використання блокувань і READPAST — потужний спосіб забезпечити високу пропускну здатність і консистентність у розподілених системах. Цей підхід особливо ефективний для часових чутливих завдань із великим об'ємом, таких як email-сповіщення.

Чи використовували ви подібний підхід у своїх додатках? Поділіться думками та досвідом у коментарях нижче!

Перекладено з: Never Send a Duplicate Email Again: The Shameful Mistake I Avoided Using Locks and READPAST for High-Throughput Email Notifications

Leave a Reply

Your email address will not be published. Required fields are marked *