Cómo usar genéricos en Rust, resumen de objetos de rasgos y kv Sever (3)

Se puede decir que en el desarrollo de Rust, la programación genérica es una habilidad que debemos dominar. Cuando construyes cada estructura o función de datos, es mejor preguntarte: **¿Necesito arreglar el tipo en este momento? **¿Es posible posponer esta decisión lo más tarde posible para dejar espacio para el futuro? Si podemos diferir las decisiones a través de genéricos, la arquitectura del sistema puede ser lo suficientemente flexible para afrontar mejor los cambios futuros.

Anteriormente aprendimos que los rasgos pueden lograr polimorfismo paramétrico, lo que significa que las funciones o estructuras de datos están representadas por T, no por tipos específicos;
también pueden implementar polimorfismo ad hoc, es decir, sobrecarga de funciones. Los diferentes parámetros de una interfaz de función tienen diferentes implementaciones;
Aún más asombroso Lo bueno es que cuando los rasgos se usan como parámetros, se pueden implementar restricciones de características y restricciones múltiples, lo que refleja la idea de que la combinación es mayor que la herencia. En C ++, es herencia múltiple. Un ejemplo de servidor KV es utilizar el rasgo que implementa la tienda como parámetro genérico para implementar el enlace retrasado. La misma estructura de datos tiene diferentes implementaciones del mismo rasgo. Por ejemplo, si se agrega un nuevo tipo de almacenamiento a kv en el futuro, se agregará el nuevo tipo de almacenamiento. Al implementar rasgos, la implementación es diferente. En C++, la clase base se usa como parámetro, lo que equivale a asignar la subclase a la clase base. Sin embargo, esto es polimorfismo en tiempo de ejecución y tiene la sobrecarga de una tabla de funciones virtuales en tiempo de ejecución, mientras que generics Se distribuye estáticamente, se ejecuta rápido, pero se compila lentamente. Los

parámetros son rasgos genéricos.
Tres escenarios de uso de parámetros genéricos
. Utilice parámetros genéricos para retrasar la vinculación de estructuras de datos;
utilice parámetros genéricos y PhantomData para declarar que no hay estructuras de datos en la estructura de datos. Tipos que se usan directamente pero que deben usarse durante la implementación;
el uso de parámetros genéricos permite que la misma estructura de datos tenga diferentes implementaciones del mismo rasgo.

Uso de parámetros genéricos para el enlace retrasado
Veamos primero algo con lo que ya estamos familiarizados: el uso de parámetros genéricos para el enlace retrasado. Almacenar en el servidor kv es un parámetro genérico que puede restringirse gradualmente en implementaciones posteriores. Siempre que se implemente el rasgo de almacenamiento, se puede utilizar como parámetro.

pub fn despacho(cmd: CommandRequest, tienda: &impl Almacenamiento) -> CommandResponse {… }
// 等价于
pub fn despacho<Tienda: Almacenamiento>(cmd: CommandRequest, tienda: &Store) -> CommandResponse {… }

Proporcionar tipos adicionales utilizando parámetros genéricos y datos fantasma.
Ahora diseñemos una estructura de datos de Usuario y Producto, los cuales tienen una identificación de tipo u64. Sin embargo, espero que la identificación de cada estructura de datos solo se pueda comparar con identificaciones del mismo tipo, es decir, si se comparan user.id y product.id, el compilador puede informar directamente un error y rechazar este comportamiento.
Primero use un Identificador de estructura de datos personalizado para representar el ID:
pub struct Identifier { internal: u64,}
Luego, en Usuario y Producto, cada uno usa el Identificador para vincular el Identificador a su propio tipo, de modo que diferentes tipos de ID no puedan compararse.

#[derive(Debug, Default, PartialEq, Eq)]
pub struct User {
    
     id: Identifier<Self>}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Product {
    
     id:Identifier<Self>}

Sin embargo, no se puede compilar. ¿por qué? Debido a que Identifier no utiliza el parámetro genérico T cuando se define, el compilador considera que T es redundante, por lo que solo se puede compilar eliminando T. Sin embargo, si se elimina T, los ID de Usuario y Producto se pueden comparar y no podremos lograr la función deseada ¿Qué debemos hacer?
PhantomData generalmente se traduce como datos fantasma en chino. Este nombre tiene un encanto maligno que hace que la gente tenga miedo de acercarse a él, pero se usa ampliamente en el procesamiento. Parámetros genéricos que no son necesarios en el proceso de definición de la estructura de datos pero sí en el proceso de implementación.

