Building A High Throughput Web App with Spring-MVC and VoltDB
Written by Andrew Wilson
My last few posts have discussed parts of a web application that integrate VoltDB into a Spring web application. Today I will show how all the pieces are put together to build a low latency, high throughput Spring-MVC application. Much of my focus will be on the data layer where VoltDB resides, but I will go all the way up to the browser too.
Spring-MVC is one of my favorite frameworks. I like that I can configure each layer of my application through annotations or xml files. This application is using a single Java server page though I would have been just as happy to use Freemarker and could have done so with hardly any effort. I also like that Spring encourages me to write interfaces and concrete implementations, ensuring that my application is as loosely coupled as possible by dynamically wiring my components together. All this makes it easy for me to make minor or even sweeping changes with some measure of isolation, thus ensuring that my application will produce the same results regardless of the changes.
This application uses a Spring repository for managing the database. I use the Converter interface to map between the data and application layer objects that allows me to again, make significant changes to the database and queries without breaking the user interface. There is Jackson enabling JSON support for some browser side queries that update an html table. There are a pair of scheduled tasks that populate the database and report on database operation statistics. Those are the key components and some of them don’t require any code on the developer’s part.
Let’s begin at the top of the Spring-MVC layer, skipping the jsp.
The first method returns our home page and the second method returns a JSON response by calling the wired VoterRepository and returning the voting results. The ElectionResults object is converted behind the scenes using the Jackson library.
The VoterRepository is just an interface and the VoterRepositoryImpl is the only concrete implementation in the application. I could support multiple databases by creating additional VoterRepository objects and configuring them within the servlet-context.xml file. Regardless that there is only one implementation, the HomeController just gets back an instance of aVoterRepository and that repository could be anything.
The getResults() method returns an instance of ElectionResults by using a Spring Converter. The Spring Converter maps the VoltDB VoltTable result into an instance of ElectionResults. Again, the ElectionResults object need not know anything about the VoltTable thanks to the converter.
The code is organized such that I can easily replace one data source for another. I could find out that one database does not meet my needs and can swap it out for another without having to change my entire application. I only need to change the repository and converter implementations to migrate my application.
Although Spring offers a lot of application portability, I should not understate the challenges involved in a migration from one database to another. You may have to rewrite more than just a repository or a service. You may have stored procedures, complicated queries using vendor specific extensions to SQL, schema constructs that are not supported by the new database and many other issues. The goal here is to minimize the changes to the services that directly access the database without having to modify the application and presentation layers, which can be very complex and very difficult to debug.
The application must also register votes. We do this using a scheduled task and discuss it in Using the Spring @Schedule Annotation. There are two tasks, one for registering votes and one for gather statistics so we can measure the transaction throughput. These two tasks run asynchronously to the rest of the application.
We’ve covered all the major components except for configuration. Let’s look at the servlet-context.xml.
There are three important configurations. The first is highlighted in red and turns on annotation scanning for the MVC layer and the tasks. These are configured separately and can cause quite a bit of confusion because it is reasonable to think that the tasks would be configured through the MVC scanner, but they are not.
The second block in blue configures the VoltDB client to connect to a VoltDB server running on the localhost. The hostsnames property is a comma delimited list that can be expressed as server1,server2 instead of pointing only to localhost.
The last configuration in green adds a list of converter objects to the ConversionServiceFactory. The factory instantiates the converter based upon the types that I want from and to. The line “results = this.conversionService.convert(voteTable, ElectionResults.class);” specifies the source object of VoltTable and the target ElectionResults class. The factory will find a matching converter and execute the conversion. Let’s look at the converter.
The VoltTableToElectionResultsConverter class is somewhat different from a typical converter. This converter can handle hierarchies of objects. The ElectionResults object contains a collection of CandidateResult objects. We want to use a converter to map the VoltTable result to a CandidateResult. Note that we wire the ApplicationContext object rather than the conversion service. We then invoke the ConversionService by getting its bean. Normally you would just wire the ConversionService directly and then you’ll get a bunch of exceptions during startup because you are creating a circular dependency. The ConversionService cannot complete initialization without fully initializing the VoltTableToElectionResultsConverter, which would typically require a fully initialized ConversionService. Consequently, we work around the problem by going through the ApplicationContext, thus eliminating the autowire of the ConversionService.
The Converter API is described in greater detail in Using the Spring Converter API with VoltDB Data Objects.
This looks like a lot of code for doing very little work and that is true. There is a lot of boilerplate code, the fixed cost of writing any application. The cost of the effort goes down as your application gets bigger.
The application runs under Tomcat and is rather fast. In fact, it runs only slightly slower than a command line application designed to “firehose” the database. In the very near future I will post what happened when we benchmarked this application connecting to VoltDB running on an Amazon EC2 cluster. The results were very surprising and easy to replicate.