Семантика наборов чтения-записи

В этом разделе подробно рассматривается реализация наборов чтения-записи в текущей версии.

Симуляция транзакций и наборы чтения-записи

Во время симуляции транзакции на одобряющем узле для нее подготавливается набор чтения-записи. Набор чтения содержит список уникальных ключей и соответствующих им номеров версий (но не значений), которые транзакция считывает во время симуляции. Набор записи содержит список уникальных ключей (хотя некоторые из них могут совпадать с ключами, присутствующими в наборе чтения) и их новые значения, которые записываются транзакцией. Для ключа устанавливается маркер удаления (вместо нового значения), если при выполнении обновления этот ключ должен быть удален.

Кроме того, если транзакция записывает значение ключа несколько раз, сохраняется только последнее записанное значение. Также, при считывании транзакцией значения ключа, возвращается значение из записанного состояния, даже если транзакция обновила значение ключа перед выполнением операции чтения. Другими словами, семантика «чтения собственных записей» не поддерживается.

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

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

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

<TxReadWriteSet>
  <NsReadWriteSet name="chaincode1">
    <read-set>
      <read key="K1", version="1">
      <read key="K2", version="1">
    </read-set>
    <write-set>
      <write key="K1", value="V1">
      <write key="K3", value="V2">
      <write key="K4", isDelete="true">
    </write-set>
  </NsReadWriteSet>
<TxReadWriteSet>

Кроме того, при выполнении транзакцией запроса по диапазону во время симуляции, такой запрос и его результаты добавляются в набор чтения-записи в качестве информации о запросе query-info.

Проверка транзакции и обновление глобального состояния с использованием набора чтения-записи

Обновляющий узел использует часть «чтения» набора чтения-записи для проверки действительности транзакции, а также часть «записи» для обновления версий и значений затронутых ключей.

На этапе проверки транзакция считается подтвержденной, если версия каждого ключа в наборе чтения транзакции совпадает с версией соответствующих ключей в глобальном состоянии при условии, что все предыдущие подтвержденные транзакции (включая предыдущие транзакции в том же блоке) уже записаны (записанное состояние — committed-state). Дополнительная проверка выполняется, если набор чтения-записи также содержит одну или несколько записей с информацией query-info.

Такая дополнительная проверка гарантирует, что ни один ключ не был вставлен/удален/обновлен в супердиапазоне результатов (т. е. множестве диапазонов), записанных в информации query-info. Другими словами, при повторном выполнении во время проверки записанного состояния любого из запросов по диапазону, выполненных транзакцией ранее при симуляции, этот запрос должен вернуть те же результаты, что и при симуляции. Если транзакция обнаруживает фантомные элементы во время записи, эта проверка отмечает транзакцию как неподтвержденную. Обратите внимание, что защита от фантомных элементов присутствует только в запросах по диапазону (например, с помощью функции GetStateByRange чейнкода) и еще не реализована для других типов запросов (например, для функции GetQueryResult чейнкода). В других типах запросов существует риск считывания фантомных записей. Поэтому их следует использовать только в транзакциях, которые осуществляют исключительно чтение и не отправляются в службу упорядочения, если приложение не может гарантировать согласованность результатов между симуляцией и проверкой/записью.

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

Пример симуляции и проверки

В этом подразделе приводится пример сценария, позволяющий лучше понять семантику. В приведенном примере ключ k в глобальном состоянии представлен кортежем (k,ver,val), где ver — последняя версия ключа k, имеющего значение val.

Теперь рассмотрим набор из пяти транзакций T1, T2, T3, T4 и T5, симулированных на одной и той же версии базы данных глобального состояния. В следующем фрагменте кода показан снимок состояния базы данных глобального состояния, относительно которого происходит симуляция транзакций, а также последовательность операций чтения и записи, выполняемых каждой из этих транзакций.

World state: (k1,1,v1), (k2,1,v2), (k3,1,v3), (k4,1,v4), (k5,1,v5)
T1 -> Write(k1, v1), Write(k2, v2*)
T2 -> Read(k1), Write(k3, v3*)
T3 -> Write(k2, v2**)
T4 -> Write(k2, v2***), read(k2)
T5 -> Write(k6, v6*), read(k5)

Теперь предположим, что эти транзакции упорядочены в последовательности T1,…,T5 (которые могут содержаться в одном или разных блоках).

  1. Транзакция T1 успешно проходит проверку, потому что не производит чтения. Далее, кортеж ключей k1 и k2 глобального состояния обновляется до (k1,2,v1'), (k2,2,v2*).
  2. Транзакция T2 не проходит проверку, поскольку считывает ключ k1, который был изменен предыдущей транзакцией T1.
  3. Транзакция T3 успешно проходит проверку, потому что не производит чтения. Далее, кортеж ключа k2 глобального состояния обновляется до (k2,3,v2**).
  4. Транзакция T4 не проходит проверку, поскольку считывает ключ k2, который был изменен предыдущей транзакцией T1.
  5. Транзакция T5 проходит проверку, поскольку считывает ключ k5, который не изменялся предыдущими транзакциями.

Примечание. Транзакции с несколькими наборами чтения-записи не поддерживаются в текущей версии.