Sled: Banco de Dados Embarcado em Rust

Guia completo da crate sled para Rust. Aprenda a usar o banco de dados embarcado key-value com trees, transacoes, watches, compare-and-swap, serializacao com serde e exemplos praticos de cache persistente.

A crate sled e um banco de dados embarcado key-value escrito inteiramente em Rust, projetado para ser rapido, confiavel e facil de usar. Diferente de bancos como PostgreSQL ou MySQL que rodam como servicos separados, o sled roda dentro do seu processo – sem configuracao de servidor, sem conexoes de rede, sem dependencias externas.

O sled utiliza uma arquitetura moderna baseada em B+ trees com log-structured storage, oferecendo operacoes atomicas, transacoes, watches (notificacoes de mudanca), e compactacao automatica. E ideal para aplicacoes que precisam de armazenamento persistente local: caches, indices, filas de trabalho, configuracoes, e qualquer cenario onde SQLite seria usado mas uma API key-value e suficiente.

Instalação

Adicione ao seu Cargo.toml:

[dependencies]
sled = "0.34"

Para serializar valores complexos:

[dependencies]
sled = "0.34"
serde = { version = "1", features = ["derive"] }
bincode = "1"

Uso Básico

Abrindo um Banco de Dados

use sled::Db;

fn main() -> sled::Result<()> {
    // Abrir (ou criar) banco de dados em um diretorio
    let db: Db = sled::open("meu_banco")?;

    // Inserir dados
    db.insert("chave1", "valor1")?;
    db.insert("chave2", b"valor em bytes")?;

    // Buscar dados
    if let Some(valor) = db.get("chave1")? {
        println!("chave1 = {}", String::from_utf8_lossy(&valor));
    }

    // Verificar existencia
    println!("chave1 existe? {}", db.contains_key("chave1")?);
    println!("chave3 existe? {}", db.contains_key("chave3")?);

    // Remover dados
    let removido = db.remove("chave1")?;
    println!("Valor removido: {:?}", removido.map(|v| String::from_utf8_lossy(&v).to_string()));

    // Contar entradas
    println!("Entradas no banco: {}", db.len());

    // Flush para garantir persistencia (sled faz flush periodico automaticamente)
    db.flush()?;

    Ok(())
}

Trabalhando com Tipos Numéricos

O sled trabalha com bytes (&[u8]), mas oferece helpers para inteiros:

use sled::IVec;

fn main() -> sled::Result<()> {
    let db = sled::open("/tmp/sled_numeros")?;

    // Armazenar inteiros como big-endian (para ordenacao correta)
    let id: u64 = 42;
    db.insert("contador", &id.to_be_bytes())?;

    // Recuperar inteiro
    if let Some(valor) = db.get("contador")? {
        let numero = u64::from_be_bytes(valor.as_ref().try_into().unwrap());
        println!("Contador: {}", numero);
    }

    // Incremento atomico com fetch_and_update
    db.insert("visitas", &0u64.to_be_bytes())?;

    for _ in 0..10 {
        db.fetch_and_update("visitas", |old| {
            let numero = match old {
                Some(bytes) => {
                    let arr: [u8; 8] = bytes.try_into().unwrap();
                    u64::from_be_bytes(arr)
                }
                None => 0,
            };
            Some((numero + 1).to_be_bytes().to_vec())
        })?;
    }

    if let Some(valor) = db.get("visitas")? {
        let visitas = u64::from_be_bytes(valor.as_ref().try_into().unwrap());
        println!("Total de visitas: {}", visitas);
    }

    let _ = std::fs::remove_dir_all("/tmp/sled_numeros");
    Ok(())
}

Iteração sobre Entradas

