Песимістичне блокування (шаблон проєктування)

Матеріал з Вікіпедії — вільної енциклопедії.
Перейти до навігації Перейти до пошуку

Песимістичне блокування (англ. Pessimistic Offline Lock) — шаблон проєктування, який запобігає конфлікту між бізнес-транзакціями, що конкурують надаючи доступ до даних в момент часу тільки одній транзакції.

Опис[ред. | ред. код]

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

Даний шаблон розв'язує цю проблему, надаючи доступ до даних в момент часу тільки одній транзакції. Таким чином перша транзакція блокує ресурс з яким вона працює, тому друга очікує коли дані звільняться.

Даний шаблон варто використовувати тоді коли шанси на конфлікт високі (доступ до одних і тих самих даних різними користувачами відбувається часто), або ж тоді коли є суворі вимоги в дотримані консистенції даних.

Алгоритм[ред. | ред. код]

  • Клієнт 1 отримує запис під номером 19 та накладає блокування на даний ресурс.
  • Клієнт 2 намагається отримати запис під номером 19 та оскільки він заблокований, користувач отримує помилку. Таким чином клієнт отримує помилку не по завершені транзакції, як це реалізовану в оптимістичному блокуванні, а заздалегідь.
  • Клієнт 1 зберігає свої зміни в сховищі та забирає блокування. Ресурс знову доступний для використання іншими клієнтами.

Типи блокування[ред. | ред. код]

Існує декілька типів блокування ресурсу.

Перший можливий тип — це ексклюзивне блокування запису (exclusive write lock). Блокування накладається лише на редагування даних. Це дозволяє запобігти конфліктів коли дві бізнес-транзакції одночасно змінюють один і той самий ресурс. Дане блокування дозволяє операцію читання, тому підходить якщо паралельний сеанс дозволяє читання застарілих даних.

Якщо бізнес-транзакції завжди потрібні актуальні дані, не зважаючи на те чи збирається вона їх змінювати чи ні варто використовувати ексклюзивне блокування читання (exclusive read lock). Такий підхід обмежує можливості паралельного доступу до ресурсу.

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

  • Можливо накласти лише одне блокування запису.
  • Можливо накласти декілька блокувань читання.
  • Блокування читання і запису являються несумісними операціями. Ресурс не можна заблокувати для запису, якщо він вже був заблокований для читання. Вірно і зворотне твердження. Ресурс не можна заблокувати для читання, якщо він вже був заблокований для запису

Таким чином, якщо на ресурс накладено блокування читання, бізнес-транзакції не можуть його редагувати, допоки всі блокування цього типу не будуть зняті. Але читання іншими процесами цього запису не спричиняє конфліктів.

Реалізація[ред. | ред. код]

При реалізації нам необхідно перед початком роботи заблокувати ресурс. Напишемо клас, який за це відповідатиме. Даний сервіс має бути одинаком для системи. Так, наприклад, якщо відомо, що розгортається лише один екземпляр аплікації записи про заблоковані ресурси можна записувати у статичний сегмент. Якщо існує декілька аплікацій, які доступаються до ресурсу цього буде недостатньо. В такому разі варто використовувати глобальний реєстр. Також варто врахувати, що під час роботи аплікації можливі збої і ресурс не буде звільнений ніколи. Тоді варто попіклуватись, про очищення ресурсів автоматично, чи за таймером.

Напишемо спрощену реалізацію класу блокування.

public class InMemoryLockService
{
	private readonly object _lockObject = new object();
	private readonly ISet<string> _lockedObjectSet = new HashSet<string>();
	
	public void AcquireLock(string recordId)
	{
		lock(_lockObject)
		{
			if (_lockedObjectSet.Contains(recordId))
			{
				throw new InvalidOperationException("Record is blocked");
			}
			
			_lockedObjectSet.Add(recordId);
		}
	}
	
	public void ReleaseLock(string recordId)
	{
		lock(_lockObject)
		{			
			_lockedObjectSet.Remove(recordId);
		}
	}
}

Тоді перед початком роботи та по завершені нам необхідно блокувати доступ до ресурсу.

public void DiscardFromUserBalance(int userId, double amount)
{
    _lockService.AcquireLock(userId.ToString());

    User user = db.GetUser(userId);
    
    if (user.Balance > amount)
    {
        user.Balance -= amount;
    }
    else
    {
        throw new InvalidOperationException("Discard amount can not be larger than user balance");
    }
    
    db.Update(user);

    _lockService.ReleaseLock(userId.ToString());
}

Інший варіант блокування передбачає додавання спеціальної мітки на сутність:

public class User
{
    public int Id { get; set; }
    public LockStatus LockStatus { get; set; }
}

public enum LockStatus 
{
    NoLock,
    ReadLock,
    WriteLock,
}

Див. також[ред. | ред. код]

Джерела[ред. | ред. код]