This is why’s 129th original article

Hello, I’m Why.

A few days ago, I saw a technical problem on a platform. It was very interesting.

The two technical points involved, we usually use more development, but belongs to a small detail, dig down, or a little interesting.

Here, let me show you what the problem is and explain it to you, okay

First, the student gave a code snippet:

He says he has a func method that does two things:

  • 1. First query the inventory of goods in the database.
  • 2. If there is inventory, subtract one from the inventory to simulate the sale of goods.

For the second thing, the student who asked the question actually wrote two operations in it, so I’ll break it down a bit more:

  • 2.1 Reduce the inventory by one.
  • 2.2 Insert order data into order table.

Obviously, both operations operate on the database, and should be atomic operations.

Therefore, a @Transactional annotation is added to the method.

Then, to solve the problem of concurrent access, he used a lock to wrap the entire code, ensuring that only one request can be executed at a time to reduce inventory and generate orders in a single structure.

It’s perfect.

The MySQL database isolation mechanism uses the repeatable read level.

This is where the problem comes in.

In the case of high concurrency, it is assumed that there really are multiple threads calling func methods at the same time.

To ensure that a transaction cannot be oversold, it is important that the transaction is opened and committed in a complete package between lock and UNLOCK.

Obviously the transaction must be started after lock.

So the key is does the transaction commit before unlock?

If the transaction commits before unlock, there is no problem.

Because the transaction has been committed, it means that the inventory must be reduced, and the lock has not been released yet, so no other thread can enter.

Here’s a simple diagram:

After unlock, another thread comes in, queries the database, and the value is the value minus the inventory.

However, if the transaction is committed after unlock, then something interesting happens and you are likely to oversold.

The diagram above looks like this, notice that the last two steps are reversed:

Let me give you an example.

Let’s say I only have one in stock.

At this point, thread A and thread B request the order.

A requested to get the lock first, and then found out that the inventory was one, and the order could be placed. After the ordering process, the inventory was reduced to 0.

However, user A performs the unlock operation first to release the lock.

Thread B immediately rushed to get the lock and executed the inventory query operation.

Note that thread A has not yet committed the transaction at this time, so the inventory read by thread B is still 1. If the program does not do A good job of control, it also goes through the order process.

Oh oh, it’s oversold.

So, to reiterate the question:

In the case of the sample code above, there is no problem if the transaction commits before the UNLOCK. But there is a problem if you go after unlock.

So does the transaction commit before or after unlock?

This matter, first understand the problem, then we first press not table. You can think about it a little bit.

I’d like to start by saying something that I’ve understated, passed over, and you probably didn’t notice:

Obviously the transaction must be started after lock.

This sentence was not said by me, but by the student who asked the question:

Do you have the slightest doubt?

Why is that obvious? Where is it obvious? Why not start a transaction as soon as you enter a method?

Please give me proof.

Come on, take a look at the evidence.

Transaction start time

Proof. We need to find it in the source code.

In addition, I have to say that the source code for Spring transactions is very clear and easy to understand, and seems to have almost no obstacles.

So if you don’t know how to gnaw the source code, then the transaction of the source code, may be you open a hole in the source code.

All right, enough talking. Let’s find out.

The answer lies in this method:

org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin

Take a look at the line I’ve framed below:

Switching JDBC Connection [HikariProxyConnection@946359486 wrapping com.mysql.jdbc.JDBC4Connection@7a24806] to manual commit

You know, I’m a tech blogger who occasionally teaches words.

Switching, Switching

Connection C.

Manual commit: Manual commit.

Switching … to … Convert what into why.

Unexpectedly, while learning technology this time, I not only learned a few words, but also learned a grammar.

So, the above sentence is very simple to translate:

Switch the database connection to manual commit.

Then, let’s look at the code logic that prints this line of log, the boxed part of the code.

I’ll take it out separately:

The logic is clear: change the AutoCommit parameter of the connection from true to false.

So now the question is, is the transaction started at this point?

I don’t think it’s on. It’s just ready.

There’s a little bit of a difference between startup and readiness, which is the step before startup.

So how do transactions start?

  • The first is to use a statement to start a transaction, which is to explicitly start a transaction. Such as the BEGIN or Start Transaction statement. The accompanying commit statements are COMMIT and rollback statements are rollback.

  • The default value of autoCOMMIT is 1, which means that automatic commit of transactions is enabled. If we execute set autocommit=0, this command will turn off auto-commit for this thread. This means that if you only execute a SELECT statement, the transaction is started and will not commit automatically. The transaction persists until you actively execute a COMMIT or ROLLBACK statement, or disconnect.

Obviously, the second approach is taken in Spring.

The above code con.setautoCommit (false) simply turns the auto-commit of this link off.

