Apache Ignite as an Inter-Microservice Transactional In-Memory Data store
For the past few years, hearing the terms “distributed” and “data store” in a single sentence would immediately move my mind to visualize a single word, “Redis”. Now, this was not entirely down to my ignorance. A quick googling would remind you that Redis is still the #1 in the rankings for Key-value stores and ranked #8 out of all DB engines. However, as the proverb goes, there is more than one way to skin a cat. And some of these ways are obviously more equal than others. Behold Apache Ignite.
Apache ignite is an in-memory computing platform that is durable, strongly consistent, and highly available with powerful SQL, key-value, and processing APIs. A comprehensive comparison between Ignite and Redis can be found here. It’s optional support for native persistence is quite advanced compared to the snapshot-based persistence model of Redis. The ability of Ignite to support ACID guaranteed transactions is quite impressive as well. But I was impressed the most by the ANSI-99 compliant, horizontally scalable, and fault-tolerant SQL engine that allows you to interact with Ignite as with a regular SQL database using JDBC, ODBC drivers, or native SQL APIs.
The Menu API is a reference data store which is the source of truth for all menu related data. It stores and manages Items, prices and stock count related information in a mySql database. The requirement here is to keep that data synched with the Order API, which actually validates, accepts and calculates the total for all orders. The Order API has its own mySql database to store the order details. In this design, the items which are managed and stored in by the menu API mySql DB are also stored in an Ignite cache which is shared with the Order API.
The order API will be reducing the available stock count of items as these items are assigned to orders. One caveat here is that the incoming orders do not have the id of the items and hence the Order API needs to query the cache by name to find the matching items. Both APIs have the option of running multiple instances in a typical cloud hosted load balanced manner.
This scenario was chosen to show several different scenarios/patterns on the usage of the ignite cache.
Few points to take out if the Ignite cache configuration, this is a cache for the “Item” objects and the atomicity mode is “Transactional”. This indicates that we require full ACID guarantees. The primary index is a long value, which in this case is the item id.
The “Item” entity which is used in the cache has a few special points as well. As the annotations suggest it is designed to be stored in a DB table named “items” (this is indeed done by the menu API) and a few of its fields are annotated with @QuerySqlField. This annotation is a special Ignite annotation which makes these fields visible to Ignite SQL queries. Some of these annotations have @QuerySqlField(index = true) which indicates that these fields are indexed within the ignite cache. I have used Lombok annotations to remove boilerplate code as much as possible. Both the cache configuration and the Item class reside in a separate library which is used by both of the APIs.
Cache Usage patterns
The first usage is a transaction-based cache insert of Item objects, which is performed by the Menu API.
Whenever TRANSACTIONAL atomicity mode is configured, Ignite supports OPTIMISTIC and PESSIMISTIC concurrency modes for transactions. Concurrency mode determines when an entry-level transaction lock should be acquired: at the time of data access or during the prepare phase. Locking prevents concurrent access to an object. Further information about TransactionConcurrency and TransactionIsolation can be found in the Ignite documentation. The main point here is that Ignite provides the user with flexibility on the concurrency and isolation levels of transactions which can be configured to match the performance and behavioral requirements of the specific use case.
In addition to this Ignite provides the capability to execute SQL queries on the same cache, as demonstrated in the code below.
This code demonstrates how the Order API queries the Item cache to find the correct item which matches the item name specified in the order. Notice that the fields annotated with @QuerySqlField can be retrieved and that the name field (which is index in the cache) is used for the WHERE clause. Since this is a read only operation, no transactions are used here. But for update queries you have the option of using transactions.
Another scenario is where a property of an element in the cache is updated via a transaction. In this case, the Order API updates the available stock count of each item in the order in a single transaction. If any of the individual operations fail, the transaction is abandoned.
Ignite integrates well with Java and spring and takes a minimal effort in getting to work. In the example mentioned here, Ignite is running in embedded mode, which means that there is no separate ignite cluster. In fact, the ignite node runs on the same JVM as the spring API which starts it. This removes any network overhead in using the ignite cache. Ignite can also be run as a separate independent cluster. You can optionally use ATOMIC as the CacheAtomicityMode instead of TRANSACTIONAL, this does not provide strong consistency but it often has better performance due to not having to lock the data, and it all depends on your usage scenario. This article is in no way a comprehensive coverage of Apache Ignite as it has several other features such as co-located processing and distributed machine learning acceleration, it merely investigate one potential usage scenario.