Operações atômicas com Deno KV
Tags: deno, typescript, database
Published: 2023-10-20 22:27:45

---
title: Operações atômicas com Deno KV
published: true
cover_image: https://raw.githubusercontent.com/khaosdoctor/blog-assets/master/images/2023/07/articles-2.png
date: 2023-07-27 08:00:25 UTC
tags: deno,typescript,databases
canonical_url: https://blog.lsantos.dev/kv-atomic-ops/
---

No [outro artigo](https://blog.lsantos.dev/deno-kv-beta) comentamos sobre a nova versão beta fechada do Deno KV e também falamos sobre o conceito de chave e valor. Mas, infelizmente, como o artigo já estava meio grande, algumas coisas ficaram de fora, uma dessas coisas foi a incrível capacidade de atomicidade do KV.

Mas o que é ser atômico? E o que isso significa no contexto de um banco de dados desse porte?

# Atomicidade

Atomicidade é a letra **A** do acrônimo **ACID** que você provavelmente já ouviu, cada uma das letras do ACID tem um significado profundo para qualquer banco de dados, especialmente se ele suportar transações:

- **Atomicidade** : Que é a mais importante pra gente aqui agora. Esse nada mais é do que o conceito de **transações**. Ou seja, um conjunto de operações que são executados de uma única vez como se fossem um único átomo, ou tudo acontece, ou nada acontece.
- **Consistência** : Garantia de que as transações no banco só vão modificar as tabelas de forma pré-definida. Além disso, essa propriedade também garante que qualquer tipo de corrupção de dados ou perda em um determinado lugar não vai afetar outras tabelas.
- **Isolamento:** Essencialmente trata o problema de corrida, quando múltiplos usuários estão realizando leituras e escritas no banco de dados, o isolamento das transações garante que essas transações concorrentes não interfiram umas com as outras.
- **Durabilidade:** É a principal propriedade de um banco de dados. Ela garante que qualquer mudança em seus dados através de qualquer transação de sucesso vão ser persistidas mesmo no evento de uma falha de sistema.

Além de [operações](https://deno.land/manual@v1.35.1/runtime/kv/operations) não atômicas, o KV também suporta transações [atômicas](https://deno.land/api@v1.35.1?s=Deno.AtomicOperation&unstable=), onde ou tudo é executado ou nada é executado. Esse conceito é baseado em **mutations** que são um conjunto de ações aplicados a um registro.

Mais uma vez o KV usa as `versionstamps` para poder saber o que foi modificado ou não, a transação só será enviada com sucesso se as `versionstamps` atuais das chaves forem as mesmas das que foram passadas na mutação, dessa forma é possível garantir que estamos modificando a última versão dos dados.

Nestas operações estão inclusas todas as anteriores com algumas diferenças:

- `check`: equivalente ao `get`, porém ao invés de obter uma chave, vai testar uma chave já obtida contra o versionstamp que está no banco de dados
- `sum`: um tipo de operação `mutate`, mas sem atalho direto
- `min`: outro `mutate`, mas que tem um atalho direto
- `max`: outro `mutate`, também um atalho direto
- `commit`: finaliza a transação e envia os valores ao banco
- `delete`: igual ao anterior

Para esta explicação é melhor usar um exemplo do que passar método por método, já que a maioria das funcionalidades são conhecidas. O próprio exemplo do Deno é muito bom porque é uma das principais funcionalidades quando estamos trabalhando com transações: **transferência de dinheiro**.

Quando estamos transferindo fundos de uma conta para a outra, primeiro temos que garantir que a primeira conta tem os fundos, se fizermos de forma assíncrona, é possível que, enquanto estamos tentando transferir o dinheiro de uma conta para a outra, uma transação aconteça no meio e não tenhamos mais os fundos necessários, por isso precisamos executar todas as operações (tirar de uma conta e colocar em outra) de uma vez só, ou não executar nenhuma.

```
const senderKey = ['account', 'alice']
const receiverKey = ['account', 'bob']
cosnt amount = 100

// tentamos a transação até conseguir
let res = { ok: false }

while (!res.ok) {
	const [senderResponse, receiverResponse] = await kv.getMany([senderKey, receiverKey])
    
    if (!senderResponse || !receiverResponse) break
    
    const senderBalance = senderRes.value
    const receiverBalance = receiverRes.value
    
    if (senderBalance < amount) {
    	throw new Error('Saldo insuficiente')
    }
    
    const newSenderBalance = senderBalance - amount
    const newReceiverBalance = receiverBalance + amount
 	
    // salvamos no banco de dados
}

```

Até aqui estamos fazendo tudo em memória. Para salvarmos no banco precisamos primeiro checar os dois saldos, podemos fazer isso com o comando `check`:

```
const senderKey = ['account', 'alice']
const receiverKey = ['account', 'bob']
const amount = 100

// tentamos a transação até conseguir
let res = { ok: false }

while (!res.ok) {
	const [senderResponse, receiverResponse] = await kv.getMany([senderKey, receiverKey])
    
    if (!senderResponse || !receiverResponse) break
    
    const senderBalance = senderRes.value
    const receiverBalance = receiverRes.value
    
    if (senderBalance < amount) {
    	throw new Error('Saldo insuficiente')
    }
    
    const newSenderBalance = senderBalance - amount
    const newReceiverBalance = receiverBalance + amount
 	
    // salvamos no banco de dados
    res = await kv.atomic()
    	.check(senderResponse)
        .check(receiverResponse)
}

```

Aqui é importante notar duas coisas:

1. Estamos usando o método `atomic()`, que é o namespace que vai ter todas as propriedades atômicas do KV
2. Estamos usando o `check` e passando como parâmetro não um valor, mas a resposta inteira do comando `getMany`, porque precisamos passar tanto o valor quanto a `versionstamp` daquela chave

O que o `check` vai fazer é realizer um `get` no KV e verificar se os dois registros são **idênticos** , se falhar, então a transação será abortada. Agora podemos atualizar os valores de cada pessoa:

```
const senderKey = ['account', 'alice']
const receiverKey = ['account', 'bob']
const amount = 100

// tentamos a transação até conseguir
let res = { ok: false }

while (!res.ok) {
	const [senderResponse, receiverResponse] = await kv.getMany([senderKey, receiverKey])
    
    if (!senderResponse || !receiverResponse) break
    
    const senderBalance = senderRes.value
    const receiverBalance = receiverRes.value
    
    if (senderBalance < amount) {
    	throw new Error('Saldo insuficiente')
    }
    
    const newSenderBalance = senderBalance - amount
    const newReceiverBalance = receiverBalance + amount
 	
    // salvamos no banco de dados
    res = await kv.atomic()
    	.check(senderResponse)
        .check(receiverResponse)
        .set(senderKey, newSenderBalance)
        .set(receiverKey, newReceiverBalance)
        .commit()
}

```

Toda transação deve ser finalizada com um `commit()` para que possamos executar a fila de operações que foram feitas.

Agora que a gente entendeu o conceito, vamos ver as outras operações.

### `kv.atomic().mutate()` - Sum, Min e Max

Além das operações normais, temos um outro método chamado `mutate` dentro de `atomic()`, este método aceita um objeto de configuração que pode ter três chaves:

- `type`: O tipo da mutação, que pode ser, até o momento, `sum`, `min` ou `max`
- `key`: A chave a ser modificada
- `value`: O novo valor, precisa ser um objeto do tipo `Deno.KvU64` que é criado a partir de um `BigInt` com `new Deno.KvU64(100n)` por exemplo

Vamos falar sobre as mutações no geral, vou dar o primeiro exemplo com o `sum`, mas não há muito a necessidade de explicar as demais com exemplos porque elas seguem a mesma ideia:

**Sum**

O `sum` vai atomicamente adicionar um valor a uma chave. Se o valor não existir, ele vai ser criado com o valor que seria adicionado, por exemplo, se adicionarmos 10 em uma chave não existente, o resultado vai ser a chave que queremos adicionar, com o valor 10. Se a chave existir, o valor vai ser adicionado através de uma soma.

> Operações de mutation **só podem ser feitas em tipos de dados BigInt** que, no Deno KV, são representadas pelo tipo `Deno.KvU64`, que significa [Deno KV Unsigned 64-bit Integer](https://deno.land/manual@v1.35.1/runtime/kv/key_space#denokvu64-type), esse tipo não pode ser armazenado em nenhuma estrutura, ele precisa ser um top-level value.

A estrutura básica de uma soma é a seguinte:

```
await kv.atomic()
	.mutate({
    	type: 'sum',
        key: ['accounts', 'alice'],
        value: new Deno.KvU64(80n),
    })
    .commit()
```

Isso significa que podemos substituir nosso código acima por algo como:

```
const senderKey = ['account', 'alice']
const receiverKey = ['account', 'bob']
const amount = 100

// tentamos a transação até conseguir
let res = { ok: false }

while (!res.ok) {
	const [senderResponse, receiverResponse] = await kv.getMany([senderKey, receiverKey])
    
    if (!senderResponse || !receiverResponse) break
    
    const senderBalance = senderRes.value
    const receiverBalance = receiverRes.value
    
    if (senderBalance < amount) {
    	throw new Error('Saldo insuficiente')
    }
 	
    // salvamos no banco de dados
    res = await kv.atomic()
    	.check(senderResponse)
        .check(receiverResponse)
        .mutate({
        	type: 'sum',
            key: senderKey,
            value: new Deno.KvU64(-BigInt(amount)),
        })
        .mutate({
        	type: 'sum',
            key: receiverKey,
            value: new Deno.KvU64(BigInt(amount)),
        })
        .commit()
}
```

**Min e Max**

Da mesma forma que o `sum`, o `min` e `max`setados como `type`vão fazer com que a chave obtenha o menor e maior valor, respectivamente, comparado entre o valor atual da chave e o valor que você está passando.

Por exemplo, se tivermos uma chave `['accounts', 'alice']` cujo valor é `100`, e passarmos uma mutation do tipo `min` com o valor de `50`, o novo valor vai ser `Math.min(100, 50)` que é `50`, porém se o valor original for `30`, a chave não será modificada. O mesmo vale para o `max`.

Da mesma forma que o `sum`, se a chave não existir ela será criada com o valor passado, ou seja, não será assumido que o valor é `0` inicialmente para nenhum dos casos.

```
await kv.atomic()
	.mutate({
    	type: 'min',
        key: ['accounts', 'alice'],
        value: new Deno.KvU64(100n),
    })
    .commit()
```

# Conclusão

Com as operações atômicas, podemos realizar transações mais seguras que são garantidas de terem o resultado esperado.

Nos próximos artigos vamos construir algum projeto usando o Deno Deploy e o Deno KV!