Lock-Free Reservation no Oracle Database 23ai/26ai: concorrência sem bloqueio em colunas numéricas

A partir do Oracle Database 23ai (e mantido no 26ai), a Oracle introduziu o Lock-Free Reservation, uma forma diferente de lidar com concorrência em colunas numéricas “quentes” – aquelas que recebem atualizações o tempo todo, como:

  • saldo de conta corrente;
  • quantidade em estoque;
  • limite de crédito;
  • contadores de uso (número de acessos, milhas, pontos etc.).
Ao invés de gerar um lock na linha (row lock) durante toda a transação (até que o commit ou rollback sejam realizados), o banco registra uma "reserva" em uma coluna especial chamada RESERVABLE, usando um journal interno. As transações podem seguir em paralelo, e o update real na linha só acontece no commit.

No modelo tradicional, quando uma sessão executa um processo de DML (insert, update, delete) a linha inteira fica bloqueada até o commit ou rollback. Se mais de uma sessão tentar atualizar a mesma linha ao mesmo tempo, as demais ficarão esperando o lock gerado pela sessão inicial, gerando:

  • esperas em enq: TX - row lock contention;
  • aumento de tempo de resposta;
  • percepção de lentidão ou “travamento” na aplicação.
Muitas vezes a sessão precisava apenas reservar um delta na coluna numérica (exemplo: reservar 1 unidade de um determinado produto), sem impedir que outras sessões façam reservas compatíveis no mesmo recurso.

O que é o Lock-free reservation e colunas 