#[derive(Debug, Default, PartialEq, Eq)]
pub struct Identifier 
{
    
     inner: u64,
 _tag: PhantomData,}

Eso es todo. De hecho, PhantomData es tal como su nombre. En realidad tiene una longitud de cero y es un ZST (Zero-Sized Type), es como si no existiera. Su única función es marcar el tipo.
Por ejemplo, la estructura de usuario tiene nombre, identificación y un parámetro genérico T propiedad de datos fantasma, que representan usuarios gratuitos o usuarios pagos. Los usuarios gratuitos pueden convertirse en usuarios pagos a través del método de suscripción y acceder a él. El uso de PhantomData para manejar dicho estado puede detectar el estado durante el período de compilación, evitando la carga de la detección en tiempo de ejecución y posibles errores.
Los genéricos se distribuyen estáticamente y los códigos de tipo correspondientes se asignan durante la compilación.

Utilice parámetros genéricos para proporcionar múltiples implementaciones.
A veces, para el mismo rasgo, queremos tener diferentes implementaciones, ¿qué debemos hacer? Por ejemplo, una ecuación puede ser una ecuación lineal o una ecuación cuadrática. Esperamos implementar diferentes iteradores para diferentes tipos.

#[derive(Debug, Default)]
pub struct Equation 
{
    
     current: u32, 
_method: PhantomData,
}// 线性增长
#[derive(Debug, Default)]
pub struct Linear;// 二次增长
#[derive(Debug, Default)]
pub struct Quadratic;
impl Iterator for Equation
 {
    
     type Item = u32;
  fn next(&mut self) -> Option {
    
     self.current += 1; if self.current >= u32::MAX {
    
     return None; } Some(self.current) }}
  impl Iterator for Equation
   {
    
    
   type Item = u32; 
   fn next(&mut self) -> Option {
    
     self.current += 1; if self.current >= u16::MAX as u32 {
    
     return None; } Some(self.current * self.current) }}

¿Hay algún beneficio al hacer esto? ¿Por qué no construir dos estructuras de datos LinearEquation y QuadraticEquation e implementar Iterator respectivamente?
De hecho, para este ejemplo, usar genéricos no tiene mucho sentido porque la ecuación en sí no comparte mucho código. Pero si Equation es diferente excepto por la lógica de implementación de Iterator, una gran cantidad de otros códigos son iguales y, en el futuro, además de ecuaciones lineales y ecuaciones cuadráticas, también admitirá ecuaciones cúbicas, cuadráticas ..., entonces use una estructura de datos genérica . Es muy necesario unificar la misma lógica y utilizar tipos específicos de parámetros genéricos para manejar la lógica cambiante.
Similar al significado de herencia en C++, Equation es la clase base y tiene el método iterador. LinearEquation y QuadraticEquation son subclases que implementan específicamente el iterador.

Situación adicional:
¿Qué debo hacer si el valor de retorno lleva parámetros genéricos?
Por ejemplo, para el método get_iter(), no nos importa qué tipo de iterador sea el valor de retorno, siempre que nos permita llamar continuamente al método next() y obtener una estructura Kvpair.
Actualmente, Rust no admite el uso del rasgo implícito como valor de retorno en un rasgo. ¿Qué hacer? Es muy simple, podemos devolver un objeto de rasgo, lo que elimina las diferencias de tipos y unifica todos los tipos diferentes que implementan Iterator bajo el mismo objeto de rasgo.

pub trait Storage {
    
    
    ...
    /// 遍历 HashTable,返回 kv pair 的 Iterator
    fn get_iter(&self, table: &str) -> 
        Result<Box<dyn Iterator<Item = Kvpair>>, KvError>;
}

Por supuesto, la programación genérica también es un arma de doble filo. Cada vez que introduzcamos la abstracción, incluso si podemos lograr una abstracción de costo cero, recordemos que la abstracción en sí misma es un costo . Cuando abstraemos código en funciones y estructuras de datos en estructuras genéricas, incluso si casi no hay costos adicionales en tiempo de ejecución, seguirá generando costos en tiempo de diseño . Si la abstracción no se hace bien, generará costos aún mayores .

