
СНИМКИ ДАННЫХ
~~~~~~~~~~~~~

Смоделируем ситуацию, воспользовшись уровнем изоляции repeatable read:

первая транзакция: |--------------------------------
просто транзакция:       |--- insert ---|
вторая транзакция:                            |-----

        => create table t(s text);
        CREATE TABLE

        => insert into t values ('first');
        INSERT 0 1

        1| => begin;
        1| BEGIN

        1| => set transaction isolation level repeatable read;
        1| SET

.......................................................................

Вот что видит первая транзакция:

        1| => select * from t;
        1|    s   
        1| -------
        1|  first
        1| (1 row)
        1| 

.......................................................................

        => begin;
        BEGIN

        => insert into t values ('second');
        INSERT 0 1

Запомним номер "просто транзакции":

        => select txid_current();
         txid_current 
        --------------
                  702
        (1 row)
        

        => commit;
        COMMIT

.......................................................................

        2| => begin;
        2| BEGIN

        2| => set transaction isolation level repeatable read;
        2| SET

.......................................................................

Вот что видит вторая транзакция:

        2| => select * from t;
        2|    s    
        2| --------
        2|  first
        2|  second
        2| (2 rows)
        2| 

А что сейчас увидит первая?

.......................................................................

То же самое, что и в первый раз - в этом и есть смысл уровня repeatable read.

        1| => select * from t;
        1|    s   
        1| -------
        1|  first
        1| (1 row)
        1| 

.......................................................................

Все разница в видимости определяется снимками. Посмотрим на снимки:

        1| => select txid_current_snapshot();
        1|  txid_current_snapshot 
        1| -----------------------
        1|  702:702:
        1| (1 row)
        1| 

        2| => select txid_current_snapshot();
        2|  txid_current_snapshot 
        2| -----------------------
        2|  703:703:
        2| (1 row)
        2| 

Снимок отображается как три значения, разделенные двоеточиями:
* Первое значение - минимальный номер активной транзакции на момент создания снимка.
  Любая более ранняя транзакция будет либо зафиксирована и видна,
  либо откачена и не видна.
* Второе значение - максимальный (еще пока не выданный) номер транзакции.
  Все транзакции начиная с этого номера не будут видны в снимке.
* Третье значение - список номеров активных транзакций на момент создания снимка.

Почему вторая транзакция не видит первую в качестве активной?

.......................................................................

Транзакция не получает номер до тех пор, пока не начнет изменять данные.
До этого она обходится виртуальным номером (virtualxid). Например, первая:

        1| => select virtualxid, mode
        1| => from pg_locks
        1| => where pid = pg_backend_pid() and locktype = 'virtualxid';
        1|  virtualxid |     mode      
        1| ------------+---------------
        1|  3/44       | ExclusiveLock
        1| (1 row)
        1| 

Виртуальный номер состоит из двух частей: номера серверного процесса
и уникального номера в пределах этого процесса.

.......................................................................

Если же попробовать посмотреть номер транзакции с помощью функции txid_current,
он будет выделен:

        1| => select txid_current();
        1|  txid_current 
        1| --------------
        1|           703
        1| (1 row)
        1| 

.......................................................................

Сопоставим снимки и информацию о версионности в строках:

        1| => select txid_current_snapshot();
        1|  txid_current_snapshot 
        1| -----------------------
        1|  702:702:
        1| (1 row)
        1| 

        2| => select txid_current_snapshot();
        2|  txid_current_snapshot 
        2| -----------------------
        2|  703:703:
        2| (1 row)
        2| 

        => select xmin, xmax, * from t;
         xmin | xmax |   s    
        ------+------+--------
          701 |    0 | first
          702 |    0 | second
        (2 rows)
        

.......................................................................

Первая транзакция видит первую строку, так как xmin меньше максимального номера,
транзакция с номером xmin не входит в активные и зафиксирована.
Но не видит вторую строку, так как xmin строки больше или равен максимальному
номеру снимка.

