Конкурентный доступ к реляционным базам данных

СхемаВопросы параллелизма в компьютерных вычислениях очень сложны! Причинами большой сложности являются огромное количество деталей, которые нужно учитывать при разработке параллельных программ. В программирование и без того существует большое количество деталей, которые создают почву для ошибок, параллелизм же, добавляет ещё.

Вопросы конкурентного доступа к реляционным базам данных встают практически перед любыми разработчиками прикладного программного обеспечения и не только перед ними. Результатом такой востребованности этой области является наличие большого количества созданных архитектурных паттернов. Это позволяет успешно справляться с большой сложностью разработки таких программ. Ниже пойдёт речь о таких рецептах, а также механизмах на которых базируется их реализация. Повествование будет иллюстрироваться примерами кода на Java, но большинство материала не привязано к языку. Цель статьи — описать проблемы конкурентного доступа к реляционным базам данных, в качестве введения в предмет, а не полноценного охвата темы.

Проблемы при конкурентном доступе


Рассмотрим ситуацию. В некой учётной системе необходимо отражать изменение остатков товара при проведении документов. Чтобы обсуждение было более предметным, я буду снабжать его примерами работающими на PostgreSQL. Вот структура базы данных для нашей учётной системы и тестовые данные.

CREATE TABLE stocks (
  id integer PRIMARY KEY,
  name varchar(256) NOT NULL,
  quantity decimal(10,2) NOT NULL DEFAULT 0.0
);

CREATE TABLE documents (
  id integer PRIMARY KEY, 
  quantity decimal(10,2) NOT NULL DEFAULT 0.0, 
  processed boolean NOT NULL DEFAULT false,
  stock integer REFERENCES stocks
);

INSERT INTO stocks (id, name, quantity) VALUES 
(1, 'сыр', 56.4), 
(2, 'молоко', 26.8);

INSERT INTO documents (id, quantity, stock) VALUES 
(1, 15.6, 1), 
(2, 26.1, 1);

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

SELECT quantity, processed, stock FROM documents WHERE id = 1;

 quantity | processed | stock 
----------+-----------+-------
    15.60 | f         |     1

SELECT name, quantity FROM stocks WHERE id = 1;

 name | quantity 
------+----------
 сыр  |    56.40

Загружены данные документа с идентификатором 1 и остатка соответствующего ему. Вычислено новое значение остатка путём списания 56.40 — 15.60 = 40.80 и данные сохранены обратно вместе с пометкой об обработке документа.

UPDATE stocks SET quantity = 40.80 WHERE id = 1;
UPDATE documents SET processed = true WHERE id = 1;

Но однопользовательские системы это давнее прошлое. Сейчас невозможно представить бизнес-приложение для работы с которым пользователи должны выстраиваться в очередь. Поэтому давайте рассмотрим ситуацию когда обрабатываются два документа одновременно. Как и в первом случае приложения считывают данные из базы.

первое приложение

SELECT quantity, processed, stock FROM documents WHERE id = 1;

 quantity | processed | stock 
----------+-----------+-------
    15.60 | f         |     1

SELECT name, quantity FROM stocks WHERE id = 1;

 name | quantity 
------+----------
 сыр  |    56.40

второе приложение

SELECT quantity, processed, stock FROM documents WHERE id = 2;

 quantity | processed | stock 
----------+-----------+-------
    26.10 | f         |     1

SELECT name, quantity FROM stocks WHERE id = 1;

 name | quantity 
------+----------
 сыр  |    56.40

И тут возникает очевидная проблема: первое приложение вычисляет 56.40 — 15.60 = 40.80, а второе 56.40 — 26.10 = 30.30. И в базу запишется любой из этих результатов. Тот, для которого будет выполнен последний update запрос. Это явно не то поведение, которое ожидает увидеть пользователь. Проблемы такого вида хорошо известны в параллельном программирование и носят название состояния гонок (race conditions). Данный случай является составным действием под общим название прочитай-измени-запиши (read-modify-write).

