Cómo evitar interfaces habladoras

10

Antecedentes: estoy diseñando una aplicación de servidor y creando dll separados para diferentes subsistemas. Para simplificar las cosas, digamos que tengo dos subsistemas: 1) Users2)Projects

La interfaz pública de los usuarios tiene un método como:

IEnumerable<User> GetUser(int id);

Y la interfaz pública de Proyectos tiene un método como:

IEnumerable<User> GetProjectUsers(int projectId);

Entonces, por ejemplo, cuando necesitamos mostrar a los usuarios para un determinado proyecto, podemos llamar GetProjectUsersy eso devolverá objetos con información suficiente para mostrar en una cuadrícula de datos o similar.

Problema: Idealmente, el Projectssubsistema no debería también almacenar información del usuario y solo debería almacenar los ID de los usuarios que participan en un proyecto. Para servir GetProjectUsers, necesita llamar GetUseral Userssistema para cada ID de usuario almacenado en su propia base de datos. Sin embargo, esto requiere muchas GetUserllamadas separadas , lo que genera muchas consultas sql separadas dentro del Usersubsistema. Realmente no he probado esto, pero tener este diseño hablador afectará la escalabilidad del sistema.

Si dejo de lado la separación de los subsistemas, podría almacenar toda la información en un solo esquema accesible por ambos sistemas y Projectspodría simplemente hacer una JOINpara obtener todos los usuarios del proyecto en una sola consulta. Projectstambién necesitaría saber cómo generar Userobjetos a partir de los resultados de la consulta. Pero esto rompe la separación que tiene muchas ventajas.

Pregunta: ¿Alguien puede sugerir una forma de mantener la separación mientras se evitan todas estas GetUserllamadas individuales durante GetProjectUsers?


Por ejemplo, una idea que tuve fue que los usuarios deberían dar a los sistemas externos la capacidad de "etiquetar" a los usuarios con un par de valor de etiqueta y solicitar a los usuarios con un cierto valor, por ejemplo:

void AddUserTag(int userId, string tag, string value);
IEnumerable<User> GetUsersByTag(string tag, string value);

Luego, el sistema Proyectos podría etiquetar a cada usuario a medida que se agregan al proyecto:

AddUserTag(userId,"project id", myProjectId.ToString());

y durante GetProjectUsers, podría solicitar a todos los usuarios del proyecto en una sola llamada:

var projectUsers = usersService.GetUsersByTag("project id", myProjectId.ToString());

la parte de la que no estoy seguro es: sí, los usuarios son agnósticos de los proyectos, pero realmente la información sobre la membresía del proyecto se almacena en el sistema de los usuarios, no en los proyectos. Simplemente no me siento natural, así que estoy tratando de determinar si hay una gran desventaja aquí que me estoy perdiendo.

Eren Ersönmez
fuente

Respuestas:

10

Lo que falta en su sistema es el caché.

Tu dices:

Sin embargo, esto requiere muchas GetUserllamadas separadas , lo que genera muchas consultas sql separadas dentro del Usersubsistema.

El número de llamadas a un método no tiene que ser el mismo que el número de consultas SQL. Obtiene la información sobre el usuario una vez, ¿por qué volvería a solicitar la misma información si no cambiara? Muy probablemente, incluso puede almacenar en caché a todos los usuarios en la memoria, lo que resultaría en cero consultas SQL (a menos que un usuario cambie).

Por otro lado, al hacer que el Projectssubsistema consulte tanto a los proyectos como a los usuarios con un INNER JOIN, introduce un problema adicional: está consultando la misma información en dos ubicaciones diferentes en su código, lo que hace que la invalidación de caché sea extremadamente difícil. Como consecuencia:

  • O no introducirás caché en ningún momento más tarde,

  • O pasará semanas o meses estudiando lo que debe invalidarse cuando cambia una información,

  • O agregará la invalidación de caché en ubicaciones sencillas, olvidando las otras y resultando en errores difíciles de encontrar.


