【Rust嵌入式键值对数据库redb】第一课 第一次写入
第一个程序:
use redb::{Database, TableDefinition, ReadableDatabase};const TABLE: TableDefinition<&str, u64> = TableDefinition::new("my_data");fn main() -> Result<(), Box<dyn std::error::Error>> {// 1) 创建或打开数据库文件let db = Database::create("target/demo.redb")?;// 2) 写事务let write_txn = db.begin_write()?;{let mut table = write_txn.open_table(TABLE)?;table.insert("alice", 1)?;table.insert("bob", 2)?;}write_txn.commit()?;// 3) 读事务let read_txn = db.begin_read()?; // 来自 ReadableDatabase traitlet table = read_txn.open_table(TABLE)?;if let Some(v) = table.get("alice")? {println!("alice = {}", v.value());}for item in table.range("a"..="z")? {let (k, v) = item?;println!("{} -> {}", k.value(), v.value());}Ok(())
}
输出:
alice = 1
alice -> 1
bob -> 2
redb是一个单写入多读取的数据库。用官方文档的话说:
Multiple reads may be performed concurrently, with each other, and with writes. Only a single write may be in progress at a time.
用Casey Rodarmor的话说:
(Result type vs mmap)
Nope, unfortunately not. The issue is that if the db file is being concurrently modified by another process, that would introduce undefined behavior.redb
cannot detect this, so it must be unsafe, indicating that the caller must ensure that the db is not concurrently modified.
因此,程序员首先得确保同一时间仅开启一个写入线程。
当然,redb本身也做了一定得保证,比如在实验中至少发现以下两点:
1、Database::create会对于一个demo.redb进行上锁,再次对其运行Database::create则会报错:
Error: DatabaseAlreadyOpen
2、多个线程begin_write试图同时写时,会阻塞。以下面程序为例。
use redb::{Database, TableDefinition};
use std::sync::Arc;
use std::thread;
use std::time::Duration;const TABLE: TableDefinition<&str, u64> = TableDefinition::new("my_data");fn main() -> Result<(), Box<dyn std::error::Error>> {std::fs::create_dir_all("target")?;let db = Arc::new(Database::create(r"D:\git\redb\target\demo.redb")?);// 第一个写事务let db1 = Arc::clone(&db);let handle1 = thread::spawn(move || {let write_txn = db1.begin_write().expect("第一个写事务失败");println!("[线程1] 第一个写事务开始,睡 5 秒...");thread::sleep(Duration::from_secs(5));{let mut table = write_txn.open_table(TABLE).unwrap();table.insert("alice", 1).unwrap();}write_txn.commit().unwrap();println!("[线程1] 第一个写事务提交完成");});// 第二个写事务let db2 = Arc::clone(&db);let handle2 = thread::spawn(move || {println!("[线程2] 尝试开始第二个写事务...");let txn = db2.begin_write().expect("[线程2] 写事务失败");println!("[线程2] 第二个写事务获得锁,开始写入");{let mut table = txn.open_table(TABLE).unwrap();table.insert("bob", 2).unwrap();}txn.commit().unwrap();println!("[线程2] 第二个写事务提交完成");});handle1.join().unwrap();handle2.join().unwrap();Ok(())
}
结果如下
[线程1] 第一个写事务开始,睡 5 秒…
[线程2] 尝试开始第二个写事务…
[线程1] 第一个写事务提交完成
[线程2] 第二个写事务获得锁,开始写入
[线程2] 第二个写事务提交完成
至于上锁和阻塞是如何实现的,我们下节课继续