В случае если бы мы проверяли документ на обработанность (поле processed), а в реальном приложении нам пришлось бы это делать, могла произойти ситуация, в которой, несмотря на проверку, документ мог быть обработан дважды. Всё тоже состояние гонок, но другой вид операции проверь-затем-действуй (check-then-act). К тому же в промежутках времени между update запросами база данных находится в несогласованном состоянии, то есть действие по документу уже выполнено, но документ не помечен как обработанный.

В результате мы имеем две наиболее общие проблемы, которые проявляются при некорректном конкурентном доступе к базам данных. Это потеря изменений, происходящих в случае ситуации гонки прочитай-измени-запиши. И несогласованное состояние данных в промежутке между update запросами.

Механизмы СУБД


Для того чтобы обсуждать конкретные рецепты конкурентного доступа к реляционным базам данных, необходимо быть знакомым с более низкоуровневыми механизмами, которые предоставляют СУБД и средства программирования. Это технические элементы на которых базируется реализация. Основными такими элементами являются транзакции и блокировки. О них и пойдёт речь далее.

Транзакции и уровни изоляции


Транзакция является единой последовательностью операций по взаимодействию с базой данных, которая воспринимается как одно целое, имеющей возможность на откат или подтверждения совершения операций. Транзакция может являться средством разрешения проблемы согласованности изменений, а также не допускать ситуации гонок. Пригодность транзакций для решения первой или обоих проблем зависит от уровня изоляции (isolation level). Изоляция — это свойство, которое определяет как/когда изменения сделанные одной операцией будут видны конкурентной операции.

Стандарт SQL определяет четыре уровня изоляции: Read uncommitted, Read committed, Repeatable read, Serializable. Все они отличаются минимально допустимым уровнем изоляции. Основные свойства этих уровней должны быть понятны из названия. Разные СУБД могут по разному реализовывать поддержку типов изоляции. Главное чтобы каждая реализация не допускала изоляцию, менее строгую, чем предписано для уровня. Например в PostgreSQL внутренне реализована поддержка только двух уровней изоляции Read committed и Serializable. Рассмотрим использование этих уровней изоляции для решение проблем описанных в примерах выше.

Использование уровня изоляции Read committed (используется по умолчанию в PostgreSQL) позволяет решить проблему несогласованного состояния данных. Это достигается за счёт того, что все изменения сделанные внутри транзакции становятся видны конкурентным транзакциям после совершения текущей.

BEGIN;

SELECT quantity, processed, stock FROM documents WHERE id = 1;
SELECT name, quantity FROM stocks WHERE id = 1;
--вычисления в коде приложения
UPDATE stocks SET quantity = 40.80 WHERE id = 1;
UPDATE documents SET processed = true WHERE id = 1;

COMMIT;

Но такая транзакция не решает проблему с состоянием гонок при конкурентном выполнении операций. В такой ситуаций может помочь другой уровень изоляции — Serializable. Это возможное решение, но оно не единственное. Поведение реализации уровня изоляции Serializable в PostgreSQL не в полной мере соответствует названию. То есть все транзакции с уровнем изоляции Serializable не выполняются последовательно. Вместо этого при совершении (commit) транзакции проверяется конфликт при изменение данных и в случае, если данные уже были изменены конкурентной транзакцией, текущая транзакция завершается ошибкой.

BEGIN ISOLATION LEVEL SERIALIZABLE;

SELECT quantity, processed, stock FROM documents WHERE id = 1;
SELECT name, quantity FROM stocks WHERE id = 1;
--вычисления в коде приложения
UPDATE stocks SET quantity = 40.80 WHERE id = 1;
UPDATE documents SET processed = true WHERE id = 1;

COMMIT;