总结一下trait:
1、特征约束。比如store trait作为参数,下次添加新的类型根本不需要修改代码,只需要在为新的存储类型添加新的方法,开闭原则;并且如果C++必须要用多态运行时,有开销,而且组合优于继承(破坏封装,方法污染,耦合强,多重继承的问题)

2、还可以有幽灵数据,数据结构定义过程中不需要,但是在实现过程中需要的泛型参数。用泛型数据结构来统一相同的逻辑,用泛型参数的具体类型来处理变化的逻辑,就非常有必要了。也是用来替代C++的继承的;

3、特殊情况就是返回值不能是trait,比如返回一个迭代器,不需要知道它的具体类型,可以用特征对象,根C++的多态很像了。最好不用吧。

Cómo se usa el objeto de rasgo en el combate real
Insertar descripción de la imagen aquí
Este ejemplo muestra que el objeto de rasgo usa el rasgo &dyn para representar el tipo real como un tipo de nivel superior que implementa el rasgo. Similar a C++, hay una tabla que contiene la implementación del rasgo por el tipo específico. Es equivalente al polimorfismo, que degenera la subclase en una clase base, pero la clase base tiene una tabla de funciones virtuales que almacena la implementación del tipo real del rasgo.
La pregunta aquí es que, como se mencionó anteriormente, el polimorfismo ad hoc se puede lograr usando rasgos como parámetros, lo cual es similar a pasar parámetros genéricos, pero la implementación es diferente según los diferentes tipos reales. ¿Por qué necesitamos objetos característicos?
1. Principalmente por la conveniencia de la programación, buena lógica y legibilidad del código. Por ejemplo, si queremos implementar un componente UI con diferentes elementos (botones, cuadros de texto, etc., estos componentes tienen métodos de dibujo). ¡Todos existen en una tabla vec y deben representarse en la pantalla uno por uno usando el mismo método! Si se utiliza una restricción de característica genérica, entonces toda la lista debe ser del mismo tipo. Para resolver todos los problemas anteriores, Rust introduce un concepto: objeto característico. Implementación de caja. Cualquier tipo que implemente la función Dibujar se puede almacenar en vec.
Los objetos de rasgo pueden simplificar el código, porque no es necesario tomar parámetros genéricos al implementarlo.
2. El segundo es cuando se necesitan valores de retorno genéricos. Actualmente, Rust no admite el uso del rasgo implícito como valor de retorno en un rasgo. ¿Qué hacer? Es simple, podemos devolver el objeto de rasgo. Por ejemplo, al devolver un iterador, todos los tipos diferentes que implementan Iterator se unifican bajo el mismo objeto de rasgo.

En resumen:
cuando el sistema necesita usar polimorfismo para resolver requisitos complejos y cambiantes, de modo que la misma interfaz pueda mostrar diferentes comportamientos , tenemos que decidir si es mejor la distribución estática en el momento de la compilación (parámetros genéricos) o el tiempo de ejecución. La distribución dinámica es mejor. (objetos característicos).
La distribución dinámica tendrá una sobrecarga de tiempo de ejecución, pero hará que el código sea más conciso, especialmente cuando necesita abstraer diferentes tipos específicos, como formar diferentes componentes en una colección de componentes de UI, necesita un objeto característico, porque las matrices solo pueden contener elementos de del mismo tipo. ¿Se pueden considerar tuplas? Y cuando el valor de retorno contiene genéricos, se debe usar un objeto de rasgo, porque actualmente Rust no admite el rasgo implícito como valor de retorno.
La distribución estática es una abstracción de costo cero, pero el costo de diseño está ahí. Si es simple, puede usarla. Si es demasiado complicada, involucra múltiples características y la conexión es complicada, no necesita considerarla. .