When will the transaction actually start?

The preceding begin/start transaction commands are not the starting point of a transaction, the transaction is not actually started until the first InnoDB table statement after them is executed.

If you want to start a transaction immediately, use the start Transaction with consistent snapshot command. Note that this command does not make sense to read at the committed isolation level (RC) and has the same effect as using start Transaction directly.

Back to the previous question: When will the first SQL statement be executed?

Right after the Lock code.

Therefore, it is obvious that the transaction must be started after lock.

This is a simple “obviously”, to set the stage for you.

Next, I’ll give you a look at the GIF to make it more intuitive.

Let’s start with this SQL:

select * from information_schema.innodb_trx;

Without further explanation, you need to know that this is a query for the current database which transactions are executing.

The transaction can be queried only after the query statement is executed, indicating that the transaction is really started:

Finally, we turn our attention to this method comment:

This parameter is true by default because switching to auto-commit is a heavy operation in some JDBC drivers.

So where is it set to true?

I don’t give up until I see the code.

So, take a look.

The setAutoCommit method has several implementation classes, and I don’t know which one will go:

So, we can set a breakpoint on the following interface:

java.sql.Connection#setAutoCommit

Then restart the program and the IDE will automatically determine which implementation class you want to use:

As you can see, the default is true.

Wait, you don’t really think I want you to see this true, do you?

I want you to know this debugging technique.

I don’t know how many friends have asked me: there are so many interface implementation classes, how can I know where to break?

I said: it’s easy, just put a breakpoint on the first line of each implementation class.

Then he said: Stop it, I always give your articles one click triple link.

I was moved, since I am such a good reader, OF course, I can directly break the breakpoint on the interface of the trick to teach him.

All right, let’s cut to the chase.

One more small detail, and this section ends.

If you look at the beginning of this section, I say that the answer lies in this method:

org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin

I just gave you the answer, cutting out the process of discovery.

But this one, like the derivation of a mathematical formula, leaves you looking confused when you skip a step.

Like this mouse:

So, how do I know where to break?

The answer is the call stack.

Let me show you my code:

What else is too? Start on line 26, break the method entry, and run:

Hey, if you look at the call stack, I’ve boxed it up here:

Look at the name. Aren’t you curious?

It is simply jumping feet, Shouting you: point me, quick, leng to do what, you TM hurry me. I have a secret here!

And then, as gently as THIS, I came here:

org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction

Here is an aspect, which can be interpreted as executing our business code logic inside the try:

In the try block, before executing our business code, there is a line like this:

There you have it. Just before this line of code, lightly tap a breakpoint and debug it, and you will find the method described at the beginning of this section:

org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin

Don’t believe it? You see, I kid you not.

There are only three calls between them:

And there you have it.

Call stack, another debugging source code tip, always working, for you.

Before or after

Okay, appetizers up front, maybe some of you are already full from appetizers.

It’s okay. Now for dinner, you can still eat it if you press the button.

Taking the previous code as an example, the flow looks like this:

  • 1. Take the lock first.
  • 2. Query the inventory.
  • 3. Determine if inventory is still available.
  • 4. Execute the logic of reducing inventory and creating orders if there is inventory.
  • 5. Return if no inventory is available.
  • 6. Release the lock.

So the code looks like this:

Exactly as in our previous code snippet, there are transactions and locks:

Back to our original question:

In the case of the sample code above does the transaction commit before or after unlock?

We can bring in a specific scenario.

For example, there are 10 Top-equipped ipads in my database. The original price is 1.6W yuan per iPad, but now the unit price is 1W yuan per iPad. Is this price enough to kill?

There’s only 10 of them anyway, so here’s what I have in my database,

And THEN I get 100 people to loot. Is that too much?

I’m using CountDownLatch to simulate concurrency:

Perform it and see the result immediately:

The part on the right of the GIF:

The code above is the browser request that triggers the Controller.

And then in the middle is the product list, which has 10 inventories.

At the bottom is the order table, without any data.

Once the code is triggered, the inventory is zero, no problem.

But there were 20 orders!

That’s 10 oversold iPad Pro tops!

Oversold, not in the campaign budget!

That’s 1.6 watts for one, and 16 watts for 10.

So ugly, harmless, even look trivial code, let me lose a whole 16W.

Well, the answer comes with the result.

In the case of the sample code above, the transaction commits after unlock.

In fact, if you analyze it carefully, you can guess, it must come after unlock.

The description “after unlock” is a bit confusing because it is a very special operation.

It makes a lot of sense to change the description:

In the case of the sample code above, the transaction commits after the method runs.

You fine product, this description is not so deceptive, and even you will suddenly realize: this is not common sense?