Представленная выше транзакция решает проблему и с ситуацией гонок и с согласованностью данных. Отличительной чертой такого подхода является то, что код выполняющий эту транзакцию узнает об успешности только после завершения транзакции. И в результате неудачи необходимо будет повторять все действия и вычисления до тех пор, пока транзакция не совершится успешно, либо не будет принято решение отказаться от выполнения действия. Такое поведение неудовлетворительно при большой конкурентной нагрузке, потому как большое количество ресурсов будет потребляться на выполнение повторов. Подобное поведение называется оптимистическим конкурентным контролем (optimistic concurrency control).

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

Блокировки


В параллельном программирования для контроля над потоками выполнения применяется механизм блокировок. Блокировки позволяют сериализовать доступ к определенным областям. СУБД тоже поддерживают механизмы блокировок для контроля доступа к данным. Рассмотрим возможности данного механизма на примере PostgreSQL.

В PostgreSQL реализована поддержка множества типов блокировок. Их основная отличительная черта — это множество типов блокировок, с которым конфликтует текущий тип блокировки. Конфликт означает, что текущая блокировка не может быть захвачена совместно с блокировками любого из конфликтующих типов. В добавок блокировки делятся на явные и неявные. Явные блокировки — это те, которые выполнены в запросе с помощью ключевого слова lock и модификаторов запросов for update или for share, другими словами указанные пользователем. Неявные блокировки — это те, которые захватываются при выполнение различных запросов (select, update, insert, alter и прочие). PostgerSQL поддерживает и отдельный вид блокировок называемых рекомендательными (advisory lock).

Блокировки захватываются с момента выполнение запроса и до конца текущей транзакции. Например в нашей гипотетической учётной системе можно было захватывать эксклюзивную блокировку на строку таблицы stocks соответствующей остатку с которым выполняется операция и на строку таблицы documents, тем самым гарантировать, что только текущая транзакция имеет доступ к этим данным.

BEGIN;

SELECT quantity, processed, stock FROM documents WHERE id = 1 FOR UPDATE;
SELECT name, quantity FROM stocks WHERE id = 1 FOR UPDATE;
--вычисления в коде приложения
UPDATE stocks SET quantity = 40.80 WHERE id = 1;
UPDATE documents SET processed = true WHERE id = 1;

COMMIT;

В коде выше происходит захват блокировок на строки в таблицах documents и stocks. В данном случае к запросу на выборку данных select добавляется ключевые слова for update. Это и есть явное блокирование строки на обновление. Блокировать целую таблицу в данном случае не эффективно. Вообще блокировать таблицу необходимо только при масштабных операциях с данными в ней, a это достаточно редкие случаи. Иначе вы получаете проблему с производительностью при конкурентном доступе, потому-что все обращения к ней сериализуются.

При попытке захвата блокировки на области блокировка на которую уже захвачена, происходит блокирование запроса (по умолчанию) до тех пор, пока блокировка не освободиться, либо в случае с мертвой блокировкой (deadlock) возвращается сообщение об ошибке. Есть возможность устанавливать интервал возвращения управления из блокированного запроса, либо сразу получать сообщение о невозможности захвата блокировки (nowait).

Виды конкурентного контроля


Конкурентный контроль это правила по которым взаимодействуют параллельные потоки выполнения на конкурентных участках. Конкурентный контроль должен гарантирует корректность вычисляемого результата. В качестве дополнительной цели выступает получение результата так быстро, как это возможно в конкретном случае. Конкурентный контроль принято делить на виды согласно времени обработки конфликтов: оптимистический (optimistic), пессимистический (pessimistic) и частично-оптимистический (semi-optimistic).

Оптимистический конкурентный контроль предполагает проверку возможного конфликта при совершении действия. Например пользователь запрашивает некоторые данные из хранилища и изменяет их, после чего он пытается сохранить свои изменения. Если текущая версия данных находящаяся в хранилище соответствует версии данных на основе которых происходили изменения, то конфликт отсутствует и данные могут быть сохранены. В обратном случае возникает конфликт, который обрабатывается либо повторным выполнением действий, либо отказом от них. Оптимистический конкурентный контроль обеспечивает хорошую производительность при относительно малом соперничестве за конкурентный доступ.