Consejos:
Sabemos que & dyn draw, Box y Arc se pueden usar como objetos característicos. Pero, ¿cuánto más gastos generales tiene la distribución dinámica en comparación con la distribución estática?
Si es & dyn draw (ptr y vptr asignados en la pila), en realidad es solo un acceso de memoria más a la vtable y el impacto no es grande. Box, Arc y Arc asignan una memoria de montón más, lo que tiene un gran impacto y hace que la velocidad sea decenas de veces más lenta.
Por lo tanto, en la mayoría de los casos, no necesitamos prestar demasiada atención a los problemas de rendimiento de los objetos de rasgo al escribir código. Si realmente le importa el rendimiento de los objetos de rasgo en la ruta crítica, intente primero ver si puede hacerlo sin realizar una asignación adicional de memoria dinámica.
(Para valores de retorno (el objeto de pila se destruirá al final de la función) y para transferencias entre subprocesos (se debe implementar el rasgo de envío), se debe usar Box, Arc)

¿Cómo diseñar y diseñar un sistema en torno a rasgos?
De hecho, no sólo los rasgos en Rust, sino también los conceptos relacionados con el procesamiento de interfaces en cualquier lenguaje son los conceptos más importantes en el uso de ese lenguaje. Básicamente, se puede decir que todo el comportamiento del desarrollo de software es el proceso de crear e iterar continuamente interfaces y luego implementarlas en estas interfaces.

Construyendo un servidor KV simple - habilidades avanzadas de rasgo
Hemos completado las funciones básicas de la tienda KV, pero dejamos dos pequeñas colas:
el método get_iter() del rasgo de almacenamiento no se ha implementado;
todavía hay algunos TODO en ejecutar() método del Servicio Notificación de eventos que necesitan ser manejados.
Almacenamiento persistente completo


impl Storage for MemTable {
    
    
    ...
    fn get_iter(&self, table: &str) -> Result<Box<dyn Iterator<Item = Kvpair>>, KvError> {
    
    
        // 使用 clone() 来获取 table 的 snapshot
        let table = self.get_or_create_table(table).clone();
        let iter = table
            .iter()
            .map(|v| Kvpair::new(v.key(), v.value().clone()));
        Ok(Box::new(iter)) // <-- 编译出错
    }
}

table.iter() usa una referencia a la tabla y devolvemos iter, pero iter hace referencia a la tabla como una variable local, por lo que no se puede compilar. En este momento, necesitamos un iterador que pueda ocupar completamente la tabla. Podemos usar table.into_iter() para transferir la propiedad de la tabla a iter: let iter = table.into_iter().map(|data| data.into ());
(Cadena, Valor) debe convertirse en Kvpair, todavía usamos into() para lograr esto.
Todavía tenemos que pensar en ello, si queremos implementar el rasgo Almacenamiento para más almacenes de datos en el futuro, ¿cómo manejaremos el método get_iter()?

Nosotros:
obtendremos un iterador propio bajo una tabla determinada,
mapearemos el iterador y
convertiremos cada elemento mapeado en un par Kv.

El paso 2 aquí es el mismo para cada implementación del método get_iter() del rasgo de almacenamiento. ¿Es posible encapsularlo, es decir, implementar un rasgo storeiterator que contenga la operación del mapa, de modo que siempre que otros tipos de almacenamiento necesiten implementar get_iter, se pueda omitir el segundo paso? De hecho, en el caso del servidor KV, dicha abstracción tiene pocos beneficios. Sin embargo, si los pasos de ahora no son 3 pasos, sino 5 pasos/10 pasos, y una gran cantidad de pasos son iguales, es decir, cada vez que implementamos una nueva tienda, tenemos que escribir el mismo código. Lógicamente, entonces, esta Abstracción es muy necesaria.

Notificación de evento de soporte


pub fn execute(&self, cmd: CommandRequest) -> CommandResponse {
    
    
    debug!("Got request: {:?}", cmd);
    // TODO: 发送 on_received 事件
    let res = dispatch(cmd, &self.inner.store);
    debug!("Executed response: {:?}", res);
    // TODO: 发送 on_executed 事件

    res
}

Para resolver estos TODO, necesitamos proporcionar un mecanismo de notificación de eventos: al crear un Servicio, registre la función de procesamiento de eventos correspondiente; cuando se ejecute el método ejecutar (), realice la notificación de evento correspondiente para que la función de procesamiento de eventos registrada pueda ser ejecutado.
Primero veamos cómo registrar la función del controlador de eventos.
Si desea poder registrarse, en otras palabras, la estructura de datos Service/ServiceInner debe tener un lugar para albergar la función de registro de eventos. Puedes intentar agregarlo a la estructura ServiceInner:


/// Service 内部数据结构
pub struct ServiceInner<Store> {
    
    
    store: Store,
    on_received: Vec<fn(&CommandRequest)>,
    on_executed: Vec<fn(&CommandResponse)>,
    on_before_send: Vec<fn(&mut CommandResponse)>,
    on_after_send: Vec<fn()>,
}

Antes de escribir el código para el registro de eventos, es mejor escribir primero una prueba y considerar cómo registrarse desde la perspectiva del usuario.


#[test]
fn event_registration_should_work() {
    
    
    fn b(cmd: &CommandRequest) {
    
    
        info!("Got {:?}", cmd);
    }
    fn c(res: &CommandResponse) {
    
    
        info!("{:?}", res);
    }
    fn d(res: &mut CommandResponse) {
    
    
        res.status = StatusCode::CREATED.as_u16() as _;
    }
    fn e() {
    
    
        info!("Data is sent");
    }

    let service: Service = ServiceInner::new(MemTable::default())
        .fn_received(|_: &CommandRequest| {
    
    })
        .fn_received(b)
        .fn_executed(c)
        .fn_before_send(d)
        .fn_after_send(e)
        .into();

    let res = service.execute(CommandRequest::new_hset("t1", "k1", "v1".into()));
    assert_eq!(res.status, StatusCode::CREATED.as_u16() as _);
    assert_eq!(res.message, "");
    assert_eq!(res.values, vec![Value::default()]);
}

Como se puede ver en el código de prueba, esperamos llamar continuamente al método fn_xxx a través de la estructura ServiceInner para registrar la función de procesamiento de eventos correspondiente para ServiceInner; una vez completada la adición, convertimos ServiceInner en Service a través del método into (). Este es un patrón de construcción clásico , que se puede ver en muchos códigos de Rust. (Para construir un objeto complejo, solo necesita pasar los parámetros a la clase de construcción y no necesita preocuparse por cómo construirlo. Al usar privado para modificar el método de construcción, el mundo exterior no puede crear el objeto directamente , y solo puede usar el método de construcción de la clase Builder interna; el proceso de construcción del objeto está oculto. ; Métodos expuestos a llamadas externas y utilizados para construir componentes.)


impl<Store: Storage> ServiceInner<Store> {
    
    
    pub fn new(store: Store) -> Self {
    
    
        Self {
    
    
            store,
            on_received: Vec::new(),
            on_executed: Vec::new(),
            on_before_send: Vec::new(),
            on_after_send: Vec::new(),
        }
    }

    pub fn fn_received(mut self, f: fn(&CommandRequest)) -> Self {
    
    
        self.on_received.push(f);
        self
    }

    pub fn fn_executed(mut self, f: fn(&CommandResponse)) -> Self {
    
    
        self.on_executed.push(f);
        self
    }

    pub fn fn_before_send(mut self, f: fn(&mut CommandResponse)) -> Self {
    
    
        self.on_before_send.push(f);
        self
    }

    pub fn fn_after_send(mut self, f: fn()) -> Self {
    
    
        self.on_after_send.push(f);
        self
    }
}

Aunque hemos completado el registro de la función de controlador de eventos, aún no hemos enviado la notificación del evento. Además, debido a que nuestros eventos incluyen eventos inmutables (como on_received) y eventos variables (como on_before_send), la notificación de eventos debe separar los dos. Para definir dos rasgos: Notificar y NotificarMut:

/// 事件通知(不可变事件)
pub trait Notify<Arg> {
    
    
    fn notify(&self, arg: &Arg);
}

/// 事件通知(可变事件)
pub trait NotifyMut<Arg> {
    
    
    fn notify(&self, arg: &mut Arg);
}



impl<Arg> Notify<Arg> for Vec<fn(&Arg)> {
    
    
    #[inline]
    fn notify(&self, arg: &Arg) {
    
    
        for f in self {
    
    
            f(arg)
        }
    }
}

impl<Arg> NotifyMut<Arg> for Vec<fn(&mut Arg)> {
    
    
  #[inline]
    fn notify(&self, arg: &mut Arg) {
    
    
        for f in self {
    
    
            f(arg)
        }
    }
}

El parámetro Arg corresponde al arg en la función de registro de eventos, como por ejemplo: fn(&CommandRequest);

Una vez implementado el rasgo Notify/NotifyMut, podemos modificar el método ejecutar():


impl<Store: Storage> Service<Store> {
    
    
    pub fn execute(&self, cmd: CommandRequest) -> CommandResponse {
    
    
        debug!("Got request: {:?}", cmd);
        self.inner.on_received.notify(&cmd);
        let mut res = dispatch(cmd, &self.inner.store);
        debug!("Executed response: {:?}", res);
        self.inner.on_executed.notify(&res);
        self.inner.on_before_send.notify(&mut res);
        if !self.inner.on_before_send.is_empty() {
    
    
            debug!("Modified response: {:?}", res);
        }

        res
    }
}

Ahora, el evento correspondiente se puede notificar a la función de controlador correspondiente. Este mecanismo de notificación es actualmente una llamada a función sincrónica. Si es necesario en el futuro, podemos cambiarlo para pasar mensajes para el procesamiento asincrónico.
Solo que todo el proceso consiste en agregar una matriz para responder al evento en la estructura del servicio. El elemento de la matriz es la función que se ejecutará. Debido a que es una matriz, puede usar el modo constructor al construir el servicio y llamar continuamente al Método fn_xxx. Este método consiste en agregar la función a ejecutar. La función agrega el evento de respuesta correspondiente a la matriz. Luego ejecute notificar en algún lugar, ejecute la función en la matriz y notifique.
Tenga en cuenta que la razón por la que se utiliza una matriz para un determinado punto de activación aquí también es para cumplir con el principio de apertura y cierre, porque, por ejemplo, después de recibir una solicitud, las cosas que se deben hacer o las notificaciones cambian con la demanda y pueden aumentar. o disminuir. Si solo usa la forma de una función y ejecuta directamente la implementación correspondiente en fn_xxx. Ante cambios, se debe modificar el código, lo que no cumple con el principio de apertura y cierre.

Ahora piense en los lugares donde todo nuestro proyecto satisface el principio de apertura y cierre:
1. Los rasgos genéricos se utilizan para lograr efectos polimórficos similares a los de C ++. No es necesario modificar el código para nuevos tipos de almacenamiento en el futuro, siempre y cuando a medida que se implementan los rasgos para los nuevos tipos. Y los genéricos son distribución estática, abstracción de costo cero
2. Para el método get_iter, el parámetro de retorno usa un objeto característico y no le importa el tipo específico, siempre que satisfaga la implementación del rasgo del iterador. un efecto similar al polimorfismo, que es distribución dinámica. ; Además, cuando analizamos diferentes tipos de iteradores, el segundo paso es mapear el iterador, por lo que encapsulamos este paso e implementamos el iterador de almacenamiento, de modo que cuando haya un nuevo tipo de
almacenamiento , no es necesario repetir el segundo paso, especialmente cuando esta operación común tiene muchos pasos, es más significativo.
3. El mecanismo de notificación de eventos aquí no es una función única que maneja eventos. Debido a que el proceso de procesamiento puede cambiar, se configura una matriz de procesamiento. Durante la construcción, puede usar el modo constructor para llamar continuamente a fn_xxx para agregar funciones a la matriz de procesamiento. Cuando se necesita notificación, notifique Ejecutar funciones en una matriz secuencialmente.
4. El código de prueba para probar la parte de la tienda también cumple con el principio de apertura y cierre. La interfaz es estable. Los rasgos genéricos se utilizan como parámetros de la interfaz. Cuando hay nuevas pruebas de tipo de almacenamiento, no es necesario modificar el código.

其他的可以不说,主要说一下事件通知机制(**构造者模式**):不是单独一个处理事件的函数,因为处理过程可能变化,所以设置了处理数组,构造时可以利用构造者模式不断调用fn_xxx往处理数组中添加函数,需要通知的时候notify顺序执行数组中的函数。

Resumen: En realidad, hay dos puntos que deben comprenderse aquí: uno es la serie general de procesos y servicios de tres partes, y luego el uso de rasgos y la implementación del patrón constructor del mecanismo de notificación de eventos; (comuníquese con el patrón de diseño)

A continuación, también necesita conocer la implementación de la parte de procesamiento de la red y la parte de seguridad de la red; así como la implementación de la programación asincrónica.

Implementación del rasgo de almacenamiento para una base de datos persistente
Hasta ahora, nuestro almacén KV ha sido un almacén KV en memoria. Una vez que finaliza la aplicación, todas las claves/valores almacenados por el usuario desaparecerán. Queremos que el almacenamiento sea duradero.
Una solución es agregar WAL y compatibilidad con instantáneas de disco a MemTable, de modo que todos los comandos de actualización enviados por los usuarios se almacenen en el disco en orden y se tomen instantáneas con regularidad para facilitar la recuperación rápida de los datos; otra solución es utilizar el almacén KV existente
. , como RocksDB o sled. RocksDB es una tienda KV integrada desarrollada por Facebook basada en el nivel DB de Google, escrita en C++, y sled es una excelente tienda KV que está surgiendo en la comunidad Rust y se compara con RocksDB. Las funciones de los dos son muy similares. Desde la perspectiva de la demostración, sled es más simple de usar y más adecuado para el contenido actual. Si se usa en un entorno de producción, RocksDB es más adecuado porque ha sido templado en varios entornos de producción complejos. Por lo tanto, hoy intentaremos implementar el rasgo de Almacenamiento para sled para que pueda adaptarse a nuestro servidor KV.
Primero introduzca sled en Cargo.toml: sled = "0.34" # sled db
Luego cree src/storage/sleddb.rs y agregue el siguiente código:


use sled::{
    
    Db, IVec};
use std::{
    
    convert::TryInto, path::Path, str};

use crate::{
    
    KvError, Kvpair, Storage, StorageIter, Value};

#[derive(Debug)]
pub struct SledDb(Db);

impl SledDb {
    
    
    pub fn new(path: impl AsRef<Path>) -> Self {
    
    
        Self(sled::open(path).unwrap())
    }

    // 在 sleddb 里,因为它可以 scan_prefix,我们用 prefix
    // 来模拟一个 table。当然,还可以用其它方案。
    fn get_full_key(table: &str, key: &str) -> String {
    
    
        format!("{}:{}", table, key)
    }

    // 遍历 table 的 key 时,我们直接把 prefix: 当成 table
    fn get_table_prefix(table: &str) -> String {
    
    
        format!("{}:", table)
    }
}

/// 把 Option<Result<T, E>> flip 成 Result<Option<T>, E>
/// 从这个函数里,你可以看到函数式编程的优雅
fn flip<T, E>(x: Option<Result<T, E>>) -> Result<Option<T>, E> {
    
    
    x.map_or(Ok(None), |v| v.map(Some))
}

impl Storage for SledDb {
    
    
    fn get(&self, table: &str, key: &str) -> Result<Option<Value>, KvError> {
    
    
        let name = SledDb::get_full_key(table, key);
        let result = self.0.get(name.as_bytes())?.map(|v| v.as_ref().try_into());
        flip(result)
    }

    fn set(&self, table: &str, key: String, value: Value) -> Result<Option<Value>, KvError> {
    
    
        let name = SledDb::get_full_key(table, &key);
        let data: Vec<u8> = value.try_into()?;

        let result = self.0.insert(name, data)?.map(|v| v.as_ref().try_into());
        flip(result)
    }

    fn contains(&self, table: &str, key: &str) -> Result<bool, KvError> {
    
    
        let name = SledDb::get_full_key(table, &key);

        Ok(self.0.contains_key(name)?)
    }

    fn del(&self, table: &str, key: &str) -> Result<Option<Value>, KvError> {
    
    
        let name = SledDb::get_full_key(table, &key);

        let result = self.0.remove(name)?.map(|v| v.as_ref().try_into());
        flip(result)
    }

    fn get_all(&self, table: &str) -> Result<Vec<Kvpair>, KvError> {
    
    
        let prefix = SledDb::get_table_prefix(table);
        let result = self.0.scan_prefix(prefix).map(|v| v.into()).collect();

        Ok(result)
    }

