Dynamodb is a low latency NoSQL database like MongoDB or Cassandra. It is fully managed by AWS so you don’t have to maintain any server for your database. Literally, there are no servers to provision, patch, or manage and no software to install, maintain or operate. DynamoDB automatically scales tables up and down to adjust for capacity and maintain performance. It supports both document and key-value data models. It uses SSD storage so it is very fast and supports Multi-AZ. DynamoDB global tables replicate your data across multiple AWS Regions to give you fast, local access to data for your globally distributed applications. For use cases that require even faster access with microsecond latency, DynamoDB Accelerator (DAX) provides a fully managed in-memory cache.
As with other AWS services, DynamoDB requires a role or user to have the right privileges to access DynamoDB. Make sure you add DynamoDB full access to the role or user(access keys) your server is using. You can also use a special IAM condition to restrict users to only access their data.
DynamoDB supports ACID transactions to enable you to build business-critical applications at scale. DynamoDB encrypts all data by default and provides fine-grained identity and access control on all your tables.
DynamoDB provides both provisioned and on-demand capacity modes so that you can optimize costs by specifying capacity per workload or paying for only the resources you consume.
When to use:
a. Gaming
b. User, vehicle, and driver data stores
c. Ads technology
d. Player session history data stores
e. Inventory tracking and fulfillment
f. Shopping carts
g. User transactions
There are two ways Dynamodb supports read:
1. Eventually consistent read – When data is saved to Dynamodb, it takes about 1 second for the data to propagate across multiple availability zones. This does not guarantee that your users will see the recent data from 1 second ago.
2. Strongly consistent read – This returns the most recent or up to date data. It will detect if there is a write operation happening at the time. If there is, then it will wait to read data afterward which results in a longer but more consistent read.
DynamoDB Tables
DynamoDB Primary Keys
1. Unique Partition Key – This key must be unique across items or rows i.e user_id. This value is input into a hash function which calculates the physical location of which data will be stored.
2. Composite Key (Partition key + Sort key) – This is useful for post or comment tables where rows belong to another entity i.e user_id as the partition key and post_timestamp for the sort key. Two or more items can have the same partition key but they must have different sort keys. These items stored physically together but sorted with their sort key.
DynamoDB Indexes
Local Secondary Index
– Can only be created when creating your table and you cannot change, remove, or add it later.
– Has the same partition as your original table.
– Has a different sort key from your original table.
– Queries based on the local secondary indexes are faster than regular queries.
Global Secondary Index
– Can add when creating your table or later.
– Has different partition and sort keys.
– Speeds up queries.
DynamoDB with Java. I am using DynamoDB wrapper.
For local development, I am using a docker image. There are other ways to install DynamoDB locally.
Run DynamoDB locally on your computer
@Bean public AmazonDynamoDB amazonDynamoDB() { AmazonDynamoDB client = AmazonDynamoDBClientBuilder.standard() .withCredentials(amazonAWSCredentialsProvider()) .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(amazonDynamoDBEndpoint, Regions.US_WEST_2.getName())) .build(); return client; } @Bean public DynamoDBMapper dynamoDBMapper() { return new DynamoDBMapper(amazonDynamoDB()); }
@DynamoDBTable(tableName="user") public class User implements Serializable { private static final long serialVersionUID = 1L; @DynamoDBHashKey(attributeName="uuid") @DynamoDBAttribute private String uuid; @DynamoDBAttribute private String email; @DynamoDBAttribute private String firstName; @DynamoDBAttribute private String lastName; @DynamoDBAttribute private String phoneNumber; @DynamoDBAttribute private Date createdAt; // setters and getters }
@Repository public class UserRepositoryImp implements UserRepository { private Logger log = LoggerFactory.getLogger(this.getClass()); @Autowired private AmazonDynamoDB amazonDynamoDB; @Autowired private DynamoDBMapper dynamoDBMapper; @Override public User create(User user) { user.setUuid(RandomGeneratorUtils.getUserUuid()); user.setCreatedAt(new Date()); dynamoDBMapper.save(user); return getById(user.getUuid()); } @Override public User getById(String id) { User user = dynamoDBMapper.load(User.class, id); return user; } @Override public List<User> getAllUser() { PaginatedScanList<User> users = dynamoDBMapper.scan(User.class, new DynamoDBScanExpression()); return (users!=null) ? users.subList(0, users.size()) : null; } @Override public boolean createTable() { // check if table has been created try { DescribeTableResult describeTableResult = amazonDynamoDB.describeTable("user"); if(describeTableResult.getTable()!=null){ log.debug("user table has been created already!"); return true; } } catch (Exception e) { } // table hasn't been created so start a createTableRequest CreateTableRequest createTableRequest = dynamoDBMapper.generateCreateTableRequest(User.class); createTableRequest.withProvisionedThroughput(new ProvisionedThroughput(5L,5L)); // create table CreateTableResult createTableResult = amazonDynamoDB.createTable(createTableRequest); long count = createTableResult.getTableDescription().getItemCount(); log.debug("item count={}",count); return false; } }
Transaction
Here is an example of sending money(balance) from one user to another.
@Override public boolean tranferBalance(double amount, User userA, User userB) { final String USER_TABLE_NAME = "user"; final String USER_PARTITION_KEY = "userid"; try { // user A HashMap<String, AttributeValue> userAKey = new HashMap<>(); userAKey.put(USER_PARTITION_KEY, new AttributeValue(userA.getUuid())); ConditionCheck checkUserAValid = new ConditionCheck() .withTableName(USER_TABLE_NAME) .withKey(userAKey) .withConditionExpression("attribute_exists(" + USER_PARTITION_KEY + ")"); Map<String, AttributeValue> expressionAttributeValuesA = new HashMap<>(); expressionAttributeValuesA.put(":balance", new AttributeValue().withN("" + (userA.getBalance() - amount))); Update withdrawFromA = new Update().withTableName(USER_TABLE_NAME).withKey(userAKey) .withUpdateExpression("SET balance = :balance") .withExpressionAttributeValues(expressionAttributeValuesA); log.debug("user A setup!"); // user B HashMap<String, AttributeValue> userBKey = new HashMap<>(); userAKey.put(USER_PARTITION_KEY, new AttributeValue(userB.getUuid())); ConditionCheck checkUserBValid = new ConditionCheck() .withTableName(USER_TABLE_NAME) .withKey(userBKey) .withConditionExpression("attribute_exists(" + USER_PARTITION_KEY + ")"); Map<String, AttributeValue> expressionAttributeValuesB = new HashMap<>(); expressionAttributeValuesB.put(":balance", new AttributeValue().withN("" + (userB.getBalance() + amount))); Update depositToB = new Update().withTableName(USER_TABLE_NAME).withKey(userBKey) .withUpdateExpression("SET balance = :balance") .withExpressionAttributeValues(expressionAttributeValuesB); log.debug("user B setup!"); HashMap<String, AttributeValue> withdrawItem = new HashMap<>(); withdrawItem.put(USER_PARTITION_KEY, new AttributeValue(userA.getUuid())); withdrawItem.put("balance", new AttributeValue("100")); // actions Collection<TransactWriteItem> actions = Arrays.asList( new TransactWriteItem().withConditionCheck(checkUserAValid), new TransactWriteItem().withConditionCheck(checkUserBValid), new TransactWriteItem().withUpdate(withdrawFromA), new TransactWriteItem().withUpdate(depositToB)); log.debug("actions setup!"); // transaction request TransactWriteItemsRequest withdrawTransaction = new TransactWriteItemsRequest() .withTransactItems(actions) .withReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL); log.debug("transaction request setup!"); // Execute the transaction and process the result. TransactWriteItemsResult transactWriteItemsResult = amazonDynamoDB.transactWriteItems(withdrawTransaction); log.debug("consumed capacity={}",ObjectUtils.toJson(transactWriteItemsResult.getConsumedCapacity())); return (transactWriteItemsResult.getConsumedCapacity()!=null) ? true : false; } catch (ResourceNotFoundException e) { log.error("One of the table involved in the transaction is not found " + e.getMessage()); } catch (InternalServerErrorException e) { log.error("Internal Server Error " + e.getMessage()); } catch (TransactionCanceledException e) { log.error("Transaction Canceled " + e.getMessage()); } catch (Exception e) { log.error("Exception, msg={}",e.getLocalizedMessage()); } return false; }