Пессимистический конкурентный контроль предполагает проверку возможного конфликта перед выполнением действия. То есть конкурентные потоки выполнения сериализуются на защищаемой области. Это обеспечивает бОльшую производительность при высоком соперничестве за конкурентный доступ.

Частично-оптимистический — это смешанный тип в котором применяются оба подхода одновременно.

Архитектурные паттерны


К данному моменту у читателя должно сформироваться общее представление о проблемах при конкурентном доступе и механизмах СУБД для их решения. В этой секции речь пойдёт о архитектурных паттернах, которые представляют решение проблем конкурентного доступа к базам данных. Ниже не будет полного описания, только общее представление и пример. За полным описанием обращайтесь по ссылкам внизу статьи.

При работе с СУБД данные загружаются в память приложения, с ними или над ними осуществляются действия и результат действий, как правило, должен быть сохранён обратно. Всё это время необходимо хранить информацию об изменениях над данными, чтобы знать, что изменилось. Помимо этого необходимо хранить информацию о созданных и удалённых объектах. Конечно, можно отражать все изменения в базе данных сразу, как только они происходят. В таком случае возникают следующие проблемы: системная транзакция выполняется очень долго, что приводит к конфликтам при конкурентном доступе, так как на протяжении всей транзакции удерживаются блокировки над изменениями в базе данных; взаимодействие с базой данных разбивается на много небольших частей, что тоже малоэффективно. Для решения проблем отслеживания изменений данных описан паттерн Unit of Work. Этот паттерн описывает объект, который отслеживает все изменения и может применить их к базе данных, чтобы согласовать её состояние с произведенными изменениями.

public class UnitOfWork {

	private List<DomainObject> newObjects; 
	private List<DomainObject> updatedObjects;
	private List<DomainObject> deletedObjects;
	
	/**
	 * Создать объект
	 * @return вернуть вновь созданный объект
	 */
	public DomainObject create() {
		DomainObject domainObject = new DomainObject();
		newObjects.add(domainObject);
		return domainObject;
	}
	
	/**
	 * Пометить объект как измененный
	 * @param domainObject измененный объект
	 */
	public void update(DomainObject domainObject) {
		updatedObjects.add(domainObject);
	}
	
	/**
	 * Пометить объект как удалённый
	 * @param domainObject удалённый объект
	 */
	public void remove(DomainObject domainObject) {
		deletedObjects.add(domainObject);
	}
	
	/**
	 * Выполнить изменения в базе данных
	 */
	public void commit() {
		//выполняет SQL запросы на вставку данных INSERT
		insert(newObjects);
		//выполняет SQL запросы на обновление данных UPDATE
		udpate(updatedObjects);
		//выполняет SQL запросы на удаление данных DELETE
		delete(deletedObjects);
	}

//реализация методов insert, update, delete и прочих

}

Выше представлена простейшая реализация паттерна Unit of Work. Объекты DomainObject могут регистрироваться в объекте UnitOfWork при выполнении над ними соответствующих действий. После завершения бизнес транзакции приложение вызывает метод commit у объекта UnitOfWork.
Бизнес транзакция обычно занимает продолжительное время. Как правило, она простирается на несколько системных транзакций. Причина этому уже упоминалась выше — проблемы с длительным захватом блокировок. Из этого следует, что необходимо реализовывать собственный механизм синхронизации, который будет работать поверх нескольких системных транзакций, так как механизмы синхронизации СУБД работают только в пределах одной системной транзакции. Для решения этой проблемы описаны два паттерна Optimistic Offline Lock и Pessimistic Offline Lock, которые реализуют оптимистический и пессимистический виды конкурентного контроля.

Допустим пользователь открывает форму редактирования, он работает над документом несколько минут и сохраняет результат. Это типичный пример бизнес транзакции.

