java - comandos - clusvcadm
Administrar el conteo del producto en la base de datos (9)
Disculpe si esta pregunta puede parecer ingenua, pero me he encontrado con un escenario en el que necesito administrar el recuento de productos en la base de datos de una tienda de comercio electrónico.
Hay una clase de Producto con una variable entera productCount
que significa la cantidad de productos disponibles en la base de datos que es visible para los usuarios del sitio. Ahora se accede a esta clase por varios hilos o puede decir varios usuarios del sitio de comercio electrónico. Todos están agregando o quitando el producto a su carrito.
El marco ORM que se está utilizando está hibernado
Código de muestra
@Entity
@Table
class Product{
@Column
private int productCount;
public void addProductToCart(){
// decrements the product count by 1 & updates the database
}
public void removeTheProductFromTheCart(){
// increments the product count by 1 & updates the database
}
Como está claro en el código, necesito mantener una comprobación de concurrencia en el conteo del producto en la base de datos para evitar actualizaciones perdidas.
Además, si varios usuarios intentan agregar solo un único producto a la izquierda en la base de datos. ¿A qué carro del usuario se debe agregar el producto?
Hice una pequeña investigación sobre esto
Las posibles formas que encontré fueron
Creando una clase singleton para Producto. Eso garantizaría que solo una instancia de producto esté disponible en toda la aplicación.
Sincronice los métodos
addProductToCart
yremoveTheProductFromTheCart
. lo que permitiría que solo un hilo actualice el conteo del producto y actualice el db a la vez.Utilice el control de concurrencia de la base de datos para aplicar algún nivel de aislamiento de transacción db, bloqueo optimista / pesimista para
productCount
. Estoy usando mysql, el nivel de aislamiento predeterminado esREPEATABLE_READ
.
¿Cuál sería el mejor enfoque para lidiar con esto?
3. Utilice el control de concurrencia de la base de datos
¿Por qué?
1 y 2 están bien si su aplicación de comercio electrónico es absolutamente la única manera de modificar el conteo del producto. Eso es un gran si. En el curso de hacer negocios y mantener el inventario, la tienda puede necesitar otras formas de actualizar el recuento de productos y la aplicación de comercio electrónico puede no ser la solución ideal. Una base de datos, por otro lado, es generalmente más fácil de enlazar en diferentes aplicaciones que ayudan al proceso de inventario de su tienda.
Los productos de base de datos suelen tener muchos mecanismos a prueba de fallos, de modo que si algo sale mal, puede rastrear qué transacciones tuvieron éxito, cuáles no, y puede retroceder a un punto específico en el tiempo. Un programa java flotando en la memoria no tiene esto fuera de la caja, tendrías que desarrollarlo tú mismo si lo hicieras 1 o 2. Spring e Hibernate y otras cosas así son mejor que nada, pero compara lo que ofrecen y lo que ofrecen una base de datos ofrece en términos de recuperación de algún desastre electrónico.
Evaluemos tres opciones.
1. Crear una clase singleton para Producto. Eso garantizaría que solo una instancia de producto esté disponible en toda la aplicación.
Una sola instancia para el producto está bien. Pero si está ofreciendo un Producto como Mobile con la cantidad 20, igual debe addProductToCart
el recuento de productos (variable estática) en addProductToCart
y decrement
el recuento de productos en removeTheProductFromTheCart
. Aún debe sincronizar el acceso a este recuento mutable O actualizar la base de datos y leer el recuento de productos.
2. Sincronice los métodos addProductToCart y removeTheProductFromTheCart. lo que permitiría que solo un hilo actualice el conteo del producto y actualice el db a la vez.
Esta es una solución, pero prefiero la tercera: eliminar la sincronización en la aplicación y proporcionar coherencia de datos en la capa de la base de datos.
3. Utilice el control de concurrencia de la base de datos para aplicar algún nivel de aislamiento de transacción db, bloqueo optimista / pesimista para productCount. Estoy usando mysql, el nivel de aislamiento predeterminado es REPEATABLE_READ.
Diferir la coherencia de la base de datos en lugar de la aplicación. Pero debe usar READ COMMITTED
para nivel de aislamiento en lugar de REPEATABLE_READ
Eche un vistazo a este artículo
LEER COMPROMETIDO
A somewhat Oracle-like isolation level with respect to consistent (nonlocking) reads
: cada lectura consistente, incluso dentro de la misma transacción, establece y lee su propia instantánea.
Como entendí, los carros también persisten en db? Y como resultado final compró productos también.
producto:
[{id:1, count:100}]
carro:
[{user_id:1, product_id:1}, {user_id:2, product_id:1}]
compró :
[{user_id:3, product_id:1}]
Entonces puedes obtener el conteo del producto
select count as total from product where id = 1
select count(*) as in_cart from cart where product_id = 1
select count(*) as total_bought from bought where product_id = 1
ahora puedes mostrar el recuento final
int count = total - (in_cart + total_bought);
De esta manera, garantiza que no se anularán ni se anularán los incrementos / decrementos. Finalmente, al lado del código de verificación para el recuento, también puede agregar un desencadenador en el nivel de DB que verifica el recuento total y si el producto se puede insertar en los carritos o comprado en la tabla.
Si el recuento de productos cambia a diario, quiero decir que ayer tenía 100 productos y vendió 20, hoy han llegado 50 productos, por lo tanto, debe tener 150 - 20 cuentas vendidas que son 130. Para tener buenos informes, puede hacer que el producto cuente diario. me gusta
[
{id:1, count:100, newly_arrived: 0, date: 23/02/2016},
{id:1, count:80, newly_arrived: 50, date: 24/02/2016}
]
entonces tus consultas cambiarán como
select count+newly_arrived as total from product where id = 1 and date = today
select count(*) as in_cart from cart where product_id = 1 and date = today
select count(*) as total_bought from bought where product_id = 1 and date = today
para esto solo tiene que insertar datos de productos nuevos a la medianoche 00:00:00 y cuando lleguen sus nuevos productos por la mañana, puede actualizarlos recientemente sin intervenir ninguna operación de inserción o eliminación en los carritos o las tablas compradas. y puede tener informes detallados fácilmente sin hacer sofisticadas consultas de informes :))
La forma correcta de hacerlo es utilizar bloqueos de base de datos, ya que está diseñado para este trabajo. Y si está utilizando hibernate, es bastante simple con LockRequest:
Session session = sessionFactory.openSession()
Transaction transaction;
boolean productTaken = false;
try {
transaction = session.beginTransaction();
Product product = session.get(Product.class, id);
if (product == null)
throw ...
Session.LockRequest lockRequest = session.buildLockRequest(LockOptions.UPGRADE);
lockRequest.lock(product);
productTaken = product.take();
if (productTaken) {
session.update(product);
transaction.commit();
}
} finally {
if (transaction != null && transaction.isActive())
transaction.rollback();
session.close();
}
Aquí estamos buscando un producto de la base de datos para actualizar, lo que evita cualquier actualización concurrente.
Si tiene que usar una base de datos relacional para esto (y no un almacén de valores clave, por ejemplo). Recomiendo hacer esto lo más cerca posible del almacenamiento para evitar bloqueos y conflictos y obtener el máximo rendimiento posible.
Por otro lado, suena como un escenario típico en el que varios nodos trabajan en una base de datos, lo que también crea problemas de latencia debido al manejo de la sesión de Hibernate.
Al final necesitas declaraciones como
UPDATE product SET productCount = productCount + 1 WHERE id = ?
Para ser ejecutado en una transacción, puede utilizar una declaración JDBC simple o un método de repositorio JPA con anotación @Query.
Para que Hibernate tenga conocimiento de los cambios, puede usar Session.refresh()
en el producto después de dicha operación.
Esto no es tanto una cuestión técnica como una cuestión de proceso. Personalmente, no reduciría el inventario hasta que alguien realmente haya comprado el producto. Agregar un artículo a un carrito de compras es un indicador de que pueden comprarlo. No es que lo tengan.
¿Por qué no reducir el recuento de inventario solo cuando el pago se ha procesado correctamente? Envuelva todo en una transacción de base de datos. Si 10 personas agregan el mismo artículo a su carrito y solo quedan 1, el primero en pagar gana. 9 personas se molestarán porque no pagaron más rápido.
La referencia de las aerolíneas es diferente. Emiten restricciones temporales, pero es mucho más complicado que administrar el inventario. Eso y están trabajando con un conjunto fluido de asientos con los que se juntan a medida que el inventario escasea.
IMO, un enfoque estratificado convencional ayudaría aquí, no estoy seguro de cuán radical sería este cambio ya que no conozco el tamaño / madurez de la aplicación, pero seguiré describiéndolo de todos modos y podrá elegir qué bits son viables.
La teoría...
Services a.k.a. "business logic", "business rules", "domain logic" etc.
^
DAOs a.k.a. "Data Access Objects", "Data Access Layer", "repository" etc.
^
Entities a.k.a. "model" - the ORM representation of the database structure
^
Database
Es útil que las entidades estén separadas de la capa DAO, por lo que son simples unidades de almacenamiento que puede completar, comparar, etc., sin incluir métodos que actúen sobre ellas. Entonces, estas son solo una representación de clase de lo que está en la base de datos y, idealmente, no deberían estar contaminadas con un código que defina cómo se usarán.
La capa DAO proporciona las operaciones CRUD básicas que permiten persistir, recuperar, fusionar y eliminar estas entidades sin necesidad de conocer el contexto en el que se realiza. Este es un lugar donde los singletons pueden ser útiles para evitar que se creen instancias múltiples una y otra vez, pero el uso de un singleton no implica seguridad de subprocesos. Personalmente, recomendaría usar Spring para hacer esto (los beans de Spring son singletons por defecto), pero supongo que se podría hacer manualmente si se prefiere.
Y la capa de servicios es donde se implementa la "lógica de dominio", es decir, las combinaciones específicas de operaciones que necesita su aplicación para realizar determinadas funciones. Los problemas de seguridad del subproceso se pueden abordar aquí y habrá momentos en los que sea necesario y cuando no lo sea.
En la práctica...
Siguiendo este enfoque, podrías terminar con algo así (muchos omitieron por brevedad):
@Entity
@Table
public class Product {
@ManyToOne
@JoinColumn
private ShoppingCart shoppingCart;
}
@Entity
@Table
public class ShoppingCart {
@OneToOne
@JoinColumn
private User user;
@OneToMany(mappedBy = "shoppingCart")
private Set<Product> products;
}
public class ShoppingCartDao { /* persist, merge, remove, findById etc. */ }
@Transactional
public class ProductService() {
private ConcurrentMap<Integer, Integer> locks =
new ConcurrentHashMap<Integer, Integer>();
public void addProductToCart(final int productId, final int userId) {
ShoppingCart shoppingCart = shoppingCartDao.findByUserId(userId);
Product product = productDao.findById(productId);
synchronized(getCacheSyncObject(productId)) {
if (product.shoppingCart == null) {
product.setShoppingCart(shoppingCart);
} else {
throw new CustomException("Product already reserved.");
}
}
}
public void removeProductFromCart(final int productId, final int userId) {
ShoppingCart shoppingCart = shoppingCartDao.findByUserId(userId);
Product product = productDao.findById(productId);
if (product.getShoppingCart() != shoppingCart) {
throw new CustomException("Product not in specified user''s cart.");
} else {
product.setShoppingCart(null);
}
}
/** @See http://.com/questions/659915#659939 */
private Object getCacheSyncObject(final Integer id) {
locks.putIfAbsent(id, id);
return locks.get(id);
}
}
Para las dos primeras posibilidades que está considerando, aquellas funcionan solo si está limitado a implementar solo una instancia de la aplicación. No puede tener singletons administrados en varias instancias de aplicaciones, no puede tener sincronización en varias JVM. Por lo tanto, si va con uno de estos, nunca podrá implementar varias instancias de su aplicación en un clúster o granja, o incluso dos instancias con un equilibrador de carga al frente. Entonces, ambos parecen indeseables, ya que solo funcionan en el modo de punto único de falla.
El enfoque de obtener los recuentos de productos de la base de datos tiene la ventaja de que sigue siendo válido a medida que su aplicación se amplía en varias instancias.
Puedes pensar que esto solo será una instancia en un servidor, así que puedo salir adelante con esto. Pero en el momento en que está compilando una aplicación, puede que no esté del todo claro cómo se implementará la aplicación (he estado en situaciones en las que no sabíamos cuál era el plan hasta que la aplicación se configuró en un entorno preprod). o en una fecha posterior, puede haber un motivo para cambiar la forma en que se implementa una aplicación; si su aplicación tiene una carga mayor a la esperada, puede ser beneficioso configurar una segunda caja.
Una cosa que no es evidente para mí es cuán vital es que el conteo del producto sea realmente correcto. En diferentes dominios de negocios (boletos de avión, envío) es común sobregirar, y puede ser más problemático que vale la pena mantener un recuento 100% exacto, especialmente si se encuentra en un punto inicial del proceso, como agregar un artículo a las compras. carro (en comparación con el punto donde el cliente realmente se compromete a hacer una compra). En el momento en que el cliente compra algo, puede tener más sentido asegurarse de reservar esos artículos con una transacción de base de datos (o no, consulte sobreventa de nuevo).
Parece común en las aplicaciones web esperar una baja tasa de conversión de los artículos en el carro a los artículos realmente comprados. Tenga en cuenta qué nivel de precisión para sus recuentos es apropiado para su dominio comercial.
La mejor manera de hacerlo en la aplicación web de comercio electrónico es usar Messaging Queue . Puedes usar RabbitMQ ,
Cree una cola en rabbitMQ y conserve todas las solicitudes aquí que reducen el recuento de productos
debido a la cola, todas las solicitudes se procesarán una por una, por lo que no habrá conflictos para reducir el producto.