Deadlock in an e-commerce application
Deadlock scenario in an e-commerce application where users place orders, and inventory is updated. We’ll focus on two operations:
- Placing an Order: Decreasing inventory when an item is ordered.
- Cancelling an Order: Increasing inventory when an order is cancelled.
These two actions can result in deadlocks if handled improperly, particularly when two transactions lock resources (like orders and inventory) in different orders.
Scenario: E-Commerce Inventory and Order Management Deadlock
Problem Case:
- Transaction A: Places an order for Product 1, reducing inventory, and locks Order 1.
- Transaction B: Cancels Order 2, increasing inventory for Product 1, and locks Order 2.
If both transactions attempt to lock Order 1 and Order 2 in different sequences, a deadlock can occur.
API Controller Example: Potential Deadlock
Problem Code: Inconsistent Locking
const { Sequelize, Transaction } = require('sequelize');
const sequelize = new Sequelize('postgres://user:pass@localhost:5432/ecommerce');
const Order = require('./models/Order'); // Order model
const Inventory = require('./models/Inventory'); // Inventory model
// Place an order
async function placeOrder(req, res) {
const { orderId, productId, quantity } = req.body;
const transaction = await sequelize.transaction();
try {
// Lock the order
const order = await Order.findOne({
where: { id: orderId },
lock: transaction.LOCK.UPDATE,
transaction,
});
// Lock the inventory
const inventory = await Inventory.findOne({
where: { productId },
lock: transaction.LOCK.UPDATE,
transaction,
});
// Update inventory
if (inventory.quantity < quantity) {
throw new Error('Not enough stock');
}
inventory.quantity -= quantity;
await inventory.save({ transaction });
// Save the order
order.status = 'PLACED';
await order.save({ transaction });
// Commit the transaction
await transaction.commit();
res.json({ message: 'Order placed successfully' });
} catch (error) {
await transaction.rollback();
res.status(500).json({ error: error.message });
}
}
// Cancel an order
async function cancelOrder(req, res) {
const { orderId, productId, quantity } = req.body;
const transaction = await sequelize.transaction();
try {
// Lock the order
const order = await Order.findOne({
where: { id: orderId },
lock: transaction.LOCK.UPDATE,
transaction,
});
// Lock the inventory
const inventory = await Inventory.findOne({
where: { productId },
lock: transaction.LOCK.UPDATE,
transaction,
});
// Update inventory
inventory.quantity += quantity;
await inventory.save({ transaction });
// Update order status
order.status = 'CANCELLED';
await order.save({ transaction });
// Commit the transaction
await transaction.commit();
res.json({ message: 'Order canceled successfully' });
} catch (error) {
await transaction.rollback();
res.status(500).json({ error: error.message });
}
}
module.exports = { placeOrder, cancelOrder };Deadlock Issue:
- Transaction A (Place Order) locks Order 1 first, then Inventory for Product 1.
- Transaction B (Cancel Order) locks Inventory for Product 1 first, then tries to lock Order 2.
This can create a deadlock if Transaction A is waiting on the inventory that Transaction B has locked, and vice versa.
Solution: Consistent Lock Ordering
To prevent deadlocks, always acquire locks in a consistent order across all operations. In this case, always lock the inventory first, then the order.
Improved Code: Consistent Locking Order
// Place an order (consistent lock order: inventory first, then order)
async function placeOrder(req, res) {
const { orderId, productId, quantity } = req.body;
const transaction = await sequelize.transaction();
try {
// Lock the inventory first
const inventory = await Inventory.findOne({
where: { productId },
lock: transaction.LOCK.UPDATE,
transaction,
});
// Lock the order next
const order = await Order.findOne({
where: { id: orderId },
lock: transaction.LOCK.UPDATE,
transaction,
});
// Update inventory
if (inventory.quantity < quantity) {
throw new Error('Not enough stock');
}
inventory.quantity -= quantity;
await inventory.save({ transaction });
// Update order status
order.status = 'PLACED';
await order.save({ transaction });
// Commit the transaction
await transaction.commit();
res.json({ message: 'Order placed successfully' });
} catch (error) {
await transaction.rollback();
res.status(500).json({ error: error.message });
}
}
// Cancel an order (consistent lock order: inventory first, then order)
async function cancelOrder(req, res) {
const { orderId, productId, quantity } = req.body;
const transaction = await sequelize.transaction();
try {
// Lock the inventory first
const inventory = await Inventory.findOne({
where: { productId },
lock: transaction.LOCK.UPDATE,
transaction,
});
// Lock the order next
const order = await Order.findOne({
where: { id: orderId },
lock: transaction.LOCK.UPDATE,
transaction,
});
// Update inventory
inventory.quantity += quantity;
await inventory.save({ transaction });
// Update order status
order.status = 'CANCELLED';
await order.save({ transaction });
// Commit the transaction
await transaction.commit();
res.json({ message: 'Order canceled successfully' });
} catch (error) {
await transaction.rollback();
res.status(500).json({ error: error.message });
}
}Key Takeaways:
- Consistent Lock Ordering: Always lock inventory first, then orders, in both the “place order” and “cancel order” operations. This prevents deadlocks by ensuring that both transactions acquire locks in the same order.
- Isolation Levels: Depending on your use case, you may also want to adjust isolation levels to reduce locking overhead, but that depends on the business logic.
By implementing consistent lock ordering, deadlocks are avoided, and the system ensures data integrity even under high concurrency.
