【问题标题】:In memory database design内存数据库设计
【发布时间】:2016-11-28 12:22:46
【问题描述】:

我正在尝试使用HashMap 创建一个内存数据库。我有一个结构 Person:

struct Person {
    id: i64,
    name: String,
}

impl Person {
    pub fn new(id: i64, name: &str) -> Person {
        Person {
            id: id,
            name: name.to_string(),
        }
    }

    pub fn set_name(&mut self, name: &str) {
        self.name = name.to_string();
    }
}

我有 struct Database:

use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;

struct Database {
    db: Arc<Mutex<HashMap<i64, Person>>>,
}

impl Database {
    pub fn new() -> Database {
        Database {
            db: Arc::new(Mutex::new(HashMap::new())),
        }
    }

    pub fn add_person(&mut self, id: i64, person: Person) {
        self.db.lock().unwrap().insert(id, person);
    }

    pub fn get_person(&self, id: i64) -> Option<&mut Person> {
        self.db.lock().unwrap().get_mut(&id)
    }
}

以及使用该数据库的代码:

let mut db = Database::new();
db.add_person(1, Person::new(1, "Bob"));

我想更改person的名字:

let mut person = db.get_person(1).unwrap();
person.set_name("Bill");

complete code in the Rust playground

编译时,我遇到了 Rust 生命周期的问题:

error[E0597]: borrowed value does not live long enough
  --> src/main.rs:39:9
   |
39 |         self.db.lock().unwrap().get_mut(&id)
   |         ^^^^^^^^^^^^^^^^^^^^^^^ temporary value does not live long enough
40 |     }
   |     - temporary value only lives until here
   |
note: borrowed value must be valid for the anonymous lifetime #1 defined on the method body at 38:5...
  --> src/main.rs:38:5
   |
38 | /     pub fn get_person(&self, id: i64) -> Option<&mut Person> {
39 | |         self.db.lock().unwrap().get_mut(&id)
40 | |     }
   | |_____^

如何实现这种方法?

