Today the order out of a stock oversold problem, looking for a long time before locating the reason, before using rails also rarely used transactions and locks, here to introduce them. Why use a transaction transaction is a unit of concurrency control, a user-defined sequence of actions. In simple terms, multiple operations within a transaction are either not executed or executed together. Transactions can help developers ensure data consistency in their applications. A common scenario for using a transaction is a bank transfer, which transfers money from one account to another. If there is an error in the middle, the whole process should be reset (Rollback). Example of a bank (pseudo code):
Activerecord::base.transaction do
David.withdrawal (m)
Mary.deposit end
In rails, you can implement a transaction by ActiveRecord the object's class method or an instance method:
Client.transaction do
@client. users.create!
@user. Clients (True). first.destroy!
product.first.destroy!
End
@client. Transaction do
@client. users.create!
@user. Clients (True). first.destroy!
product.first.destroy!
End
You can see in the example above that each transaction contains a number of different model. Calling multiple model objects in the same transaction is a common behavior because transactions are bound to a database connection, not a model object, and transactions are necessary only if you operate on multiple records and want them to be a whole. In addition, Rails has included methods such as #save and #destroy in one transaction, so no explicit calls are needed for a single database record.
Trigger Transaction rollbackThe transaction resets the state of the record through the rollback process. In Rails, rollback is only triggered by a exception. This is a very critical point where code in many transaction blocks does not trigger an exception, so even if an error occurs, the transaction is not rolled back. For example, the following wording:
Activerecord::base.transaction do
david.update_attribute (: Amount, david.amount-100)
Mary.update_ Attribute (: Amount) End
Because Rails, #update_attribute method does not trigger exception when the call fails, it simply returns false, so you must ensure that the function in the transaction block throws an exception when it fails. The correct way to do this is to:
Activerecord::base.transaction do
david.update_attributes! (: Amount => -100)
mary.update_attributes! (: Amount =>) End
It is worth mentioning here, rails in the Convention, with the exclamation point function will usually throw an exception in the failure, so we write our own method with the! Number must also raise an exception in case of failure, so that it conforms to the Rails specification. At the same time, I also see some code in the transaction block in the use of #find_by method, in fact, find_by and other magic methods when the record can not find the return of nil, and #find method can not find the record will throw a ActiveRecord:: Recordnotfound exception. For example, the following examples:
Activerecord::base.transaction do
david = User.find_by_name ("David")
if (david.id!= john.id)
john.update_attributes! (: Amount => -100)
mary.update_attributes! (: Amount =>)
End End
Did you find the logic on the top wrong? The nil object also has an ID method that causes the record to be hidden without the error being found. Also, because Find_by does not throw an exception, the following code is incorrectly executed. This means that sometimes in some situations, we need to throw an exception artificially. So this code is changed to the following form: When the error occurs, the transaction itself rolls back, and the exception is thrown in the outer layer. Therefore, your caller must consider the catch of this exception and handle it accordingly.
Activerecord::base.transaction do
david = User.find_by_name ("David")
raise Activerecord::recordnotfound if David.nil?
if (david.id!= john.id)
john.update_attributes! (: Amount => -100)
mary.update_attributes! (: Amount => ) End
There is a special exception, Activerecord::rollback, when it is thrown, the transaction itself rolls back, but it is not thrown back, so you do not need to catch and handle it externally.
when to use nested transactions. Error use or excessive use of nested exceptions is a more common error. When you nest a transaction in another transaction, there is a parent transaction and a child transaction, which sometimes leads to strange results. For example, here are examples from the Rails API documentation:
User.transaction do
user.create (: username => ' Kotori ')
user.transaction do
user.create (: username = > ' Nemu ')
raise Activerecord::rollback
end
As mentioned above, Activerecord::rollback does not propagate to upper-level methods, so in this example, the parent transaction does not receive an exception that is thrown by the child transaction. Because the contents of the child transaction block are also merged into the parent transaction, in this case, two User records are created.
Nested transactions can be interpreted in such a way that the contents of the child transaction are merged into the parent transaction, and the transaction becomes empty.
In order to ensure that the rollback of a child transaction is known by the parent transaction, you must manually add the Require_new => true option in the child transaction. For example, the following wording:
User.transaction do
user.create (: username => ' Kotori ')
user.transaction (: Requires_new => true)
User.create (: Username => ' Nemu ')
raise Activerecord::rollback
end
Transactions are bound to the current database connection, so if your application writes to multiple databases at the same time, you must wrap the code in a nested transaction. Like what:
Client.transaction do
product.transaction do
product.buy (@quantity)
client.update_attributes! (: Sales _count => @sales_count + 1)
end
transaction-related callbacksIt is mentioned above that #save and #destroy methods are automatically wrapped in a transaction, so that related callbacks, such as #after_save are still part of the transaction, so the callback code may be rolled back.
So, if you want your code to execute outside of a transaction, you can use a callback function such as #after_commit or # After_rollback.
Transaction TrapsDo not capture Activerecord::recordinvalid exceptions within a transaction. Because of some databases, this exception can cause transactions to fail, such as Postgres. Once the transaction fails, the transaction must be rerun from the beginning if the code is to work correctly.
In addition, when testing rollback or transaction rollback related callbacks, it is best to turn off the transactional_fixtures option, which is open in the general test framework.
Common transaction Anti-pattern. Using a transaction in a single record operation unnecessary use of code in a nested transaction transaction does not cause rollback to use transactions in controller
LockLock statements in rails use locks that we often say are pessimistic locks, SQL statements are generally: Select ... where ... for update in general, transactions are used in conjunction with locks, and the issue of order oversold mentioned in this article is caused by unused locks, The code is as follows:
SKU = USER.FIND_BY_ID (sku_id)
activerecord::base.transaction do
if (Sku.stock > 0)
sku.update_ attributes! (stock:sku.stock-1)
order.create! (order_attrs)
End End
In the case of high concurrency, this problem occurs, multiple concurrent simultaneous execution to and meet the conditions of the third line of code, if the statement will be executed, there will be oversold problem, then how to do not oversold it. is to lock.
SKU = USER.FIND_BY_ID (sku_id)
activerecord::base.transaction do
sku.lock!
if (Sku.stock > 0)
sku.update_attributes! ( stock:sku.stock-1)
order.create! ( Order_attrs)
End
So there won't be any oversold cases, I guess someone has a question, so there's no such thing as oversold. When multiple concurrent executions and satisfies the fourth line of code, the statements under the IF condition are executed, NONO, not so, the lock statement executes: SELECT * From ... where ... for update, the detected data is the most recent data, so this problem does not occur.