Hello,
Days ago I posted here about one of the nicest features of Oracle 23c: the lock-free reservations. Please read that post if you haven’t yet 🙂 .
One of the things that let me intrigued was to understand how Oracle manages the journal table, how a session could only see its own reservations and if there would be a way of reading all the pending reservations.
So, I went a little further on the investigation. I tried to find if there was a VPD, generated a 10043 trace of a query against the journal table to see if Oracle was adding a filter predicated in the background, and finally I dumped a block of the journal table to see the content.
There in the dump, I saw the two transactions in the ITL, and then realized the obvious: of course, it’s just the case that the INSERT to the journal table and the pending UPDATE to the main table they represent, are both in the same transaction!
This makes total sense: as always has been the case, only the session that is executing a transaction can see the modified data. Other sessions cannot, because this would be a dirty read, that is never allowed in Oracle.
So, this solve my understanding of how Oracle manages the pending updates to a reservable column.
But, you know, this is kind of weird in a way, because as opposed to what happens in a regular transaction with regular row locks, a transaction with reservations is not totally isolated: yes, you heard right! In a totally isolated transaction, you don’t need to know what is happening in other sessions, since they are completely isolated. One transaction can only change the same data after the previous finishes manipulating that row. This, via row locks, is what guarantee the isolation.
In a transaction with lock-free reservations, on the other hand, your transaction needs to see what is pending in all other transactions for that table, to then decide for example if your update will work or will receive an error for violating a constraint.
So, I still think it would be important to have a global view of all pending reservations in a table. Consider for example, you issue an UPDATE decreasing 5 against a reservable column that has a value of 20 and receive an error for violating the check constraint that avoids negative numbers. You go and check again, value is 20, I’m decreasing 5, final result should be 15, why am I receiving an error?
Yes, we know technically that this is because there are 16 or more reserved across all other transactions, but at this point we simply cannot determine what transaction is reserving it. If I knew it, a DBA could for example kill the reserving session to release the reservation if my transaction was for some reason more important than the others.
The fact is that there is a lack of visibility of pending reservations that thus affect other transactions. And I think this should be improved.
Let me explain in more details the difference between a regular transaction and one with lock-free reservations:
Regular Transaction without lock-free reservations
If you instruct Oracle to update a non-reservable column with the following statement:
update my_table set my_column = my_column + 1;
What Oracle will really do is:
- Read the CURRENT value of MY_COLUMN. This is a consistent read, so it considers the value of the last committed transaction. Any ongoing transactions are ignored (remember the ACID acronym for transactions? Theoretically a transaction occurs entirely in one atomic moment which is the COMMIT time; before that moment, that transaction does not exist).
- Calculate the new value of MY_COLUMN, adding 1 to the current value.
- Check if the new value would hit any existing constraint. If yes, throw an error, if not store the new value in MY_COLUMN and lock the row.
So, let’s say the value was 3. Your update will add 1, so put 4 in the column.
Before the COMMIT, other transactions could only see the “old” value, before your update (consistent read), and would be locked if tried to update it. After the COMMIT the row will be unlocked, meaning any other session can now read the new value and also change it in a new transaction if needed.
Transaction on lock-free reservations columns
This is not the case for lock-free reservations! If you instruct Oracle to do a similar update but now on a reservable column:
update my_table set my_reservable_column = my_reservable_column + 1;
this is what Oracle will do:
- Read the CURRENT value of MY_RESERVABLE_COLUMN.
- Get all the pending reserved updates FROM ALL TRANSACTIONS to get the theoretical current value. So, Oracle is “invading” other’s transactions are to check what they are doing.
- Calculate the theoretical final value considering all pending reservations and your new update.
- Check if this theoretical final value will violate an existing constraint. If yes, throw an error, if not store the new reservation in the journal table.
Then, when you COMMIT the transaction:
- Read again the CURRENT value of MY_RESERVABLE_COLUMN. It may be changed since the first verification.
- Please note that the constraint validation does not need to be rechecked since the update was reserved.
- Calculate the new value with your Increment/decrement.
- Store the new value in MY_RESERVABLE_COLUMN.
I hope this made the differences more clear to you.
So, what’s my point when I said it’s weird not being able see all the pending reservations?
Let’s imagine a very common case that would benefit from lock-free reservations: to control the inventory of products. So I would have a table with my products, and a reservable column INVENTORY that would contain the number of available pieces of a that particular product, and it would be RESERVABLE to allow lock-free reservations. This way, you could start a new order, put a lot of different products in the cart, and since the moment you put a certain quantity in your cart, that quantity is reserved for you until you issue the order (COMMIT your transaction).
This is the perfect case for lock-free reservations. You don’t lock other people putting the same product in the cart. Each different order can reserve the quantity they need, having guaranteed the reservations and avoiding the disgust of receiving an “out of stock” error message when checking out. Perfect, amazing.
But now, let’s see you want to purchase all the available inventory for a certain product, no matter how many they are. The ideal situation in this case would be: let’s see how many units are in stock, deduct all the reservations (meaning, people with the item in the cart), and create an order with all the remaining.
For example, if you have a current inventory of 20 and 6 units reserved in other people’s carts, your ideal order would be of 14 units, to bring the inventory down to 0.
Well, this is not exactly possible with lock-free reservations because you simply cannot see all the pending reservations.
So, translating it back to Oracle, if I have a CHECK constraint avoiding the inventory to go negative, and I see there are 20 units in stock, I can TRY to put the 20 in the cart. This would translate into an UPDATE like:
update products set inventory = inventory - 20;
As explained above, Oracle would verify the current value and all the pending reservations and decide if my update would be allowed or not. Considering the example with 6 units already reserved, my UPDATE would violate the CHECK constraint and if I really wanted to buy all available, I would need to be guessing values, decreasing the quantity one by one, until I do not violate the constraint.
They would be blind tests! Why? Because I cannot see the total number of reservations.
You may argue that my examples are not strong enough, or this is a situation that would not exist in real world. Maybe, but I still think there should be a way to see all the pending transactions. I don’t think it would be considered a dirty read per se, since it’s not a change executed but not committed; in this case, it’s a change NOT executed yet, just reserved to take place at COMMIT.
That’s it.
I know, a lot of text and not much sample codes this time, but I think this was necessary.
Let me know you thoughts. Do you agree with my point? Was I clear enough explaining it?
See you on the next one!