    fn get_iter(&self, table: &str) -> Result<Box<dyn Iterator<Item = Kvpair>>, KvError> {
    
    
        let prefix = SledDb::get_table_prefix(table);
        let iter = StorageIter::new(self.0.scan_prefix(prefix));
        Ok(Box::new(iter))
    }
}

impl From<Result<(IVec, IVec), sled::Error>> for Kvpair {
    
    
    fn from(v: Result<(IVec, IVec), sled::Error>) -> Self {
    
    
        match v {
    
    
            Ok((k, v)) => match v.as_ref().try_into() {
    
    
                Ok(v) => Kvpair::new(ivec_to_key(k.as_ref()), v),
                Err(_) => Kvpair::default(),
            },
            _ => Kvpair::default(),
        }
    }
}

fn ivec_to_key(ivec: &[u8]) -> &str {
    
    
    let s = str::from_utf8(ivec).unwrap();
    let mut iter = s.split(":");
    iter.next();
    iter.next().unwrap()
}

Este código implementa principalmente el rasgo Almacenamiento. Cada método es muy simple, simplemente agrega una encapsulación a las funciones proporcionadas por sled.
El código de prueba se puede reutilizar, lo que también refleja el principio de apertura y cierre del código de prueba, la interfaz de prueba es estable y la implementación se puede cambiar.


mod sleddb;

pub use sleddb::SledDb;

#[cfg(test)]
mod tests {
    
    
    use tempfile::tempdir;

    use super::*;

    ...

    #[test]
    fn sleddb_basic_interface_should_work() {
    
    
        let dir = tempdir().unwrap();
        let store = SledDb::new(dir);
        test_basi_interface(store);
    }

    #[test]
    fn sleddb_get_all_should_work() {
    
    
        let dir = tempdir().unwrap();
        let store = SledDb::new(dir);
        test_get_all(store);
    }

    #[test]
    fn sleddb_iter_should_work() {
    
    
        let dir = tempdir().unwrap();
        let store = SledDb::new(dir);
        test_get_iter(store);
    }
}

En el código de prueba general que se ejecutó al final, se puede ver que la función principal casi no ha cambiado. El tipo de almacenamiento específico se modificó al construir el servicio y se usó el modo constructor para llamar continuamente al método fn_xxx para presionar. la función notificada en la matriz de eventos. La función de notificación será llamada durante la ejecución.
Además, si desea agregar un evento de notificación, solo necesita llamar a la función fn_xxx una vez más durante la construcción, sin modificar el código; si desea agregar un nuevo tipo de almacenamiento, también puede implementar el rasgo de tienda para ello. sin modificar el código, ejecutar llamará automáticamente a las funciones correspondientes para manejar. (Implica la serialización y deserialización de archivos protobuf, la función de ejecución del comando correspondiente es la lógica de almacenamiento y lectura)


use anyhow::Result;
use async_prost::AsyncProstStream;
use futures::prelude::*;
use kv1::{
    
    CommandRequest, CommandResponse, Service, ServiceInner, SledDb};
use tokio::net::TcpListener;
use tracing::info;

#[tokio::main]
async fn main() -> Result<()> {
    
    
    tracing_subscriber::fmt::init();
    let service: Service<SledDb> = ServiceInner::new(SledDb::new("/tmp/kvserver"))
        .fn_before_send(|res| match res.message.as_ref() {
    
    
            "" => res.message = "altered. Original message is empty.".into(),
            s => res.message = format!("altered: {}", s),
        })
        .into();
    let addr = "127.0.0.1:9527";
    let listener = TcpListener::bind(addr).await?;
    info!("Start listening on {}", addr);
    loop {
    
    
        let (stream, addr) = listener.accept().await?;
        info!("Client {:?} connected", addr);
        let svc = service.clone();
        tokio::spawn(async move {
    
    
            let mut stream =
                AsyncProstStream::<_, CommandRequest, CommandResponse, _>::from(stream).for_async();
            while let Some(Ok(cmd)) = stream.next().await {
    
    
                info!("Got a new command: {:?}", cmd);
                let res = svc.execute(cmd);
                stream.send(res).await.unwrap();
            }
            info!("Client {:?} disconnected", addr);
        });
    }
}

Supongo que te gusta

Origin blog.csdn.net/weixin_53344209/article/details/130099981
Recomendado
Clasificación