fn main() -> sled::Result<()> {
    let db = sled::open("/tmp/sled_iter")?;

    // Inserir dados
    for i in 0..20u32 {
        let chave = format!("usuario:{:04}", i);
        let valor = format!("Usuario #{}", i);
        db.insert(chave.as_bytes(), valor.as_bytes())?;
    }

    // Iterar sobre todas as entradas (ordenadas lexicograficamente)
    println!("=== Todas as entradas ===");
    for resultado in db.iter() {
        let (chave, valor) = resultado?;
        println!(
            "  {} = {}",
            String::from_utf8_lossy(&chave),
            String::from_utf8_lossy(&valor)
        );
    }

    // Iterar em ordem reversa
    println!("\n=== Ultimas 5 ===");
    for resultado in db.iter().rev().take(5) {
        let (chave, valor) = resultado?;
        println!(
            "  {} = {}",
            String::from_utf8_lossy(&chave),
            String::from_utf8_lossy(&valor)
        );
    }

    // Range scan (prefixo)
    println!("\n=== Usuarios 005-009 ===");
    for resultado in db.range("usuario:0005".."usuario:0010") {
        let (chave, valor) = resultado?;
        println!(
            "  {} = {}",
            String::from_utf8_lossy(&chave),
            String::from_utf8_lossy(&valor)
        );
    }

    // Scan por prefixo
    println!("\n=== Prefixo 'usuario:001' ===");
    for resultado in db.scan_prefix("usuario:001") {
        let (chave, valor) = resultado?;
        println!(
            "  {} = {}",
            String::from_utf8_lossy(&chave),
            String::from_utf8_lossy(&valor)
        );
    }

    let _ = std::fs::remove_dir_all("/tmp/sled_iter");
    Ok(())
}

Recursos Avançados

Trees (Namespaces/Tabelas)

Trees sao como tabelas ou namespaces dentro do mesmo banco:

fn main() -> sled::Result<()> {
    let db = sled::open("/tmp/sled_trees")?;

    // Abrir (ou criar) trees nomeadas
    let usuarios = db.open_tree("usuarios")?;
    let sessoes = db.open_tree("sessoes")?;
    let configs = db.open_tree("configs")?;

    // Cada tree e independente
    usuarios.insert("1", b"Maria Silva")?;
    usuarios.insert("2", b"Joao Santos")?;

    sessoes.insert("sess_abc123", b"usuario:1")?;
    sessoes.insert("sess_def456", b"usuario:2")?;

    configs.insert("app_nome", b"Minha App")?;
    configs.insert("app_versao", b"1.0.0")?;

    // Listar trees existentes
    println!("Trees: {:?}", db.tree_names());

    // Cada tree tem seu proprio contador
    println!("Usuarios: {}", usuarios.len());
    println!("Sessoes: {}", sessoes.len());
    println!("Configs: {}", configs.len());

    // Limpar uma tree sem afetar outras
    sessoes.clear()?;
    println!("Sessoes apos limpar: {}", sessoes.len());
    println!("Usuarios mantidos: {}", usuarios.len());

    // Remover uma tree completamente
    db.drop_tree("sessoes")?;

    let _ = std::fs::remove_dir_all("/tmp/sled_trees");
    Ok(())
}

Transações