After the method, before I go into the specifics, I want to take a quick look at why this code was written.

I guess it might be.

The initial code structure looks like this:

And then, in the concurrent scenario, the inventory is a shared resource, and it has to be locked.

So here’s what happened:

When I look at the code again later, I realize that this third step has to be a transaction.

So the code looks like this:

The evolution path made sense, and the resulting code looked almost flawless.

But what went wrong?

To find the answer

The answer is still in this class:

org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction

Earlier, when we talked about transaction startup, we talked about line 382.

And then inside the try block is our business code.

Now, we’re going to look at the transaction commit, so focus on what I’ve framed.

First of all, in the catch block, line 392, the method name is pretty obvious:

CompleteTransactionAfterThrowing complete transaction submitted after throw an exception.

As you can see in my code, I only use the @Transactional annotation and do not specify exceptions.

So here’s the question:

What are the exceptions that Spring manages to roll back by default?

If you don’t know the answer, look at the source code with questions.

If you know the answer but haven’t actually seen the code, you can also look for the source code.

If you know the answer and have seen this part of the source code, review the past and learn something new.

Answer first: The default rollback exception is RuntimeException or Error.

I just need to throw a RuntimeException subclass in the business code, like this:

Then set a breakpoint at line 392 to start debugging:

Just debug a few steps down and you’ll get to this method:

org.springframework.transaction.interceptor.RuleBasedTransactionAttribute#rollbackOn

Finding that the winner object is empty, the logic follows:

return super.rollbackOn(ex);

The answer lies behind this line of code:

If the exception type is RuntimeException or a subclass of Error, return true, that is, call rollback:

If false is returned, no rollback is required, call commit:

So how do I make it return false?

It’s very simple. It works like this:

The frame leaves a hole for you, so you use it.

When I change the code to the above, then restart the project and access the code again.

Let’s look for specific implementation logic where a specified exception does not roll back.

It’s also in the method we just saw:

You see, the winner is no longer null. It’s a NoRollbackRuleAttribute object.

So this line of code returns false:

return ! (winner instanceof NoRollbackRuleAttribute);

Else branch commit ();

As I write here, I suddenly thought of a SAO operation, and may even become a sand sculpture interview question:

Does this operation roll back or not roll back?

If you see this code in your project, you’re going to call it stupid.

But that’s what interviewers love to do.

When I thought of this question, I didn’t know what the answer was, but I knew it was in the source code:

After the for loop, winner is a RollbackRuleAttribute object, so the following code returns true and needs to be rolled back:

return ! (winner instanceof NoRollbackRuleAttribute);

So the question becomes why is winner RollbackRuleAttribute after the for loop?

The answer is you have to debug it yourself, and it’s easy to understand, but IT’s hard for me to describe.

In short: The reason winner is a RollbackRuleAttribute is because the looped list added the RollbackRuleAttribute object first.

So why is the RollbackRuleAttribute object added to the collection in the first place?

org.springframework.transaction.annotation.SpringTransactionAnnotationParser#parseTransactionAnnotation(org.springframew ork.core.annotation.AnnotationAttributes)

Don’t ask. Ask because that’s what the code says.

Why is the code written this way?

I think the developer who designed the code thought rollbackFor had a higher priority than noRollbackFor.

One more question:

How does Spring source code match the current exception that needs to be rolled back?

Don’t think about it that complicated, just simple, just recurse, and then layer by layer find the parent class, compare the name and then you’re done.

Note the comments in the screenshot:

One is Found it!

Found, matched, and happy with an exclamation point.

If we’ve gone as far as we can go and haven’t found it…

As far as I’m concerned, I’ve been working as far as I can. So far..” The meaning of. The extent or extent of the work is emphasized.

So, the above sentence means:

If we have gone as far as we can without a match, the code would have to be written like this:

Exception classes, the farthest of which is throwable.class. No match, return -1.

Well, through two useless knowledge points, along with some practical English, about the business code out of the abnormal rollback or submit this piece of code is almost the same.

But I still recommend you to Debug it yourself, but it’s fun.

Then we move on to normal submissions.

So in this block of code, we talked about try, we talked about catch.

All I need is finally.

I read some articles online that finally is the place to commit.

That’s wrong, man.

This is just resetting the database connection.

The method has been made clear to you:

Spring transactions are done based on ThreadLocal. Within the current transaction, there may be some personalization of isolation level, rollback type, timeout duration, and so on.

Regardless of whether the transaction returns normally or if an exception occurs, as soon as it completes, all these personalized configurations must be restored to the default configuration.

So, put it in the finally code block to execute.

The real commit is this line of code:

So here comes the question:

When I get here, does the transaction have to commit?

Don’t be too sure, man. Look at the code:

org.springframework.transaction.support.AbstractPlatformTransactionManager#commit

There are two more decisions to make before committing. If the transaction is marked rollback-only, it must still be rolled back.

Plus, look at the log.

The lock was released before I even submitted the transaction?

Moving on to commit logic, we come across an old friend:

HikariCP, the default connection pool after SpringBoot 2.0, is much better, as described in the previous article.

There is not much space to cover transaction commits.

Show us the way:

com.mysql.cj.protocol.a.NativeProtocol#sendQueryString

Make a break point at the entrance to this method:

Then you’ll notice that a lot of SQL passes through this place.

So, in order for you to debug smoothly, you need to set the breakpoint:

This stops only when the SQL statement is commit.

Another debugging detail. Here you go. You’re welcome.

Now that we know why, I’ll change the code slightly:

Replaced ReentrantLock with synchronized.

Do you think there will be problems with this code?

Those of you who said no questions please reflect on it.

The principle of this place is exactly the same as what was said in front of it, and there are problems for sure.

This lock is wrong.

Transactional Transactional Transactional Transactional Transactional Transactional Transactional Transactional Transactional Transactional Transactional Transactional Transactional Transactional Transactional Transactional Transactional Transactional Transactional Transactional Transactional Transactional

Don’t say you wrote it, just say you found it while reviewing the code.

In addition, remember to expand, now are all cluster services, add a distributed lock.

But the principle is the same.

Now that we’re talking about distributed locks, we’re going to have to go back and forth with the interviewer.

You brought it up and led the interviewer to your main battleground.

Here’s an interview tip for you. You’re welcome.

The solution

Now we know the cause of the problem.

There’s a solution all around the corner.

Use locks correctly to put the entire transaction within the scope of the lock:

This ensures that the transaction commits before unlock.

Isn’t it?

If you are right, that’s all for today. Please go back and wait for notice.

Don’t take them to the ditch, my friend.

Do you think this transaction will work?

Tips to here have not figured out the students, quickly search for several scenarios of transaction failure.

Here’s a scenario that works:

It’s just the way you inject yourself, I find it disgusting.

If there is such code in the project, it must be that the code layer is not good, the project structure is extremely chaotic.

Is not recommended.

You can also use programmatic transactions to write and control the start, commit, and rollback of transactions yourself.

Transactional is better than using @Transactional directly.

There is also a slightly sluttier solution.

Leave everything else unchanged, just change @Transactional:

Serialize the isolation levels, run the test cases again, and never oversold.

You don’t even need the logic of locking.

Do you think it’s okay?

What?

Serialization performance can not keep up with ah!

This is too pessimistic, for the same row of data, read and write will be locked. When the read/write lock conflicts, subsequent transactions queue up.

This SAO operation, just know, don’t use.

Just think of it as a piece of useless knowledge.

However, if you are a scenario that does not pursue performance, this useless knowledge becomes a dirty operation.

rollback-only

Rollback -only was mentioned earlier, so I just mentioned it in one sentence for the sake of better writing. In fact, it also has a lot of stories, so I will take a separate section to briefly talk about it and simulate the scene for you.

In the future, you will feel very friendly when you see this anomaly.

Spring’s transaction propagation level defaults to REQUIRED, meaning that if there is no transaction currently in place, a new transaction is created, and if one already exists in the context, it is shared.

Directly on the code:

There are two transactions: sellProduct and sellProductBiz. SellProductBiz is an inner transaction that throws an exception.

When the entire logic is executed, this exception is thrown:

Transaction rolled back because it has been marked as rollback-only

Based on the stack of exceptions, you can find this place, which appeared earlier:

So, we just need to analyze why this if condition is satisfied, and then we can sort of figure out the context.

if (! shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly())

The previous shouldCommitOnGlobalRollbackOnly defaults to false:

Problem is reduced to: defStatus. IsGlobalRollbackOnly () why is true?

Why is that?

Because sellProductBiz throws an exception, will call completeTransactionAfterThrowing method rollback logic.

There must be something wrong with this method.

org.springframework.transaction.support.AbstractPlatformTransactionManager#processRollback

In this case, set the rollbackOnly of the link to true.

So, when a later transaction wants to commit, check this parameter, oh oh, roll back.

It goes something like this:

If this is not the exception you expected, how do you resolve it?

Understanding the propagation mechanism of transactions is as simple as:

In this way, a new business, running up no problems, mutual interference.

One last word

Ok, see here arrange a concern, zhou is more original very tired, need some positive feedback.

Thank you for reading, I insist on original, very welcome and thank you for your attention.

I’m Why, or you can call me Crooked, a programmer who mainly writes code, often writes articles, and occasionally shoots videos.