【问题讨论】:

    标签: rust


    【解决方案1】:

    编译器会拒绝您的代码,因为它违反了 Rust 强制执行的正确性模型并可能导致崩溃。一方面,如果允许编译get_person(),则可能会从两个线程调用它并在没有互斥锁保护的情况下修改底层对象,从而导致内部String 对象的数据竞争。更糟糕的是,即使在单线程场景中,通过执行以下操作也可能造成严重破坏:

    let mut ref1 = db.get_person(1).unwrap();
    let mut ref2 = db.get_person(1).unwrap();
    // ERROR - two mutable references to the same object!
    
    let vec: Vec<Person> = vec![];
    vec.push(*ref1);  // move referenced object to the vector
    println!(*ref2);  // CRASH - object already moved
    

    要更正代码,您需要调整设计以满足以下约束:

    • 不允许任何引用超过被引用对象;
    • 在可变引用的生命周期内,不得存在对该对象的其他引用(可变或不可变)。

    add_person 方法已经符合这两个规则,因为它会吃掉您传递给它的对象,并将其移动到数据库中。

    如果我们修改 get_person() 以返回不可变引用会怎样?

    pub fn get_person(&self, id: i64) -> Option<&Person> {
        self.db.lock().unwrap().get(&id)
    }
    

    即使这个看似无辜的版本仍然无法编译!那是因为它违反了第一条规则。 Rust 不能静态地证明引用不会比数据库本身寿命长,因为数据库是在堆上分配的并且是引用计数的,所以它可以随时被删除。但是,即使有可能以某种方式显式声明引用的生命周期可能无法超过数据库,在解锁互斥锁后保留引用将允许数据竞争。根本没有办法实现get_person() 并仍然保持线程安全。

    读取的线程安全实现可以选择返回数据的副本。 Person 可以实现clone() 方法,get_person() 可以这样调用它:

    #[derive(Clone)]
    struct Person {
        id: i64,
        name: String
    }
    // ...
    
    pub fn get_person(&self, id: i64) -> Option<Person> {
        self.db.lock().unwrap().get(&id).cloned()
    }
    

    这种更改不适用于get_person() 的其他用例,该方法用于获取可变引用以更改数据库中的人员的明确目的。获取对共享资源的可变引用违反了第二条规则,并可能导致如上所示的崩溃。有几种方法可以使其安全。一种是在数据库中提供一个代理来设置每个Person 字段:

    pub fn set_person_name(&self, id: i64, new_name: String) -> bool {
        match self.db.lock().unwrap().get_mut(&id) {
            Some(mut person) => {
                person.name = new_name;
                true
            }
            None => false
        }
    }
    

    随着Person 上字段数量的增加,这很快就会变得乏味。它也可能会变慢,因为每次访问都必须获取单独的互斥锁。

    幸运的是,有一种更好的方法来实现条目的修改。请记住,使用可变引用违反了规则除非Rust 可以证明引用不会“逃脱”正在使用它的块。这可以通过反转控件来确保——而不是返回可变引用的get_person(),我们可以引入modify_person(),将可变引用传递到可调用对象,它可以做任何喜欢的事情用它。例如:

    pub fn modify_person<F>(&self, id: i64, f: F) where F: FnOnce(Option<&mut Person>) {
        f(self.db.lock().unwrap().get_mut(&id))
    }
    

    用法如下所示:

    fn main() {
        let mut db = Database::new();
    
        db.add_person(1, Person::new(1, "Bob"));
        assert!(db.get_person(1).unwrap().name == "Bob");
    
        db.modify_person(1, |person| {
            person.unwrap().set_name("Bill");
        });
    }
    

    最后,如果您担心get_person() 克隆Person 的唯一原因是检查它的性能,那么创建modify_person 的不可变版本作为非复制替代方案是微不足道的get_person():

    pub fn read_person<F, R>(&self, id: i64, f: F) -> R
        where F: FnOnce(Option<&Person>) -> R {
        f(self.db.lock().unwrap().get(&id))
    }
    

    除了对Person 进行共享引用之外,read_person 还允许闭包在选择时返回一个值,通常是从它接收的对象中获取的值。它的用法类似于modify_person 的用法,但增加了返回值的可能性:

    // if Person had an "age" field, we could obtain it like this:
    let person_age = db.read_person(1, |person| person.unwrap().age);
    
    // equivalent to the copying definition of db.get_person():
    let person_copy = db.read_person(1, |person| person.cloned());
    

    【讨论】:

      【解决方案2】:

      这篇文章使用well explained answer 中引用为“控制反转”的模式,并添加 only 糖来演示内存中数据库的另一个 api。

      使用宏规则可以像这样公开一个 db 客户端 api:

      fn main() {
          let db = Database::new();
      
          let person_id = 1234;
      
          // probably not the best design choice to duplicate the person_id,
          // for the purpose here is not important 
          db.add_person(person_id, Person::new(person_id, "Bob"));
      
          db_update!(db #person_id => set_name("Gambadilegno"));
      
          println!("your new name is {}",  db.get_person(person_id).unwrap().name);
      }
      

      我自以为是的宏格式如下:

      <database_instance> #<object_key> => <method_name>(<args>)
      

      下面是宏实现和完整的演示代码:

      use std::collections::HashMap;
      use std::sync::Arc;
      use std::sync::Mutex;
      
      macro_rules! db_update {
          ($db:ident # $id:expr => $meth:tt($($args:tt)*)) => {
              $db.modify_person($id, |person| {
                  person.unwrap().$meth($($args)*);
              });
          };
      }
      
      #[derive(Clone)]
      struct Person {
          id: u64,
          name: String,
      }
      
      impl Person {
          pub fn new(id: u64, name: &str) -> Person {
              Person {
                  id: id,
                  name: name.to_string(),
              }
          }
      
          fn set_name(&mut self, value: &str) {
              self.name = value.to_string();
          }
      }
      
      struct Database {
          db: Arc<Mutex<HashMap<u64, Person>>>, // access from different threads
      }
      
      impl Database {
          pub fn new() -> Database {
              Database {
                  db: Arc::new(Mutex::new(HashMap::new())),
              }
          }
      
          pub fn add_person(&self, id: u64, person: Person) {
              self.db.lock().unwrap().insert(id, person);
          }
      
          pub fn modify_person<F>(&self, id: u64, f: F)
          where
              F: FnOnce(Option<&mut Person>),
          {
              f(self.db.lock().unwrap().get_mut(&id));
          }
      
          pub fn get_person(&self, id: u64) -> Option<Person> {
              self.db.lock().unwrap().get(&id).cloned()
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2011-09-30
        • 2011-11-15
        • 1970-01-01
        • 2010-09-22
        • 2013-09-25
        • 2017-02-20
        • 1970-01-01
        相关资源
        最近更新 更多