use sled::transaction::{ConflictableTransactionError, TransactionError};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let db = sled::open("/tmp/sled_transacoes")?;
    let contas = db.open_tree("contas")?;

    // Configurar saldos iniciais
    contas.insert("alice", &1000u64.to_be_bytes())?;
    contas.insert("bob", &500u64.to_be_bytes())?;

    // Transferencia atomica com transacao
    let resultado = contas.transaction(|tx| {
        let saldo_alice = match tx.get("alice")? {
            Some(bytes) => u64::from_be_bytes(bytes.as_ref().try_into().unwrap()),
            None => return Err(ConflictableTransactionError::Abort("Alice nao encontrada")),
        };

        let saldo_bob = match tx.get("bob")? {
            Some(bytes) => u64::from_be_bytes(bytes.as_ref().try_into().unwrap()),
            None => return Err(ConflictableTransactionError::Abort("Bob nao encontrado")),
        };

        let valor_transferencia = 200u64;

        if saldo_alice < valor_transferencia {
            return Err(ConflictableTransactionError::Abort("Saldo insuficiente"));
        }

        // Ambas as operacoes sao atomicas
        tx.insert("alice", &(saldo_alice - valor_transferencia).to_be_bytes())?;
        tx.insert("bob", &(saldo_bob + valor_transferencia).to_be_bytes())?;

        Ok(valor_transferencia)
    });

    match resultado {
        Ok(valor) => println!("Transferencia de {} realizada!", valor),
        Err(TransactionError::Abort(msg)) => println!("Transferencia abortada: {}", msg),
        Err(TransactionError::Storage(e)) => println!("Erro de storage: {}", e),
    }

    // Verificar saldos
    let saldo_alice = u64::from_be_bytes(
        contas.get("alice")?.unwrap().as_ref().try_into().unwrap(),
    );
    let saldo_bob = u64::from_be_bytes(
        contas.get("bob")?.unwrap().as_ref().try_into().unwrap(),
    );
    println!("Alice: {}, Bob: {}", saldo_alice, saldo_bob);

    // Transacao multi-tree
    let pedidos = db.open_tree("pedidos")?;
    let estoque = db.open_tree("estoque")?;

    estoque.insert("notebook", &10u32.to_be_bytes())?;

    let resultado = (&contas, &pedidos, &estoque)
        .transaction(|(tx_contas, tx_pedidos, tx_estoque)| {
            let saldo = match tx_contas.get("alice")? {
                Some(b) => u64::from_be_bytes(b.as_ref().try_into().unwrap()),
                None => return Err(ConflictableTransactionError::Abort("Conta nao encontrada")),
            };

            let qtd_estoque = match tx_estoque.get("notebook")? {
                Some(b) => u32::from_be_bytes(b.as_ref().try_into().unwrap()),
                None => return Err(ConflictableTransactionError::Abort("Produto nao encontrado")),
            };

            let preco = 500u64;

            if saldo < preco {
                return Err(ConflictableTransactionError::Abort("Saldo insuficiente"));
            }
            if qtd_estoque == 0 {
                return Err(ConflictableTransactionError::Abort("Sem estoque"));
            }

            tx_contas.insert("alice", &(saldo - preco).to_be_bytes())?;
            tx_estoque.insert("notebook", &(qtd_estoque - 1).to_be_bytes())?;
            tx_pedidos.insert("pedido:1", b"alice:notebook:1")?;

            Ok(())
        });

    match resultado {
        Ok(()) => println!("Pedido criado com sucesso!"),
        Err(e) => println!("Erro no pedido: {:?}", e),
    }

    let _ = std::fs::remove_dir_all("/tmp/sled_transacoes");
    Ok(())
}

Compare-and-Swap (CAS)

fn main() -> sled::Result<()> {
    let db = sled::open("/tmp/sled_cas")?;

    db.insert("versao", b"1")?;

    // Compare-and-swap: atualiza apenas se o valor atual e o esperado
    let resultado = db.compare_and_swap(
        "versao",
        Some(b"1" as &[u8]),  // valor esperado (atual)
        Some(b"2" as &[u8]),  // novo valor
    )?;

    match resultado {
        Ok(()) => println!("CAS bem-sucedido: versao atualizada para 2"),
        Err(erro) => {
            println!(
                "CAS falhou: valor atual e {:?}, esperado era 'Some(1)'",
                erro.current.map(|v| String::from_utf8_lossy(&v).to_string())
            );
        }
    }

    // CAS para criar apenas se nao existir
    let resultado = db.compare_and_swap(
        "novo_campo",
        None::<&[u8]>,        // espera que NAO exista
        Some(b"criado" as &[u8]),
    )?;

    match resultado {
        Ok(()) => println!("Campo criado com sucesso"),
        Err(_) => println!("Campo ja existia"),
    }

    // CAS para deletar condicionalmente
    let resultado = db.compare_and_swap(
        "versao",
        Some(b"2" as &[u8]),  // espera que seja "2"
        None::<&[u8]>,        // deleta
    )?;

    match resultado {
        Ok(()) => println!("Campo removido"),
        Err(_) => println!("Valor mudou, nao removido"),
    }

    let _ = std::fs::remove_dir_all("/tmp/sled_cas");
    Ok(())
}

Watches (Observadores de Mudança)

use std::thread;
use std::time::Duration;

fn main() -> sled::Result<()> {
    let db = sled::open("/tmp/sled_watch")?;

    // Observar mudancas em um prefixo
    let mut subscriber = db.watch_prefix("evento:");

    // Thread que faz mudancas
    let db_clone = db.clone();
    let writer = thread::spawn(move || {
        for i in 0..5 {
            thread::sleep(Duration::from_millis(100));
            db_clone
                .insert(
                    format!("evento:{}", i).as_bytes(),
                    format!("dados do evento {}", i).as_bytes(),
                )
                .unwrap();
            println!("[writer] Inseriu evento:{}", i);
        }
    });

    // Thread que observa mudancas
    let watcher = thread::spawn(move || {
        let mut contagem = 0;
        while contagem < 5 {
            // Bloqueia ate receber uma notificacao
            if let Some(evento) = (&mut subscriber).next() {
                for (chave, _) in evento.iter() {
                    println!(
                        "[watcher] Mudanca detectada: {}",
                        String::from_utf8_lossy(chave)
                    );
                    contagem += 1;
                }
            }
        }
        println!("[watcher] Todas as mudancas recebidas!");
    });

    writer.join().unwrap();
    watcher.join().unwrap();

    let _ = std::fs::remove_dir_all("/tmp/sled_watch");
    Ok(())
}

Serialização com Serde

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Produto {
    id: u64,
    nome: String,
    preco: f64,
    estoque: u32,
    categorias: Vec<String>,
    ativo: bool,
}

struct ProdutoStore {
    tree: sled::Tree,
}

impl ProdutoStore {
    fn novo(db: &sled::Db) -> sled::Result<Self> {
        Ok(ProdutoStore {
            tree: db.open_tree("produtos")?,
        })
    }

    fn salvar(&self, produto: &Produto) -> Result<(), Box<dyn std::error::Error>> {
        let chave = format!("prod:{:08}", produto.id);
        let valor = bincode::serialize(produto)?;
        self.tree.insert(chave.as_bytes(), valor)?;
        Ok(())
    }

    fn buscar(&self, id: u64) -> Result<Option<Produto>, Box<dyn std::error::Error>> {
        let chave = format!("prod:{:08}", id);
        match self.tree.get(chave.as_bytes())? {
            Some(bytes) => {
                let produto: Produto = bincode::deserialize(&bytes)?;
                Ok(Some(produto))
            }
            None => Ok(None),
        }
    }

    fn listar(&self) -> Result<Vec<Produto>, Box<dyn std::error::Error>> {
        let mut produtos = Vec::new();
        for resultado in self.tree.iter() {
            let (_, valor) = resultado?;
            let produto: Produto = bincode::deserialize(&valor)?;
            produtos.push(produto);
        }
        Ok(produtos)
    }

    fn buscar_por_categoria(&self, categoria: &str) -> Result<Vec<Produto>, Box<dyn std::error::Error>> {
        let todos = self.listar()?;
        Ok(todos
            .into_iter()
            .filter(|p| p.categorias.iter().any(|c| c == categoria))
            .collect())
    }

    fn remover(&self, id: u64) -> sled::Result<bool> {
        let chave = format!("prod:{:08}", id);
        Ok(self.tree.remove(chave.as_bytes())?.is_some())
    }

    fn atualizar_estoque(&self, id: u64, quantidade: i32) -> Result<Option<Produto>, Box<dyn std::error::Error>> {
        let chave = format!("prod:{:08}", id);
        let resultado = self.tree.fetch_and_update(chave.as_bytes(), |old| {
            old.and_then(|bytes| {
                let mut produto: Produto = bincode::deserialize(bytes).ok()?;
                let novo_estoque = produto.estoque as i32 + quantidade;
                if novo_estoque < 0 {
                    return None; // Nao permite estoque negativo
                }
                produto.estoque = novo_estoque as u32;
                bincode::serialize(&produto).ok()
            })
        })?;

        match resultado {
            Some(bytes) => Ok(Some(bincode::deserialize(&bytes)?)),
            None => Ok(None),
        }
    }

