Original link: https://luyuhuang.tech/2023/06/18/simple-transaction.html
In server programming, transactions are often very important, and one of its very important functions is to ensure the integrity of a series of operations. For example, the server needs to execute two modification operations a and b successively to process a request, and both of them may fail; if a succeeds but b fails, the transaction will be responsible for rolling back the modification of a. Just imagine if the a operation is to deduct the balance, and the b operation is to deliver the goods. If the delivery fails, the money has to be returned. If the server uses a database system that supports transactions, such as MySQL, things are easy to handle. Otherwise, implementing similar logic would be tricky and error-prone.
I hope to have a simple transaction system to achieve this effect: For example, in the following code, handler
function handles business logic. As long as an exception is thrown anywhere in handler
function, all modifications in handler
, whether it is _G.DB.last_update_time
, data.order
or data.money
, will be rolled back.
1 |
|
Because our program is single-threaded, there is no need to consider issues such as transaction isolation. So this so-called “transactional system” is just an automatic rollback mechanism.
In fact, I have seen similar transaction implementations before. Its approach is to store the data that needs to be modified (such as data
above) in two copies, one is the official data and the other is the temporary data. The business code modifies the temporary data. If no exception is thrown, the temporary data overwrites the formal data (commit); otherwise, the formal data overwrites the temporary data (rollback). Temporary data is only a shallow copy of official data, and even then, the memory overhead is still very large. And because it is a shallow copy, this mechanism is invalid for fields of reference types (such as table). I don’t think this approach is good enough.
Recently, I was inspired by Nondeterministic Computing in section 4.3 of SICP , and thought that rolling back data is actually very simple-just change it back. When we modify the data, we record the value of the data before the modification. If an exception is caught, the corresponding data is changed back to the value before the modification. Let’s start with pcall
:
1 |
|
Since pcall
can be nested, ie pcall(function() pcall(function() end) end)
, we use the stack to save the transaction context, push the stack in begin
, and pop the stack when commit
and rollback
. So the top of the stack is the context of the current transaction. Call set
to perform the modification operation, which will save the original value of the data in the context.
1 |
|
When Committing, all assignment operations of the current transaction take effect, and the side effects caused by the current transaction are also side effects of the upper-level transaction. The original value of the data recorded in the current transaction needs to be moved to the context of the upper-level transaction (if any). When rolling back, the original value of each set
operation is taken out sequentially from the back to the front, and the data is set to the value before modification to complete the rollback operation.
1 |
|
When using it, it cannot be assigned directly, and it needs to call set
. Of course, it can also be made into a meta table, but I don’t like it very much.
1 |
|
The entire implementation can be said to be very simple and effective, and the overhead is not large. The code was written by me, and there is still room for optimization: the old data storage in stack
can use a more compact data structure; the code can be implemented in C to improve performance, etc.
This article is transferred from: https://luyuhuang.tech/2023/06/18/simple-transaction.html
This site is only for collection, and the copyright belongs to the original author.