Com o Lock-Free Reservation os updates nessa coluna seguem um padrão específico. Em vez de alterar a linha imediatamente, o Oracle grava a operação em uma tabela de journal (SYS_RESERVJRNL_<obj#>), associada à tabela base. 
O valor final na coluna é ajustado no commit, levando em conta todas as reservas concorrentes que foram aprovadas.
Ao criar a tabela com uma coluna RESERVABLE, o Oracle cria automaticamente a tabela de journal (reservations journal) no mesmo schema/tablespace para registrar as reservas.

De forma resumida, para usar lock-free reservations

  • A coluna deve ser numérica: NUMBER, INTEGER ou FLOAT;
  • A tabela precisa ter PRIMARY KEY;
  • A coluna RESERVABLE só pode participar de CHECK constraints (incluindo constraints de tabela);
  • Não pode ter índice;
  • Máximo de 10 colunas reservable por tabela;
  • Tabelas especiais NÃO suportam colunas reservable: Blockchain, sharded, external, cluster, IOT, temporárias;
  • Não é permitido usar a coluna reservable em partição, nem especificar storage para ela;
  • Só funciona em tabelas de usuário – se tentar aplicar em tabelas de sistema, você verá erros como ORA-5575x.

Padrão para o processo de update

  • Proibido: SET col_reservable = :valor_direto → gera erro.
  • Em PK composta, todas as colunas da PK têm que ir no WHERE.
  • Você pode atualizar várias colunas reservable na mesma instrução, mas não pode misturar reservable + não-reservable no mesmo UPDATE, nem usar RETURNING.
Exemplo:
UPDATE <tabela>
   SET <col_reservable> = <col_reservable> + (<expr>)
 WHERE <pk_column> = <expr>;

-- ou

UPDATE <tabela>
   SET <col_reservable> = <col_reservable> - (<expr>)
 WHERE <pk_column> = <expr>;

 Exemplo prático - conta bancária com e sem lock-free

    Cenário tradicional (sem lock-free)

Criação da tabela sem lock-free e inserção de dados para simulação
SQL>
SQL> CREATE TABLE conta_trad (
  2      id_conta NUMBER PRIMARY KEY,
    cliente  VARCHAR2(50),
    saldo    NUMBER
      CONSTRAINT ck_conta_trad_saldo_min CHECK (saldo >= 0)
);  3    4    5    6

Table created.

SQL> INSERT INTO conta_trad (id_conta, cliente, saldo)
VALUES (1, 'Cliente 1', 100);  2

1 row created.

SQL> COMMIT;

Commit complete.

SQL>

Simulação do processo com um update na sessão01 sem commit, em seguida um update sem commit na sessão02.
### SESSÃO 01

SQL> 
SQL> UPDATE conta_trad
  2  SET saldo = saldo - 90
  3  WHERE id_conta = 1;

1 row updated.

SQL> 

### SESSÃO 02
[oracle@oracle26ai ~]$ sqlplus USER/PASSWORD@FREEPDB1

SQL*Plus: Release 23.26.0.0.0 - Production on Wed Oct 29 10:28:58 2025
Version 23.26.0.0.0

Copyright (c) 1982, 2025, Oracle.  All rights reserved.

Last Successful login time: Wed Oct 29 2025 10:27:51 -03:00

Connected to:
Oracle AI Database 26ai Free Release 23.26.0.0.0 - Develop, Learn, and Run for Free
Version 23.26.0.0.0

SQL> UPDATE conta_trad
  2  SET saldo = saldo - 5
  3  WHERE id_conta = 1;


******* LOCK GERADO COMO ESPERADO *******

Como esperado no modelo tradicional, quando duas sessões tentam atualizar o mesmo registro, ocorre contenção de bloqueio: a primeira sessão obtém o lock da linha e a segunda fica aguardando a liberação desse lock para prosseguir.

    Cenário com lock-free

Criação da tabela com lock-free e inserção de dados para simulação.
SQL>
SQL> CREATE TABLE conta (
    id_conta NUMBER PRIMARY KEY,
    cliente  VARCHAR2(50),
    saldo    NUMBER RESERVABLE
             CONSTRAINT ck_conta_saldo_min CHECK (saldo >= 0)
);
  2    3    4    5    6
Table created.

SQL> INSERT INTO conta (id_conta, cliente, saldo)
VALUES (1, 'Cliente 1', 100);  2

1 row created.

SQL> COMMIT;

Commit complete.

SQL>

Simulação do processo com um update na sessão01 sem commit, em seguida um update sem commit na sessão02.
### SESSÃO 01
SQL> SQL> UPDATE conta SET saldo = saldo - 20 WHERE id_conta = 1; 2 3 1 row updated. SQL> !

### SESSÃO 02
[oracle@oracle26ai ~]$ sqlplus USER/PASSWORD@FREEPDB1
SQL*Plus: Release 23.26.0.0.0 - Production on Wed Oct 29 10:40:40 2025 Version 23.26.0.0.0 Copyright (c) 1982, 2025, Oracle. All rights reserved. Last Successful login time: Wed Oct 29 2025 10:39:58 -03:00 Connected to: Oracle AI Database 26ai Free Release 23.26.0.0.0 - Develop, Learn, and Run for Free Version 23.26.0.0.0 SQL> UPDATE conta SET saldo = saldo - 5 WHERE id_conta = 1; 2 3 1 row updated. SQL>
###### VEJA QUE O UPDATE FOI REALIZADO SEM FICAR EM LOCK ###########

Na primeira sessão, atualizamos o saldo da conta 1 subtraindo 20 unidades, mantendo a transação aberta, sem COMMIT. Em seguida, abrimos uma segunda sessão e executamos outro UPDATE na mesma linha, dessa vez subtraindo 5 unidades.

Embora no modelo tradicional a segunda sessão ficasse bloqueada aguardando o lock de linha da primeira transação, com a coluna saldo definida como RESERVABLE o Oracle registra apenas os deltas (-20 e -5) de forma lock-free. Assim, ambas as sessões conseguem atualizar a mesma linha sem contenção de bloqueio, e o valor final do saldo será a combinação dos deltas aplicados após os commits.

Para entender melhor o que está acontecendo, podemos enxergar a tabela de journal criada automaticamente.
SQL>
SQL>
SQL> UPDATE conta
   SET saldo = saldo - 20
 WHERE id_conta = 1;  2    3

1 row updated.

SQL> !
[oracle@oracle26ai ~]$ sqlplus lab_txn/lab_txn@FREEPDB1

SQL*Plus: Release 23.26.0.0.0 - Production on Wed Oct 29 10:40:40 2025
Version 23.26.0.0.0

Copyright (c) 1982, 2025, Oracle.  All rights reserved.

Last Successful login time: Wed Oct 29 2025 10:39:58 -03:00

Connected to:
Oracle AI Database 26ai Free Release 23.26.0.0.0 - Develop, Learn, and Run for Free
Version 23.26.0.0.0

SQL> UPDATE conta
   SET saldo = saldo - 5
 WHERE id_conta = 1;  2    3

1 row updated.

SQL>

Essa é a reservation journal table associada às tabelas com colunas RESERVABLE. 
Atenção: Ela é mantida pela engine do banco; use só para consulta quando precisar entender o comportamento.

Logo após um UPDATE em uma coluna reservable e antes de um commit, na mesma sessão é possível consultar os dados da journal table.

Veja o exemplo da consulta feita na sessão02, antes e após o commit.
SQL>
SQL> SELECT object_name, object_type
  FROM user_objects
 WHERE object_name LIKE 'SYS_RESERVJRNL%';  2    3

OBJECT_NAME                                                                                                                      OBJECT_TYPE
-------------------------------------------------------------------------------------------------------------------------------- -----------------------
SYS_RESERVJRNL_73620                                                                                                             TABLE

SQL> select * from SYS_RESERVJRNL_73620;

ORA_SAGA_ID$                     ORA_TXN_ID$      ORA_STATUS$ ORA_ST   ID_CONTA S SALDO_RESERVED
-------------------------------- ---------------- ----------- ------ ---------- - --------------
                                 0500040094030000 ACTIVE      UPDATE          1 -              5

SQL> commit;

Commit complete.

SQL> select * from SYS_RESERVJRNL_73620;

no rows selected

SQL>

Voltando na sessão01 e consultando a sua journal table. 
SQL> set lines 210
SQL> SELECT object_name, object_type
  FROM user_objects
 WHERE object_name LIKE 'SYS_RESERVJRNL%';  2    3

OBJECT_NAME                                                                                                                      OBJECT_TYPE
-------------------------------------------------------------------------------------------------------------------------------- -----------------------
SYS_RESERVJRNL_73620                                                                                                             TABLE

SQL> select * from SYS_RESERVJRNL_73620;

ORA_SAGA_ID$                     ORA_TXN_ID$      ORA_STATUS$ ORA_ST   ID_CONTA S SALDO_RESERVED
-------------------------------- ---------------- ----------- ------ ---------- - --------------
                                 01001C002C030000 ACTIVE      UPDATE          1 -             20

SQL> select * from conta;

  ID_CONTA CLIENTE                                                 SALDO
---------- -------------------------------------------------- ----------
         1 Cliente 1                                                  95

SQL> commit;

Commit complete.

SQL> select * from SYS_RESERVJRNL_73620;

no rows selected

SQL> select * from conta;

  ID_CONTA CLIENTE                                                 SALDO
---------- -------------------------------------------------- ----------
         1 Cliente 1                                                  75

SQL>

O Lock-Free Reservation chega como uma resposta direta a um problema clássico de concorrência: a disputa por linhas “quentes” em colunas numéricas que vivem recebendo updates. Em vez de manter sessões presas em row locks enquanto uma transação não faz COMMIT, o banco passa a registrar apenas deltas internamente, aplicando o ajuste efetivo no momento do commit. 
O resultado é simples de perceber na prática: menos esperas em enq: TX - row lock contention, melhor tempo de resposta e uma experiência de aplicação muito mais fluida em cenários de alta concorrência.



Comentários