Рассмотрим ситуацию с оптимистическим конкурентным контролем. В случае обнаружения конфликта при сохранении результата редактирования, пользователю будет выведено уведомление с измененными данными и диалог с выбором дальнейших действий. Паттерн Optimistic Offline Lock описывает механизм контроля изменений, который, в общем случае, опирается на механизм версионности данных. Каждый раз при сохранении изменения происходит сравнение версии данных в базе данных и версии на основе которой были выполнены изменения. Если эти версии равны то происходит изменение текущей версии и результат сохраняется, в противном случае возникает конфликт и он обрабатывается либо повтором действий, либо отменой.

public class DomainObject {
	
	private Integer id;
	private Integer version;
	private Object data;
	
	//геттеры и сеттеры
}

public class UnitOfWork {

//реализация методов, описаны выше

	public void update(List<DomainObject> updatedObjects) throws SQLException {
		PreparedStatement ps = 
			connection.prepareStatement(
				"update domain_objects set data = ?, version = version + 1 " +
				"where id = ? and version = ?"
				);
		
		for (DomainObject domainObject: updatedObjects) {
			ps.setObject(1, domainObject.getData());
			ps.setInt(2, domainObject.getId());
			ps.setInt(3, domainObject.getVersion());
			if (ps.executeUpdate() == 0) {
				throw new RuntimeException("Конфликт версий");
			}
		}
		
	}
	
}

В приведённом выше коде представлена реализация update метода с описанным выше паттерном Optimistic Offline Lock. В этой реализации происходит сохранение списка измененных объектов с проверкой версии. Если версия изменилась, то не будет обновлена ни одна запись в таблице и в данном примере происходит бросание исключения. Это исключение соответствует конфликту при котором, запись уже была обновлена конкурентным потоком выполнения.
В случае с пессимистическим конкурентным контролем, реализация паттерна Pessimistic Offline Lock представлена ниже.

class DomainObject {
	
	private Integer id;
	private Boolean blocked;
	private Object data;

	//геттеры и сеттеры
}

public class UnitOfWork {

//реализация методов, описаны выше

	public void update(List<DomainObject> updatedObjects) throws SQLException {
		PreparedStatement updateStatement = 
			connection.prepareStatement(
				"update domain_objects set data = ?, blocked = false " +
				"where id = ? and blocked = true"
				);
		
		for (DomainObject domainObject: updatedObjects) {
			updateStatement.setObject(1, domainObject.getData());
			updateStatement.setInt(2, domainObject.getId());			
			updateStatement.executeUpdate();
		}
		
	}

	public DomainObject get(Integer id) throws SQLException {
		PreparedStatement updateStatement = 
			connection.prepareStatement(
				"update domain_objects set blocked = true " +
				"where id = ? and blocked = false"
				);
		
		updateStatement.setInt(1, id);
				
		if (updateStatement.executeUpdate() == 1) {
			PreparedStatement selectStatement =
				connection.prepareStatement(
						"select * from domain_objects where id = ?"
						);			
			selectStatement.setInt(1, id);
			ResultSet result = selectStatement.executeQuery();
			//result содержит необхродимые данные
			//возврашение объекта DomainObject на основе данных result
			return new DomainObject();
		}
		else {
			throw new RuntimeException("Блокировка уже захвачена");
		}
	}
}

В представленном коде при получении данных из базы данных устанавливается блокировка на поле blocked. Если она уже установлена вызывается исключение, в противном случае возвращается результат. Далее при обновлении объекта блокировка сбрасывается. Данная реализация будет работать при всех уровнях изоляции транзакций. При условии, что сохранение данных будет выполняться после захвата блокировок.

Представленные реализации лишь иллюстрируют описание и не предназначены для использования в реальных приложениях!

Политика блокировок бизнес уровня