Вторая транзакция видит обе строки, так как xmin меньше максимального номера,
транзакция с номером xmin не входит в активные и зафиксирована.

.......................................................................

Рассмотрим еще один пример. Здесь у первой транзакции будет настоящий номер.

первая транзакция: |--- insert ---------------------
просто транзакция:       |--- insert ---|
вторая транзакция:                            |-----

        2| => commit;
        2| COMMIT

        1| => commit;
        1| COMMIT

        => truncate table t;
        TRUNCATE TABLE

.......................................................................

        1| => begin;
        1| BEGIN

        1| => set transaction isolation level repeatable read;
        1| SET

        1| => insert into t values ('first');
        1| INSERT 0 1

        1| => select * from t;
        1|    s   
        1| -------
        1|  first
        1| (1 row)
        1| 

.......................................................................

        => insert into t values ('second');
        INSERT 0 1

.......................................................................

        2| => begin;
        2| BEGIN

        2| => set transaction isolation level repeatable read;
        2| SET

        2| => select * from t;
        2|    s    
        2| --------
        2|  second
        2| (1 row)
        2| 

.......................................................................

Снова смотрим на снимки. Сначала первая транзакция:

        1| => select txid_current_snapshot();
        1|  txid_current_snapshot 
        1| -----------------------
        1|  705:705:
        1| (1 row)
        1| 

Информацию о версионности придется смотреть из двух транзакций, так как ни одна из них
не видит полной картины (или можно было бы воспользоваться pageinspect):

        1| => select xmin, xmax, * from t;
        1|  xmin | xmax |   s   
        1| ------+------+-------
        1|   705 |    0 | first
        1| (1 row)
        1| 

        2| => select xmin, xmax, * from t;
        2|  xmin | xmax |   s    
        2| ------+------+--------
        2|   706 |    0 | second
        2| (1 row)
        2| 

.......................................................................

Первая транзакция видит первую строку, так как она ее создала.
Но не видит вторую строку, так как xmin строки больше или равен максимальному
номеру снимка.

.......................................................................

Теперь вторая транзакция:

        2| => select txid_current_snapshot();
        2|  txid_current_snapshot 
        2| -----------------------
        2|  705:707:705
        2| (1 row)
        2| 

        1| => select xmin, xmax, * from t;
        1|  xmin | xmax |   s   
        1| ------+------+-------
        1|   705 |    0 | first
        1| (1 row)
        1| 

        2| => select xmin, xmax, * from t;
        2|  xmin | xmax |   s    
        2| ------+------+--------
        2|   706 |    0 | second
        2| (1 row)
        2| 

.......................................................................

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

.......................................................................

БЛОКИРОВКИ НА УРОВНЕ СТРОК
~~~~~~~~~~~~~~~~~~~~~~~~~~

Рассмотрим пример двух транзакций, обновляющих одну строку.

        1| => commit;
        1| COMMIT

        2| => commit;
        2| COMMIT

        => create table block(n numeric);
        CREATE TABLE

        => insert into block values (42);
        INSERT 0 1

.......................................................................

        1| => begin;
        1| BEGIN

        1| => update block set n = 1;
        1| UPDATE 1

        1| => select txid_current();
        1|  txid_current 
        1| --------------
        1|           709
        1| (1 row)
        1| 

        2| => begin;
        2| BEGIN

        2| => update block set n = 2;

.......................................................................

Вторая транзакция заблокирована до тех пор, пока первая не завершит транзакцию
либо фиксацией изменений, либо откатом.
О том, что строка изменяется, транзакция узнает по тому, что xmax
принадлежит активной транзакции.

        => select xmin, xmax from block;
         xmin | xmax 
        ------+------
          708 |  709
        (1 row)
        

.......................................................................

