Using the Redis NoSql database with .NET Part 7: transactions
April 17, 2017 Leave a comment
In the previous post we looked at the hash data type in Redis. Hashes are key-value collections like dictionaries in .NET or maps in Java. For a given Redis key hashes can store multiple key-value pairs. Hashes offer a certain degree of object-oriented design where an object, such as a product or customer can be described by key-values like “id”: 23, “name”: “Unknown LTD” and so on. We can use the string data type to store the properties of an object as a JSON string as we saw before in this series. However, if you need to access the individual properties of an object then a hash can be a more suitable option.
We’ve now discussed all available data types in Redis. We’ll now turn our attention to transactions.
Transactions in Redis
If you’ve worked with transactions in a relational database like SQL Server then you’ll know at least a bit about them, what they are and how they work. A transaction is a group of operations that are executed as a block. They either succeed or fail together. We say that transactions are atomic, i.e. the operations in a transaction form a single unit that will run as a single operation without interruption. Ideally an external process cannot modify the resources that are part of a transaction while the transaction is executing. Also, if a single member of the transaction fails then the changes up to that point are rolled back and the operations after the failed one are not executed at all.
Database engines implement these characteristics to a varying degree. SQL Server has full support for transactions with rollbacks. MongoDb has no transactions implementation at all at this point of time. Redis lies somewhere in between. It supports transactions and we can watch specific resources while a transaction is executed for concurrency control. However, there’s no rollback if a single operation fails. If an operation fails during execution then the rest of the transaction members will still be executed. Note that the operations must first be queued up the same way as we write Redis commands. The queued up commands are at checked for syntactic correctness. We’ll see in a bit what this means.
There are 5 transaction related commands in Redis at this time:
- MULTI: marks the start of a transaction, similar to BEGIN TRANSACTION in SQL Server
- DISCARD: marks the end of a transaction but does not execute the commands, similar to ROLLBACK TRANSACTION in SQL Server
- EXEC: marks the end of a transaction and executes the commands, like COMMIT TRANSACTION in SQL Server
- WATCH: monitors a specific resource during a transaction. If the monitored resource changed before the transaction was executed then the transaction won’t run. This is a simplified way of implementing a concurrency check in Redis. We’ll see in a bit how this works in practice.
- UNWATCH: stop watching the resources declared by WATCH earlier. We probably won’t use this command too often with transactions since DISCARD and EXEC also unwatch all resources automatically so there’s no need to explicitly call UNWATCH
We’ll simulate the trading of shares between two parties. Run redis-cli in a command window and insert two start values:
SET person:1:shares 150
SET person:2:shares 300
Trading between people is always a good candidate for transactions. If one person sells some amount of goods to another then the one selling should see their level of ownership decrease by the same amount as the level increases for the buying party. We shouldn’t end up in a situation where we see that the seller sold the items but the purchase is not visible on the buyer’s side.
We start a transaction by the…
…command which has no parameters and simply outputs OK in the console. We can now start writing Redis statements one after the other:
INCRBY person:1:shares -50
…i.e. person 1 sells 50 shares. The command is not executed at this point so person 1 still has 150 shares. You’ll see that the command returned QUEUED. Let’s add a corresponding statement for person 2:
INCRBY person:2:shares 50
This will be queued as well. We can now execute the transaction:
…which will respond with the new share levels:
1) (integer) 100
2) (integer) 350
If we perform the same steps but run DISCARD instead of EXEC then nothing happens to the share ownership and the transaction is not executed.
Let’s see what happens if we enter a syntactically wrong operation to the transaction:
INCRBY person:2:shares 50
Note that we didn’t provide the mandatory argument to INCRBY for person 1 and we got the following feedback:
(error) ERR wrong number of arguments for ‘incrby’ command
INCRBY for person 2 was queued. However, the transaction fails at the EXEC command with the following message:
(error) EXECABORT Transaction discarded because of previous errors.
None of the statements in the transaction will be executed. Even if there’s at least one step that was correctly queued before the syntactically wrong command the transaction will be discarded.
So person 1 has 100 shares and person 2 has 350 shares at this point. Let’s see how the WATCH command works. Open another command prompt and start the redis-cli in it as well. Let’s say that we only want to execute the transaction if the share levels were not modified just before. Execute the following commands in the first CLI window:
INCRBY person:1:shares 50
INCRBY person:2:shares -50¨
Don’t run EXEC just yet. Modify the shares of person 1 in the second CLI window:
SET person:1:shares 400
…now now run EXEC in the first window. It will respond with (nil) i.e. no statement was executed. The WATCH statement declared that we wanted to run the transaction on an unchanged state of the resources being watched. We didn’t want to run the EXEC command if the watched resources were modified just before the transaction execution.
That’s all there is about transaction basics in Redis. We’ll turn our attention to messaging in the next post.
You can view all posts related to data storage on this blog here.