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
| Banco | Tipo | API | Transacoes | Async | Melhor para |
|---|---|---|---|---|---|
sled | KV embarcado | Rust nativo | Sim | Nao (sync) | Cache, indices, filas |
rusqlite | SQL embarcado | SQL | Sim | Nao | Dados relacionais locais |
SQLx + SQLite | SQL embarcado | SQL async | Sim | Sim | Dados relacionais async |
RocksDB | KV embarcado | C++ bindings | Sim | Nao | Alto throughput |
LMDB | KV embarcado | C bindings | Sim | Nao | Leitura intensiva |
redb | KV embarcado | Rust nativo | Sim | Nao | Alternativa 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: