In the last post I used optimistic locking with the Version attribute provided by DynamoDBMapper. Here I am going to look at the pessimistic locking method
Pessimistic locking techniques require us to acquire a lock on the resource before we can modify it. As only one process can acquire the lock, e are guaranteed to be working with the latest version of the object.
Notice there is no version attribute here.
The DynamoDb representation is as below:
The next step is to setup the Lock Client. The Lock client uses a separate DynamoDb table to manage locks. Instead of manually setting up the table, I used a utility method provided by the LockClient library:
The table created is as below:
A table is created with the hash key as 'key'. This table is used to determine if a Cake item is locked and who owns the lock.
The Cake consumer threads is as below:
The code works as below:
The change in lock values can be seen in the Cake Locks table:
The assumption here is that code will finish its operations within 2 milliseconds. How does the locking logic work ?
So any code that reads the Lock table - if it finds an entry will always assume that someone is holding the lock. The assumption is also that lock is being held for atleast 'DurationTime' as specified in the Table.
So in our code, Lets say Thread 0 wins the lock and will hold it for 2 milliseconds. It after 1 millisecond, Thread 1 arrives it will see that the lock is being held for a duration of 2 milliseconds and will therefore wait for 2 milliseconds before trying to acquire again. In effect this is 3 milliseconds since Thread 0 acquired the lock.
When Thread 1 finally checks again, the record would have been deleted (as Thread 0 had released the lock after 2 milliseconds). So Thread 1 will acquire the lock.
What if Thread 0 had crashed while holding the lock ?
If Thread had crashed without releasing the lock, an entry would remain in table. When Thread 1 checks again, the recordVersionNumber would be same as when it checked earlier. This means, lock is stale and so Thread 0 will acquire the lock.
The lock client has additional features - non blocking wait, automatic heartbeats that extend your lock duration etc.
My code output is as below:
What fits my use case better ?
I compared both locking mechanisms - for my usecase, Optimistic Locking is a better fit. The DynamoMapper can handle the contention among multiple write threads ensuring none of them updates stale information.
Pessimistic locking is more of an advisory locking mechanism, with more responsibility on the code to get it right. A more appropriate use case for this would be when I need to perform some operations if I have a lock on the record.
Pessimistic locking techniques require us to acquire a lock on the resource before we can modify it. As only one process can acquire the lock, e are guaranteed to be working with the latest version of the object.
Dynamo Db inherently does not provide a lock implementation, Amazon has however released an open source library called the Amazon DynamoDB Lock Client, which can be used for this purpose.
Similar to our previous post I created a simple Cake class:
@DynamoDBTable(tableName = "Cake") public class Cake { @DynamoDBHashKey(attributeName = "id") private String id; private int noOfSlices; private String lastEatenBy; //setter getters }
The next step is to setup the Lock Client. The Lock client uses a separate DynamoDb table to manage locks. Instead of manually setting up the table, I used a utility method provided by the LockClient library:
CreateDynamoDBTableOptions lockTableOptions = CreateDynamoDBTableOptions.builder(amazonDynamoDB, new ProvisionedThroughput(25L, 25L), "CakeLock").build(); AmazonDynamoDBLockClient.createLockTableInDynamoDB(lockTableOptions);
The Cake consumer threads is as below:
static class CakeEater implements Callable<Void> { private String name; private DynamoDBMapper mapper; private String cakeKey; public CakeEater(String name, DynamoDBMapper mapper, String cakeKey) { this.name = name; this.mapper = mapper; this.cakeKey = cakeKey; System.out.println("Eater " + name + " ready to start eating"); } @Override public Void call() throws IOException { // Whether or not to create a heartbeating background thread final boolean createHeartbeatBackgroundThread = false; //build the lock client long leaseDuration = 2L; final AmazonDynamoDBLockClient client = new AmazonDynamoDBLockClient( AmazonDynamoDBLockClientOptions.builder(amazonDynamoDB, "CakeLock") .withTimeUnit(TimeUnit.MILLISECONDS) .withLeaseDuration(leaseDuration) .withHeartbeatPeriod(2L) .withCreateHeartbeatBackgroundThread(createHeartbeatBackgroundThread) .build()); while (true) { //try to acquire a lock on the partition key - cakeKey try { System.out.println("Attempt to acquire lock by " + name); final Optional<LockItem> lockItem = Optional.ofNullable( client.acquireLock(AcquireLockOptions.builder(cakeKey).build())); if (lockItem.isPresent()) { System.out.println("Acquired lock by " + name); Cake cakeId = new Cake(); cakeId.setId(this.cakeKey); //Time to eat the cake quickly - within 200 millis Cake cake = mapper.load(cakeId); if (cake != null && cake.getNoOfSlices() > 0) { System.out.println(name + " looking to eat. Available Pieces " + cake.getNoOfSlices() + ".Last eaten by " + cake.getLastEatenBy()); cake.setNoOfSlices(cake.getNoOfSlices() - 1); cake.setLastEatenBy(name); try { mapper.save(cake); // processing complete } catch (Exception e) { System.err.println("Saving Cake for " + name + " failed. " + e.getMessage()); } } else { System.out.println("Cake is over. " + name + " stopping"); break; } client.releaseLock(lockItem.get()); //lock released System.out.println(name + " taking a break"); Thread.sleep(leaseDuration * 3); } else { System.out.println(name + " failed to acquire lock"); } } catch (InterruptedException e) { e.printStackTrace(); } } client.close(); return null; } }
- Code tries to acquire a lock for 2 milliseconds.
- If lock is acquired, code will fetch the cake record and eat a piece.
- Once done the code releases the lock.
The change in lock values can be seen in the Cake Locks table:
The assumption here is that code will finish its operations within 2 milliseconds. How does the locking logic work ?
The way locks are expired is that a call to acquireLock reads in the current lock, checks the RecordVersionNumber of the lock (which is a GUID) and starts a timer. If the lock still has the same GUID after the lease duration time has passed, the client will determine that the lock is stale and expire it
So in our code, Lets say Thread 0 wins the lock and will hold it for 2 milliseconds. It after 1 millisecond, Thread 1 arrives it will see that the lock is being held for a duration of 2 milliseconds and will therefore wait for 2 milliseconds before trying to acquire again. In effect this is 3 milliseconds since Thread 0 acquired the lock.
When Thread 1 finally checks again, the record would have been deleted (as Thread 0 had released the lock after 2 milliseconds). So Thread 1 will acquire the lock.
What if Thread 0 had crashed while holding the lock ?
If Thread had crashed without releasing the lock, an entry would remain in table. When Thread 1 checks again, the recordVersionNumber would be same as when it checked earlier. This means, lock is stale and so Thread 0 will acquire the lock.
The lock client has additional features - non blocking wait, automatic heartbeats that extend your lock duration etc.
My code output is as below:
Cake has been saved Eater Eater0 ready to start eating Eater Eater1 ready to start eating Eater Eater2 ready to start eating Attempt to acquire lock by Eater1 Attempt to acquire lock by Eater0 Attempt to acquire lock by Eater2 Acquired lock by Eater0 Eater0 looking to eat. Available Pieces 10.Last eaten by null Eater0 taking a break Attempt to acquire lock by Eater0 Acquired lock by Eater0 Eater0 looking to eat. Available Pieces 9.Last eaten by Eater0 Eater0 taking a break Attempt to acquire lock by Eater0 Acquired lock by Eater0 Eater0 looking to eat. Available Pieces 8.Last eaten by Eater0 Eater0 taking a break Attempt to acquire lock by Eater0 Acquired lock by Eater0
Splendid post. Also visit here: Feeta.pk - house for sale in Islamabad . it is very helpful for us thanks for sharing.
ReplyDeleteWow amazing, i will definately go for these helpful tips. Redspider - classified script is about to tell you that thanks for sharing.
ReplyDeleteCheapest Flight Booking Travels with SkyGoTrip
ReplyDelete