    fn contagem(&self) -> usize {
        self.tree.len()
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let db = sled::open("/tmp/sled_serde")?;
    let store = ProdutoStore::novo(&db)?;

    // Inserir produtos
    let produtos = vec![
        Produto {
            id: 1,
            nome: "Notebook Dell".to_string(),
            preco: 4599.90,
            estoque: 15,
            categorias: vec!["eletronicos".to_string(), "computadores".to_string()],
            ativo: true,
        },
        Produto {
            id: 2,
            nome: "Mouse Logitech".to_string(),
            preco: 189.90,
            estoque: 50,
            categorias: vec!["eletronicos".to_string(), "perifericos".to_string()],
            ativo: true,
        },
        Produto {
            id: 3,
            nome: "Cadeira Gamer".to_string(),
            preco: 1299.00,
            estoque: 8,
            categorias: vec!["moveis".to_string(), "escritorio".to_string()],
            ativo: true,
        },
        Produto {
            id: 4,
            nome: "Teclado Mecanico".to_string(),
            preco: 459.00,
            estoque: 30,
            categorias: vec!["eletronicos".to_string(), "perifericos".to_string()],
            ativo: true,
        },
    ];

    for produto in &produtos {
        store.salvar(produto)?;
    }
    println!("Produtos salvos: {}\n", store.contagem());

    // Buscar por ID
    if let Some(produto) = store.buscar(1)? {
        println!("Produto #1: {} - R${:.2}", produto.nome, produto.preco);
    }

    // Listar todos
    println!("\n=== Todos os Produtos ===");
    for produto in store.listar()? {
        println!(
            "  #{}: {} - R${:.2} (estoque: {})",
            produto.id, produto.nome, produto.preco, produto.estoque
        );
    }

    // Buscar por categoria
    println!("\n=== Perifericos ===");
    for produto in store.buscar_por_categoria("perifericos")? {
        println!("  {} - R${:.2}", produto.nome, produto.preco);
    }

    // Atualizar estoque
    println!("\n=== Atualizando Estoque ===");
    store.atualizar_estoque(1, -3)?; // Vender 3
    store.atualizar_estoque(2, 10)?; // Receber 10

    if let Some(p) = store.buscar(1)? {
        println!("Notebook estoque: {}", p.estoque); // 12
    }
    if let Some(p) = store.buscar(2)? {
        println!("Mouse estoque: {}", p.estoque); // 60
    }

    let _ = std::fs::remove_dir_all("/tmp/sled_serde");
    Ok(())
}

Configuração do Banco

fn main() -> sled::Result<()> {
    let config = sled::Config::new()
        .path("/tmp/sled_config")
        .cache_capacity(1024 * 1024 * 64) // 64 MB de cache
        .mode(sled::Mode::HighThroughput)  // Otimizar para throughput
        .flush_every_ms(Some(1000))        // Flush a cada 1 segundo
        .temporary(false);                  // Persistente (true = deletar ao dropar)

    let db = config.open()?;

    db.insert("teste", "funciona")?;
    println!("Banco configurado e funcionando!");

    // Banco temporario (auto-delete)
    let db_temp = sled::Config::new()
        .temporary(true)
        .open()?;

    db_temp.insert("temp", "sera deletado")?;
    // Banco sera deletado quando db_temp for dropado

    let _ = std::fs::remove_dir_all("/tmp/sled_config");
    Ok(())
}

Boas Práticas

1. Use Prefixos para Organizar Chaves

fn main() -> sled::Result<()> {
    let db = sled::open("/tmp/sled_prefixos")?;

    // Padrao: tipo:id
    db.insert("usuario:1", b"Maria")?;
    db.insert("usuario:2", b"Joao")?;
    db.insert("pedido:1", b"pedido de Maria")?;
    db.insert("sessao:abc", b"usuario:1")?;

    // Busca eficiente por tipo
    println!("Usuarios:");
    for r in db.scan_prefix("usuario:") {
        let (k, v) = r?;
        println!("  {} = {}", String::from_utf8_lossy(&k), String::from_utf8_lossy(&v));
    }

    // Ou use Trees separadas para melhor isolamento
    let _ = std::fs::remove_dir_all("/tmp/sled_prefixos");
    Ok(())
}

2. Serialize com Bincode para Performance

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Dados {
    campo: String,
    valor: u64,
}

fn salvar(tree: &sled::Tree, chave: &str, dados: &Dados) -> Result<(), Box<dyn std::error::Error>> {
    // BOM: bincode e compacto e rapido
    let bytes = bincode::serialize(dados)?;
    tree.insert(chave, bytes)?;
    Ok(())
}

fn carregar(tree: &sled::Tree, chave: &str) -> Result<Option<Dados>, Box<dyn std::error::Error>> {
    match tree.get(chave)? {
        Some(bytes) => Ok(Some(bincode::deserialize(&bytes)?)),
        None => Ok(None),
    }
}

3. Use Chaves Numéricas com Big-Endian

fn main() -> sled::Result<()> {
    let db = sled::open("/tmp/sled_be")?;

    // BIG-ENDIAN para ordenacao correta
    for id in [1u64, 10, 100, 2, 20, 200] {
        db.insert(&id.to_be_bytes(), format!("item {}", id).as_bytes())?;
    }

    // Iteracao retorna em ordem numerica correta
    println!("Ordem correta (big-endian):");
    for r in db.iter() {
        let (k, v) = r?;
        let id = u64::from_be_bytes(k.as_ref().try_into().unwrap());
        println!("  {} = {}", id, String::from_utf8_lossy(&v));
    }
    // 1, 2, 10, 20, 100, 200

    let _ = std::fs::remove_dir_all("/tmp/sled_be");
    Ok(())
}

4. Flush Antes de Encerrar

fn main() -> sled::Result<()> {
    let db = sled::open("/tmp/sled_flush")?;

    db.insert("importante", b"dados criticos")?;

    // sled faz flush periodico, mas para garantir:
    db.flush()?;

    // Ou use flush_async em contextos async
    // db.flush_async().await?;

    let _ = std::fs::remove_dir_all("/tmp/sled_flush");
    Ok(())
}

5. Trate Erros de Corrupção

fn abrir_banco_seguro(caminho: &str) -> Result<sled::Db, Box<dyn std::error::Error>> {
    match sled::open(caminho) {
        Ok(db) => {
            // Verificar integridade basica
            let _ = db.len(); // Tenta ler
            Ok(db)
        }
        Err(e) => {
            eprintln!("Erro ao abrir banco: {}", e);
            eprintln!("Tentando recuperar...");

            // Backup do banco corrompido
            let backup = format!("{}.bak.{}", caminho, std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH).unwrap().as_secs());
            if std::fs::rename(caminho, &backup).is_ok() {
                eprintln!("Banco corrompido movido para: {}", backup);
            }

            // Criar banco novo
            let db = sled::open(caminho)?;
            Ok(db)
        }
    }
}

Exemplos Práticos

Exemplo Completo: Cache Persistente

use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime, UNIX_EPOCH};

#[derive(Debug, Serialize, Deserialize)]
struct EntradaCache<T: Serialize> {
    valor: T,
    criado_em: u64,     // timestamp epoch seconds
    ttl_segundos: u64,  // time-to-live
    acessos: u64,       // contador de acessos
}

struct CachePersistente {
    tree: sled::Tree,
    db: sled::Db,
}

impl CachePersistente {
    fn novo(caminho: &str) -> Result<Self, Box<dyn std::error::Error>> {
        let db = sled::open(caminho)?;
        let tree = db.open_tree("cache")?;
        Ok(CachePersistente { tree, db })
    }

    fn agora_epoch() -> u64 {
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs()
    }

    fn inserir<T: Serialize>(
        &self,
        chave: &str,
        valor: &T,
        ttl: Duration,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let entrada = EntradaCache {
            valor: bincode::serialize(valor)?,
            criado_em: Self::agora_epoch(),
            ttl_segundos: ttl.as_secs(),
            acessos: 0,
        };
        let bytes = bincode::serialize(&entrada)?;
        self.tree.insert(chave.as_bytes(), bytes)?;
        Ok(())
    }

    fn obter<T: for<'de> Deserialize<'de>>(
        &self,
        chave: &str,
    ) -> Result<Option<T>, Box<dyn std::error::Error>> {
        match self.tree.get(chave.as_bytes())? {
            Some(bytes) => {
                let mut entrada: EntradaCache<Vec<u8>> = bincode::deserialize(&bytes)?;

                // Verificar TTL
                let agora = Self::agora_epoch();
                if agora - entrada.criado_em > entrada.ttl_segundos {
                    // Expirado - remover
                    self.tree.remove(chave.as_bytes())?;
                    return Ok(None);
                }

                // Incrementar contador de acessos
                entrada.acessos += 1;
                let bytes_atualizado = bincode::serialize(&entrada)?;
                self.tree.insert(chave.as_bytes(), bytes_atualizado)?;

                // Deserializar valor
                let valor: T = bincode::deserialize(&entrada.valor)?;
                Ok(Some(valor))
            }
            None => Ok(None),
        }
    }

    fn invalidar(&self, chave: &str) -> sled::Result<bool> {
        Ok(self.tree.remove(chave.as_bytes())?.is_some())
    }

    fn invalidar_prefixo(&self, prefixo: &str) -> sled::Result<u64> {
        let mut removidos = 0;
        let chaves: Vec<sled::IVec> = self
            .tree
            .scan_prefix(prefixo.as_bytes())
            .filter_map(|r| r.ok())
            .map(|(k, _)| k)
            .collect();

        for chave in chaves {
            self.tree.remove(chave)?;
            removidos += 1;
        }
        Ok(removidos)
    }

    fn limpar_expirados(&self) -> Result<u64, Box<dyn std::error::Error>> {
        let agora = Self::agora_epoch();
        let mut removidos = 0;

        let entradas: Vec<(sled::IVec, sled::IVec)> = self
            .tree
            .iter()
            .filter_map(|r| r.ok())
            .collect();

        for (chave, valor) in entradas {
            if let Ok(entrada) = bincode::deserialize::<EntradaCache<Vec<u8>>>(&valor) {
                if agora - entrada.criado_em > entrada.ttl_segundos {
                    self.tree.remove(chave)?;
                    removidos += 1;
                }
            }
        }

        Ok(removidos)
    }

    fn estatisticas(&self) -> Result<CacheStats, Box<dyn std::error::Error>> {
        let mut total = 0u64;
        let mut expirados = 0u64;
        let mut total_acessos = 0u64;
        let mut tamanho_bytes = 0u64;
        let agora = Self::agora_epoch();

        for resultado in self.tree.iter() {
            let (chave, valor) = resultado?;
            total += 1;
            tamanho_bytes += chave.len() as u64 + valor.len() as u64;

            if let Ok(entrada) = bincode::deserialize::<EntradaCache<Vec<u8>>>(&valor) {
                total_acessos += entrada.acessos;
                if agora - entrada.criado_em > entrada.ttl_segundos {
                    expirados += 1;
                }
            }
        }

        Ok(CacheStats {
            total_entradas: total,
            expirados,
            total_acessos,
            tamanho_bytes,
        })
    }

    fn flush(&self) -> sled::Result<()> {
        self.db.flush()?;
        Ok(())
    }
}