Политика блокировок — это правила, которыми регулируется конкурентный доступ к данным. В данном абзаце речь идет о политики блокировок на бизнес уровне, а не реализации на уровне запросов к СУБД и механизмов синхронизации платформы. Необходимо учитывать, что политика блокировок должна быть связанна с бизнес-логикой. И нельзя оставлять вопрос с конкурентным доступом на потом. Например в системе, где область применение говорит о том, что некий документ должен редактироваться единолично, необходимо чтобы бизнес-логика обладала знаниями о этом требовании. В случае реализации такого требования бизнес области, только с помощью механизмов синхронизации, реализация бизнес уровня в приложении будет разделена на несколько частей, что не очень хорошо. Во первых, потому что поддержка такой системы усложняется, хотя вариант оптимизирующий производительность путём использования встроенных процедур СУБД достаточно распространён. Во вторых, существует проблема с длинной системной транзакции, то есть нельзя позволять приложению держать системную транзакцию открытой то же время, что и бизнес транзакцию. Область ответственности за бизнес транзакцию лежит на бизнес логике приложения. Поэтому политика блокировок в бизнес транзакции не может рассматриваться отдельно от бизнес логики.

Дополнительные источники (en)


Patterns of Enterprise Application Architecture (Martin Fowler, David Rice, Matthew Foemmel, Edward Hieatt, Robert Mee, Randy Stafford)
PostgreSQL Concurrency Control
Concurrency control
Isolation level
Поделиться публикацией

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

    0
    Неплохая статья, но, мне кажется, что concurrent в переводе на русский--это не конкурентный, а параллельный/одновременный/синхронный. В русском переводе Фаулера используется обычно «параллельный».
      +2
      Термин «конкурентный» (competitive) выбрал чтобы подчеркнуть именно соревновательный аспект. У Фаулера данный термин не присутствует. А там где речь идёт о параллелизме используется термин «параллельный». Хотя соглашусь, что повсеместно использования термина «параллельный» было бы менее запутанным.
        +1
        www.gnu.org/software/pth/pth-manual.html#item_o_concurrency_vs_2e_parallelism

        Concurrency exists when at least two threads are in progress at the same time. Parallelism arises when at least two threads are executing simultaneously. Real parallelism can be only achieved on multiprocessor machines, of course. But one also usually speaks of parallelism or high concurrency in the context of preemptive thread scheduling and of low concurrency in the context of non-preemptive thread scheduling.

        Так что, конкурентность здесь больше по смыслу
        0
        Мне кажется, имело бы смысл, написать про возможные проблемы при использовании того или иного уровня изоляции транзакций: грязные чтения, неповторяющиеся чтения, etc.
          0
          Целью написания статьи было — минимальное введение в предмет, без перегруженности деталями. Поэтому я позволил себе опустить описание явлений связанных с различными уровнями изоляции. Но видимо зря.
        • НЛО прилетело и опубликовало эту надпись здесь
            0
            Будем надеяться что автор коснётся этих проблем в будущих статьях.
              +2
              Одни из самых распространненых грабель у 1Сников
              • НЛО прилетело и опубликовало эту надпись здесь
                  0
                  К примеру, можно указать время блокировки или null вместо false, а в коде предусмотреть, что если блокировка поставлена больше 30 секунд назад (или любое другое время, в каждом случае выбираемое индивидуально: в первом случае это время было бы несколько секунд, во втором — несколько минут), то считать строку не заблокированной.
                  Как вариант, еще можно использовать уникальный идентификатор заблокировавшего процесса, соответственно, если блокировка по времени истекла и ее забрал кто-то другой, то первый процесс должен проверить, он ли ставил блокировку.
                  В общем, вариантов много можно выдумать и они не сложные, но в каждом конкретном случае подход может оказаться разным в зависимости от ТЗ.
                  +4
                  Есть такое хорошее слово 'совместный' — дословный перевод concurrent, и по смыслу подходит, и читать легче было бы.
                    +2
                    Очень правильная статья, заставляет задуматься!
                    Основной ошибкой проектирования систем является непонимание архитектора, что ошибка выполнения транзакции — это НОРМАЛЬНОЕ состояние, а не исключительная ситуация. При optimistic locking справедливо правило: «let it fault!». Стандартное решение в данной ситуации — это отслеживать коллизии и делать повтор. Однако ни один фреймворк по дефолту это не предлагает.

                    На практике во всех движках вместо ТруЪ serializable level используют более слабый snapshot isolation en.wikipedia.org/wiki/Snapshot_isolation
                    Каждая транзакция видит «снимок» базы на момент ее начала. Метод базируется на сохранении нескольких версий измененных данных, доступных на время другим транзакциям: en.wikipedia.org/wiki/Multiversion_concurrency_control
                    MVCC вместе с write lock дает хорошую производительность, и уровень близкий к serializable. Различие заключается лишь в том, что последовательность обработки двух разных транзакций не гарантируется.
                      0
                      Не во всех, а только в версионных. Для реализации честного Serializable нужно уметь делать предикатные блокировки, чего MVCC не умеет. Блокировочные же планировщики отлично с этим справляются — см. MSSQL, DB2.
                      0
                      Большое спасибо за статью. Раньше всегда в таблицы вводил поле version, чтоб контролировать доступ и изменение. Теперь буду знать.
                        0
                        Как уже было сказано Serializable Postgree — не соответствует честному Serializable, который описан в стандарте. В частности он допускает аномалии типа write skew, чего честный Serializable делать не должен.
                        Вообще, класика подробностей про уровней изоляции — A Critique of ANSI SQL Isolation Levels research.microsoft.com/pubs/69541/tr-95-51.pdf
                          0
                          В PostgreSQL 9.1, beta которой вышла на днях, изменено поведение уровня Serializable. Новая фича называется SSI (Serializable Snapshot Isolation), и гарантирует True serializability без существенного замедления работы СУБД. Предыдущее поведение Serializable стало Repeatable Read. Подробнее: www.postgresql.org/docs/9.1/static/transaction-iso.html#XACT-SERIALIZABLE. PostgreSQL, кстати, первая СУБД, реализовавшая SSI.
                            0
                            Молодцы! =)) Другое дело что «без существенного замедления работы» вопрос открытый. В случае конфликта откат транзакции скорее всего будет дороже блокировки, хотя тут все от конкретных сценариев зависит. Но в любом случае, хорошо, что появился уровень изоляции гарантирующий честный Serializability без дополнительных приседаний.
                              0
                              Я имел ввиду без существенного замедления для случаев, когда конфликтов нет, т.е. в нормальных условиях для абсолютного большинства транзакций. В случае, когда конфликт есть, корректность важнее скорости, если же это не так — можно установить repeatable read и вернуться к поведению, которое было в 9.0 и более ранних версиях.
                                0
                                А я имел ввиду сравнение с традиционными блокировочными планировщиками =)
                                В отсутствие конфликтов накладные расходы в среднем одинаковые, а вот если конфликт случился, то в большинстве случаев подождать на блокировке будет дешевле чем откатывать транзакцию и пытаться выполнить ее снова.
                                  0
                                  Вот только конфликты в такой системе будут происходить гораздо чаще, и они внесут больший вклад в общую производительность, чем откат транзакций при конфликте в MVCC.
                                    0
                                    С чего бы это им чаще происходить? В обычном MVCC-шном снэпшоте RW не конфликтуют, поэтому там потенциально число конфликтов меньше, однако в предложенной технике RW вполне себе конфликтующие операции. Что не удивительно, иначе транзакции нельзя было бы корректно сериализовать.
                                    Таким образом, число конфликтов, при одинаковой схеме данных и запросах, при использовании обоих техник должно быть одинаковым, и это при условии что в SSI корректно реализовали все известные блокировочные оптимизации типа key-range lock-а.
                          0
                          Хорошая статья, а не Пименов ли заказал ее для внутреннего обучения?
                            0
                            Нет. И понятие такого то нет «внутреннее обучение». Не знаю было ли)

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое