Optimistic locking is best suited for scenarios when conflicts are unlikely, or the number of conflicts will be very less.
Below is a simple example which explains the optimistic locking, where multiple threads (5 in this case) try to update the quantity in product_inventory table for product_id 1. Only one thread at a time would be able to successfully update the quantity for product_id 1. Other threads trying to update the quantity in database table will fail, if another thread have already updated the quantity (which means changed the version number) in between..
Also the program doesn't allow the updateProductInventory() operation where the update would result in setting a value which is less than zero for quantity. It throws an InsufficientProductInventoryException.
Note :
One very important thing to note is, in Optimistic Locking, we don't lock the table row, multiple threads can read the table and try to update the table. The update in table will be successfull if no other thread have updated the version in between, but if the version is updated in between by any other thread then update will fail. Since we don't lock the table row, this approach performs better and when number of conflicts are expected to be very less, optimistic locking will be a better approach.
CREATETABLE `inventory`.`product_inventory` (
`product_id`INTNOT NULL,
`product_name`VARCHAR(500) NULL,
`quantity`INTNOT NULL,
`version`BIGINTNOT NULL DEFAULT 0,
PRIMARY KEY (`product_id`));INSERT INTO product_inventory(product_id,product_name,quantity)
VALUES (1,'Some Popular Product',5);Step 3 : Write the code, allowing multiple threads to try to update the same record e.g. same product_id
importjava.sql.Connection;
importjava.sql.DriverManager;
importjava.sql.PreparedStatement;
importjava.sql.ResultSet;
importjava.sql.SQLException;
publicclassTest {
privatestaticfinalStringDB_URL = "jdbc:mysql://localhost:3306/inventory?useSSL=false";
privatestaticfinalStringDB_USER = "root";
privatestaticfinalStringDB_PASSWORD = "YOUR_DB_USER_PASSWORD";
privatestaticfinalStringPRODUCT_INVENTORY_TABLE = "product_inventory";
publicstaticvoidmain(String[] args) {
// Simulating 5 threads trying to update the same productThreadt1 = newThread(() -> updateProductInventory(1, -1));
Threadt2 = newThread(() -> updateProductInventory(1, -2));
Threadt3 = newThread(() -> updateProductInventory(1, -2));
Threadt4 = newThread(() -> updateProductInventory(1, 2));
Threadt5 = newThread(() -> updateProductInventory(1, -1));
t1.start(); t2.start(); t3.start(); t4.start(); t5.start();
}
privatestaticvoidupdateProductInventory(intproductId, intquantityChange) {
System.out.println(Thread.currentThread().getName() + " entered updateProductInventory() execution: " + quantityChange);
try (Connectionconnection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD)) {
connection.setAutoCommit(false); // Begin transaction// Select the row with versionStringselectQuery = "SELECT quantity, version FROM " + PRODUCT_INVENTORY_TABLE + " WHERE product_id = ?";
try (PreparedStatementselectStmt = connection.prepareStatement(selectQuery)) {
selectStmt.setInt(1, productId);
ResultSetrs = selectStmt.executeQuery();
if (rs.next()) {
intcurrentQuantity = rs.getInt("quantity");
longversion = rs.getLong("version"); // BIGINT maps to long in JavaintnewQuantity = currentQuantity + quantityChange;
// Ensure quantity does not go below zeroif (newQuantity < 0) {
thrownewInsufficientProductInventoryException(
Thread.currentThread().getName() + " Insufficient inventory for product ID " + productId + ". " +
"Current quantity: " + currentQuantity + ", attempted change: " + quantityChange);
}
// Try to update the row with optimistic lockStringupdateQuery = "UPDATE " + PRODUCT_INVENTORY_TABLE + " SET quantity = ?, version = version + 1 " +
"WHERE product_id = ? AND version = ?";
try (PreparedStatementupdateStmt = connection.prepareStatement(updateQuery)) {
updateStmt.setInt(1, newQuantity);
updateStmt.setInt(2, productId);
updateStmt.setLong(3, version); // Use long for BIGINT columnintrowsAffected = updateStmt.executeUpdate();
if (rowsAffected == 0) {
thrownewOptimisticLockException(
Thread.currentThread().getName() + " failed to update product ID " + productId + " due to version mismatch caused by concurrent modification.");
}
System.out.println(Thread.currentThread().getName() + " updated product " + productId +
" to quantity: " + newQuantity);
}
} else {
System.out.println("Product with ID " + productId + " not found.");
}
connection.commit(); // Commit transaction
} catch (InsufficientProductInventoryException | OptimisticLockExceptione) {
connection.rollback(); // Rollback transactionSystem.err.println(e.getMessage());
} catch (SQLExceptione) {
connection.rollback(); // Rollback transaction on SQLExceptionSystem.err.println(Thread.currentThread().getName() + " encountered a database error: " + e.getMessage());
} catch (Exceptione) {
connection.rollback(); // Rollback transaction on generic errorsSystem.err.println(Thread.currentThread().getName() + " encountered an error: " + e.getMessage());
}
} catch (Exceptione) {
e.printStackTrace();
}
}
}Note that at the start of every run, I manually set the quanity of product_id 1 to 5 with an update statement
UPDATE product_inventory set quantity=5where product_id =1Thread-1 entered updateProductInventory() execution: -2
Thread-0 entered updateProductInventory() execution: -1
Thread-3 entered updateProductInventory() execution: 2
Thread-2 entered updateProductInventory() execution: -2
Thread-4 entered updateProductInventory() execution: -1
Thread-3 updated product 1 to quantity: 7
Thread-1 failed to update product ID 1 due to version mismatch caused by concurrent modification.
Thread-2 failed to update product ID 1 due to version mismatch caused by concurrent modification.
Thread-0 failed to update product ID 1 due to version mismatch caused by concurrent modification.
Thread-4 failed to update product ID 1 due to version mismatch caused by concurrent modification.
Thread-2 entered updateProductInventory() execution: -2
Thread-0 entered updateProductInventory() execution: -1
Thread-4 entered updateProductInventory() execution: -1
Thread-3 entered updateProductInventory() execution: 2
Thread-1 entered updateProductInventory() execution: -2
Thread-0 updated product 1 to quantity: 4
Thread-4 failed to update product ID 1 due to version mismatch caused by concurrent modification.
Thread-3 failed to update product ID 1 due to version mismatch caused by concurrent modification.
Thread-2 failed to update product ID 1 due to version mismatch caused by concurrent modification.
Thread-1 failed to update product ID 1 due to version mismatch caused by concurrent modification.
From the above two execution outputs, we see only one thread was successfully able to update the quantity for product_id 1. All other four threads failed to update the quantity, due to version mismatch, which was caused by the updation of version in the database table by another thread. We can't guarantee how many threads would be able to successfully update the quantity, that depends on the order of execution of threads and execution delay at runtime.
We can see that many threads might fail updating the quantity in database table due to version mismatch, which is caused by the updation of version in the database table by another thread. So a retry mechanism to try the update operation is very much needed.
packagenet.mahtabalam;
importjava.sql.Connection;
importjava.sql.DriverManager;
importjava.sql.PreparedStatement;
importjava.sql.ResultSet;
publicclassTestWithRetry {
privatestaticfinalStringDB_URL = "jdbc:mysql://localhost:3306/inventory?useSSL=false";
privatestaticfinalStringDB_USER = "root";
privatestaticfinalStringDB_PASSWORD = "YOUR_DB_USER_PASSWORD";
privatestaticfinalStringPRODUCT_INVENTORY_TABLE = "product_inventory";
privatestaticfinalintMAX_RETRIES = 3; // Max number of retries for version mismatchpublicstaticvoidmain(String[] args) {
// Simulating 5 threads trying to update the same productThreadt1 = newThread(() -> updateProductInventory(1, -1));
Threadt2 = newThread(() -> updateProductInventory(1, -2));
Threadt3 = newThread(() -> updateProductInventory(1, -2));
Threadt4 = newThread(() -> updateProductInventory(1, 2));
Threadt5 = newThread(() -> updateProductInventory(1, -1));
t1.start(); t2.start(); t3.start(); t4.start(); t5.start();
}
privatestaticvoidupdateProductInventory(intproductId, intquantityChange) {
intattempts = 0;
while (attempts < MAX_RETRIES) {
attempts++;
System.out.println(Thread.currentThread().getName() + " attempt " + attempts + " to updateProductInventory() with quantity change: " + quantityChange);
try (Connectionconnection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD)) {
connection.setAutoCommit(false); // Begin transaction// Select the row with versionStringselectQuery = "SELECT quantity, version FROM " + PRODUCT_INVENTORY_TABLE + " WHERE product_id = ?";
try (PreparedStatementselectStmt = connection.prepareStatement(selectQuery)) {
selectStmt.setInt(1, productId);
ResultSetrs = selectStmt.executeQuery();
if (rs.next()) {
intcurrentQuantity = rs.getInt("quantity");
longversion = rs.getLong("version"); // BIGINT maps to long in JavaintnewQuantity = currentQuantity + quantityChange;
// Ensure quantity does not go below zeroif (newQuantity < 0) {
thrownewInsufficientProductInventoryException(
Thread.currentThread().getName() + " Insufficient inventory for product ID " + productId + ". " +
"Current quantity: " + currentQuantity + ", attempted change: " + quantityChange);
}
// Try to update the row with optimistic lockStringupdateQuery = "UPDATE " + PRODUCT_INVENTORY_TABLE + " SET quantity = ?, version = version + 1 " +
"WHERE product_id = ? AND version = ?";
try (PreparedStatementupdateStmt = connection.prepareStatement(updateQuery)) {
updateStmt.setInt(1, newQuantity);
updateStmt.setInt(2, productId);
updateStmt.setLong(3, version); // Use long for BIGINT columnintrowsAffected = updateStmt.executeUpdate();
if (rowsAffected == 0) {
thrownewOptimisticLockException(
Thread.currentThread().getName() + " failed to update product ID " + productId + " due to version mismatch caused by concurrent modification.");
}
System.out.println(Thread.currentThread().getName() + " successfully updated product " + productId +
" to quantity: " + newQuantity);
connection.commit(); // Commit transactionreturn; // Exit method on successful update
}
} else {
System.out.println("Product with ID " + productId + " not found.");
return; // Exit method if product not found
}
} catch (InsufficientProductInventoryExceptione) {
connection.rollback(); // Rollback transactionSystem.err.println(e.getMessage());
return; // Exit method on insufficient inventory
} catch (OptimisticLockExceptione) {
connection.rollback(); // Rollback transactionSystem.err.println(e.getMessage());
} catch (Exceptione) {
connection.rollback(); // Rollback transaction in case of an errorSystem.err.println(Thread.currentThread().getName() + " encountered an error: " + e.getMessage());
return; // Exit method on general error
}
} catch (Exceptione) {
e.printStackTrace();
return; // Exit method if unable to connect to database
}
// If we reach here, it means the update failed due to version mismatchif (attempts < MAX_RETRIES) {
System.out.println(Thread.currentThread().getName() + " retrying due to version mismatch (attempt " + attempts + " falied).");
} else {
System.err.println(Thread.currentThread().getName() + " reached max retries. Update failed for product ID " + productId + ".");
}
}
}
}Thread-2 attempt 1 to updateProductInventory() with quantity change: -2
Thread-4 attempt 1 to updateProductInventory() with quantity change: -2
Thread-0 attempt 1 to updateProductInventory() with quantity change: -1
Thread-3 attempt 1 to updateProductInventory() with quantity change: 1
Thread-1 attempt 1 to updateProductInventory() with quantity change: -2
Thread-2 successfully updated product 1 to quantity: 3
Thread-0 failed to update product ID 1 due to version mismatch caused by concurrent modification.
Thread-4 failed to update product ID 1 due to version mismatch caused by concurrent modification.
Thread-3 failed to update product ID 1 due to version mismatch caused by concurrent modification.
Thread-4 retrying due to version mismatch (attempt 1 falied).
Thread-4 attempt 2 to updateProductInventory() with quantity change: -2
Thread-3 retrying due to version mismatch (attempt 1 falied).
Thread-3 attempt 2 to updateProductInventory() with quantity change: 1
Thread-0 retrying due to version mismatch (attempt 1 falied).
Thread-0 attempt 2 to updateProductInventory() with quantity change: -1
Thread-1 failed to update product ID 1 due to version mismatch caused by concurrent modification.
Thread-1 retrying due to version mismatch (attempt 1 falied).
Thread-1 attempt 2 to updateProductInventory() with quantity change: -2
Thread-4 successfully updated product 1 to quantity: 1
Thread-3 failed to update product ID 1 due to version mismatch caused by concurrent modification.
Thread-0 failed to update product ID 1 due to version mismatch caused by concurrent modification.
Thread-3 retrying due to version mismatch (attempt 2 falied).
Thread-3 attempt 3 to updateProductInventory() with quantity change: 1
Thread-0 retrying due to version mismatch (attempt 2 falied).
Thread-0 attempt 3 to updateProductInventory() with quantity change: -1
Thread-0 successfully updated product 1 to quantity: 0
Thread-3 failed to update product ID 1 due to version mismatch caused by concurrent modification.
Thread-3 reached max retries. Update failed for product ID 1.
Thread-1 Insufficient inventory for product ID 1. Current quantity: 1, attempted change: -2
Pessimistic Locking : https://github.com/eMahtab/pessimistic-locking