Al releer su pregunta, noté una palabra clave que me perdí la primera vez: escalabilidad . Como regla general, puede seguir el siguiente patrón:

  1. Pregúntese si el sistema es lento (es decir, viola un requisito no funcional de rendimiento o es simplemente una pesadilla de usar).

    Si el sistema no es lento, no se preocupe por el rendimiento. Preocuparse por código limpio, legibilidad, facilidad de mantenimiento, pruebas, cobertura de sucursales, diseño limpio, documentación detallada y fácil de entender, buenos comentarios de código.

  2. En caso afirmativo, busque el cuello de botella. Lo haces no adivinando, sino perfilando . Al crear un perfil, usted determina la ubicación exacta del cuello de botella (dado que cuando adivina , casi siempre puede equivocarse), y ahora puede centrarse en esa parte del código.

  3. Una vez que encuentre el cuello de botella, busque soluciones. Lo hace adivinando, comparando, perfilando, escribiendo alternativas, entendiendo las optimizaciones del compilador, entendiendo las optimizaciones que dependen de usted, haciendo preguntas sobre Stack Overflow y moviéndose a lenguajes de bajo nivel (incluido Assembler, cuando sea necesario).

¿Cuál es el problema real con el Projectssubsistema que solicita información al Userssubsistema?

¿El eventual problema de escalabilidad futura? Esto no es un problema. La escalabilidad puede convertirse en una pesadilla si comienza a fusionar todo en una solución monolítica o consulta los mismos datos desde múltiples ubicaciones (como se explica a continuación, debido a la dificultad de introducir caché).

Si ya hay un problema de rendimiento notable, entonces, paso 2, busque el cuello de botella.

Si parece que, de hecho, existe el cuello de botella y se debe al hecho de que las Projectssolicitudes de los usuarios a través del Userssubsistema (y está situado en el nivel de consulta de la base de datos), solo entonces debe buscar una alternativa.

La alternativa más común sería implementar el almacenamiento en caché, reduciendo drásticamente el número de consultas. Si se encuentra en una situación en la que el almacenamiento en caché no ayuda, un perfil adicional puede mostrarle que necesita reducir el número de consultas, o agregar (o eliminar) índices de base de datos, o lanzar más hardware, o rediseñar completamente todo el sistema .

Arseni Mourzenko
fuente
A menos que te esté malentendiendo, estás diciendo "mantener las llamadas individuales de GetUser, pero usar el almacenamiento en caché para evitar los viajes de ida y vuelta de db".
Eren Ersönmez
@ ErenErsönmez: en GetUserlugar de consultar la base de datos, buscará en la memoria caché. Esto significa que en realidad no importa cuántas veces llame GetUser, ya que cargará datos de la memoria en lugar de la base de datos (a menos que la memoria caché se haya invalidado).
Arseni Mourzenko
esta es una buena sugerencia, dado que no he hecho un buen trabajo destacando el problema principal, que es "deshacerse de la charla sin fusionar sistemas en un solo sistema". Mi ejemplo de usuarios y proyectos, naturalmente, lo llevaría a creer que hay un número relativamente pequeño de usuarios que rara vez cambian. Quizás un mejor ejemplo hubiera sido Documentos y Proyectos. Imagine que tiene un par de millones de documentos, miles se agregan todos los días y el sistema Project usa el sistema Document para almacenar sus documentos. ¿Todavía recomendarías el almacenamiento en caché entonces? Probablemente no, ¿verdad?
Eren Ersönmez
@ ErenErsönmez: cuantos más datos tenga, más caché aparece. Como regla general, compare el número de lecturas con el número de escrituras. Si se agregan "miles" de documentos por día y hay millones de selectconsultas por día, será mejor que use el almacenamiento en caché. Por otro lado, si está agregando miles de millones de entidades a una base de datos pero solo obtiene unos pocos miles de selects con s muy selectivos where, el almacenamiento en caché puede no ser tan útil.
Arseni Mourzenko
probablemente tenga razón, probablemente estoy tratando de solucionar un problema que aún no tengo. Probablemente lo implemente como está e intentaré mejorarlo más tarde si es necesario. Si el almacenamiento en caché no es apropiado porque, por ejemplo, es probable que las entidades se lean solo 1-2 veces después de ser agregadas, ¿cree que la posible solución I que agregué a la pregunta podría funcionar? ¿Ves un gran problema con eso?
Eren Ersönmez