#[derive(Debug)]
struct CacheStats {
    total_entradas: u64,
    expirados: u64,
    total_acessos: u64,
    tamanho_bytes: u64,
}

impl std::fmt::Display for CacheStats {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Entradas: {} | Expirados: {} | Acessos: {} | Tamanho: {} bytes",
            self.total_entradas, self.expirados, self.total_acessos, self.tamanho_bytes
        )
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cache = CachePersistente::novo("/tmp/sled_cache")?;

    println!("=== Cache Persistente com Sled ===\n");

    // Inserir dados com diferentes TTLs
    cache.inserir("api:users:1", &"Maria Silva".to_string(), Duration::from_secs(3600))?;
    cache.inserir("api:users:2", &"Joao Santos".to_string(), Duration::from_secs(3600))?;
    cache.inserir("api:config", &vec!["pt-BR", "en-US"], Duration::from_secs(86400))?;
    cache.inserir("temp:sessao:abc", &42u64, Duration::from_secs(1))?; // Expira em 1s

    // Buscar dados
    if let Some(nome) = cache.obter::<String>("api:users:1")? {
        println!("Usuario 1: {}", nome);
    }

    if let Some(idiomas) = cache.obter::<Vec<&str>>("api:config")? {
        println!("Idiomas: {:?}", idiomas);
    }

    // Acessar multiplas vezes para incrementar contador
    for _ in 0..5 {
        let _ = cache.obter::<String>("api:users:1")?;
    }

    // Estatisticas
    let stats = cache.estatisticas()?;
    println!("\nEstatisticas: {}", stats);

    // Esperar TTL expirar
    std::thread::sleep(Duration::from_secs(2));

    // Tentar obter dado expirado
    match cache.obter::<u64>("temp:sessao:abc")? {
        Some(_) => println!("Sessao ainda ativa"),
        None => println!("\nSessao expirada (TTL 1s)"),
    }

    // Limpar expirados
    let removidos = cache.limpar_expirados()?;
    println!("Entradas expiradas removidas: {}", removidos);

    // Invalidar por prefixo
    let removidos = cache.invalidar_prefixo("api:users:")?;
    println!("Entradas de usuarios removidas: {}", removidos);

    // Estatisticas finais
    let stats = cache.estatisticas()?;
    println!("\nEstatisticas finais: {}", stats);

    // Flush e limpar
    cache.flush()?;
    let _ = std::fs::remove_dir_all("/tmp/sled_cache");

    Ok(())
}

Comparação com Alternativas

BancoTipoAPITransacoesAsyncMelhor para
sledKV embarcadoRust nativoSimNao (sync)Cache, indices, filas
rusqliteSQL embarcadoSQLSimNaoDados relacionais locais
SQLx + SQLiteSQL embarcadoSQL asyncSimSimDados relacionais async
RocksDBKV embarcadoC++ bindingsSimNaoAlto throughput
LMDBKV embarcadoC bindingsSimNaoLeitura intensiva
redbKV embarcadoRust nativoSimNaoAlternativa ao sled

sled e ideal quando voce quer um banco embarcado 100% Rust, sem dependencias de C/C++, com API simples de key-value. Para dados relacionais, use rusqlite ou SQLx. Para throughput maximo, considere RocksDB. redb e uma alternativa mais recente ao sled.

Conclusão

O sled oferece uma solucao elegante e performatica para armazenamento persistente em Rust. Com sua API intuitiva de key-value, suporte a transacoes atomicas, trees para organizacao, watches para reatividade, e zero dependencias externas, ele e perfeito para cache persistente, indices, filas de trabalho e qualquer cenario que precise de dados locais rapidos e confiaveis.

Lembre-se de usar prefixos ou trees para organizar chaves, serializar com bincode para eficiencia, usar big-endian para chaves numericas, e fazer flush antes de encerrar a aplicacao.

Proximos passos:

  • Explore serde para serializar tipos complexos no sled
  • Veja tokio para integrar sled com aplicacoes async (usando spawn_blocking)
  • Compare com rusqlite para cenarios que precisam de SQL