Optimistic Locking
Jan 3, 2022
optimistic locking
mongo
Purpose
The purpose of this document is to learn about optimistic locking and its use cases.
Use Case
Suppose there are multiple threads that are trying to increment a count field in a table.
How would you do it?
You could try to increment the count by
- reading the current count from the table.
- increment it.
- save it.
The problem is these 3 steps are not atomic. Example:
Time | Thread 1 | Thread 2
T1 | reads 50 | reads 50
T2 | increments to 51 | increments to 51
T3 | save 51 |
T4 | | save 51
A count is missed here. We need some kind of locking or transactions.
Iteration 1: Optimistic Locking
Is a way to write to the DB only if the record hasn't changed since the previous read.
MongoDB does it with a @version field.
Updates to the table succeed only if the “read” version is the same when saving it back, otherwise, an OptimisticLockingFailureException is thrown.
Here is an example:
ExecutorService executorService = new ThreadPoolExecutor(
5, 5, 5L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
List<Callable<Integer>> callableTasks = new ArrayList<>();
Callable<Integer> potentiallyFailingJob = () -> {
TimeUnit.MILLISECONDS.sleep(300);
Table table = tableRepository.findByRecordType(Table.RecordType.REQUEST_COUNT.name());
Integer count = table.getCount();
count += 1;
table.setCount(count);
return this.tableRepository.save(table).getCount();
};
for (int i = 0; i < 10; i++) {
callableTasks.add(potentiallyFailingJob);
}
List<Future<Integer>> futures = executorService.invokeAll(callableTasks);
for (Future<Integer> f : futures) {
try {
f.get();
} catch (InterruptedException e) {
System.out.println(System.currentTimeMillis() + " : Interrupted Exception catch.");
} catch (ExecutionException e) {
System.out.println(System.currentTimeMillis() + " : Execution Exception catch.");
}
}
executorService.shutdown();
Table Class looks like
public class Table {
public enum RecordType {
REQUEST_COUNT;
}
@Id
private String id;
private RecordType recordType;
@Version
private Long version;
private Integer count;
public Table() {}
// add getters and setters mate.
}
This works but OptimisticLockingFailureException will be thrown when multiple threads try to update the table after the version has changed.
Iteration 2: Optimistic Locking w/ retry
boolean runWithRetries(int maxRetries, Callable<Integer> t, ExecutorService executorService) {
int count = 0;
while (count < maxRetries) {
try {
System.out.println("Try: " + count);
Integer result = executorService.submit(t).get();
System.out.println("Try: " + count + ", result: " + result);
return true;
} catch (RejectedExecutionException | InterruptedException | ExecutionException e) {
if (++count >= maxRetries) {
return false;
}
}
}
return false;
}
and add
System.out.println("Was retry successful ? " + (runWithRetries(5, potentiallyFailingJob, executorService) ? "it was" : "no, it wasn't"));
when ExecutionException is caught.
Great. This will retry 5 times before giving up.
Conclusion
We have learned what is optimistic locking and how to use it.
Hope you enjoyed this newsletter.