Расширение pgrowlocks позволяет удобно посмотреть на блокировку:

        => create extension if not exists pgrowlocks;
        CREATE EXTENSION

        => select * from pgrowlocks('block');
         locked_row | locker | multi | xids  |       modes       |  pids  
        ------------+--------+-------+-------+-------------------+--------
         (0,1)      |    709 | f     | {709} | {"No Key Update"} | {8976}
        (1 row)
        

Здесь мы видим тип блокировки и номер процесса.

.......................................................................

Кроме блокировки строки тот факт, что одна транзакция заблокирована другой,
отмечается и в блокировке высокого уровня:

        1| => select transactionid, pid, mode, granted
        1| => from pg_locks
        1| => where locktype = 'transactionid';
        1|  transactionid | pid  |     mode      | granted 
        1| ---------------+------+---------------+---------
        1|            709 | 8976 | ExclusiveLock | t
        1|            710 | 8989 | ExclusiveLock | t
        1|            709 | 8989 | ShareLock     | f
        1| (3 rows)
        1| 

.......................................................................

Существуют как эксклюзивные блокировки строки (for update, for no key update),
так и разделяемые (for key share, for share).
В случае разделяемой блокировки одна строка может быть заблокирована несколькими
транзакциями одновременно. Посмотрим пример.

        1| => rollback;
        1| ROLLBACK

        2| => rollback;
        2| UPDATE 1
        2| ROLLBACK

.......................................................................

        1| => begin;
        1| BEGIN

        1| => select * from block for share;
        1|  n  
        1| ----
        1|  42
        1| (1 row)
        1| 

        2| => begin;
        2| BEGIN

        2| => select * from block for share;
        2|  n  
        2| ----
        2|  42
        2| (1 row)
        2| 

.......................................................................

        => select * from pgrowlocks('block');
         locked_row | locker | multi |   xids    |     modes     |    pids     
        ------------+--------+-------+-----------+---------------+-------------
         (0,1)      |      1 | t     | {712,713} | {Share,Share} | {8976,8989}
        (1 row)
        

Что при этом записано в поле xmax строки?

.......................................................................

В поле xmax записывается число, называемое multixact.
Специальный бит указывает, что это число не является номером транзакции.
Расшифровка, которую показывает pgrowlocks, хранится в каталоге $PGDATA/pg_multixact.

        => select xmin, xmax, * from block;
         xmin | xmax | n  
        ------+------+----
          708 |    1 | 42
        (1 row)
        

.......................................................................

БЛОКИРОВКИ НА УРОВНЕ ТАБЛИЦ
~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

        1| => rollback;
        1| ROLLBACK

        2| => rollback;
        2| ROLLBACK

.......................................................................

        1| => begin;
        1| BEGIN

        1| => select * from block;
        1|  n  
        1| ----
        1|  42
        1| (1 row)
        1| 

        1| => select relation::regclass, pid, mode, granted
        1| => from pg_locks
        1| => where locktype = 'relation' and relation != 'pg_locks'::regclass;
        1|  relation | pid  |      mode       | granted 
        1| ----------+------+-----------------+---------
        1|  block    | 8976 | AccessShareLock | t
        1| (1 row)
        1| 

При обращении к таблице она блокируется в разделяемом режиме AccessShareLock,
чтобы другой процесс не мог изменить определение этой таблицы (например,
удалить столбец).

.......................................................................

        1| => update block set n = 1;
        1| UPDATE 1

        1| => select relation::regclass, pid, mode, granted
        1| => from pg_locks
        1| => where locktype = 'relation' and relation != 'pg_locks'::regclass;
        1|  relation | pid  |       mode       | granted 
        1| ----------+------+------------------+---------
        1|  block    | 8976 | AccessShareLock  | t
        1|  block    | 8976 | RowExclusiveLock | t
        1| (2 rows)
        1| 

При изменении таблицы добавляется блокировка в режиме RowExclusiveLock,
запрещающая, в частности, выполнение команды create index по таблице.

.......................................................................

Конец демонстрации.

        1| => \q
        2| => \q
        => \q
