Hi, I’m Jack

Business chapter, back again! In the third article, I will share a real case of a problem encountered by the project team to take a deeper look at Spring transactions

A, the opening

Initially, our table has only one data with ID 2, name wangjie, and age set to 0

1 Do not add the synchronized keyword

So first of all, what are the problems with this program

    @Transactional
    public  void transactionalMethod(){
        User user = userDao.findOne(2);
        user.setAge(user.getAge()+1);
        userDao.updateUser(user);

    }
Copy the code

instructions

TransactionalMethod (), findOne(), age (), age (), age (), age (), age ()); The effect is to add one year to user id 2

This is an update operation, so think about it, what’s the problem with this method?

Yes, because there is no lock, in the case of concurrency, it is likely that the thread is not safe, resulting in execution results that are not necessarily the same as expected

Let’s test it out:

@GetMapping("/transactionalMethod") public void transactionalMethod() { final CountDownLatch latch = new CountDownLatch(1000); try { for (int i = 0; i < latch.getCount() ; i++) { new Thread(() -> { userService.transactionalMethod(); latch.countDown(); }).start(); } }catch (Exception e){ System.out.println(e.getMessage()); }finally { latch.countDown(); }}Copy the code

instructions

In the Controller class, create the transactionalMethod method. Method to simulate 1000 concurrent requests

Expectations: Age updated to 1,000 years

After startup, we access the /transactionalMethod interface with the following result:

Results 1

That is, the wangjie user’s age has been updated to 93, which is inconsistent with what was expected

Take a look at execution logs

The discovery of multiple identical data verifies that concurrency problems did occur during execution, causing multiple threads to obtain the same value at the same time, resulting in dirty data execution

So, next, we lock the method transactionalMethod with the synchronized keyword:

2 Add the synchronized keyword

    @Transactional
    public  synchronized void transactionalWithSynchronized(){
        User user = userDao.findOne(2);
        user.setAge(user.getAge()+1);
        userDao.updateUser(user);
        
Copy the code

instructions

Add synchronized keyword on the basis of the original method

The upper calls, call transactionalWithSynchronized method instead

@GetMapping("/transactionalMethod") public void transactionalMethod() { final CountDownLatch latch = new CountDownLatch(1000); try { for (int i = 0; i < latch.getCount() ; i++) { new Thread(() -> { userService.transactionalWithSynchronized(); latch.countDown(); }).start(); } }catch (Exception e){ System.out.println(e.getMessage()); }finally { latch.countDown(); }}Copy the code

The result is 1000 threads running concurrently:

The results of 2

Yi? It’s 817. It’s still not a thousand, right? Why is that?

We can see that the SQL execution process, there is a duplicate age, indicating concurrency

It’s not scientific to add synchronized, but there’s a concurrency conflict? Isn’t that a weird bug? A colleague at the time said he was skeptical at first

How? When you encounter problems, you have to find ways to solve them. For bugs that do not conform to common sense, many people tend to find answers by comparing them with other normal scenarios. Although this method is not highly technical, I personally think it is a relatively efficient way. Just figure out what the differences are, think about the causes, and then work on them, okay

So we are comparing with other normal use synchronized methods first, find the only difference is that: @ Transactional annotation transactionalWithSynchronized method

Let’s try removing the @Transactional annotation

3 Add synchronized(remove the @Transactional annotation)

    public synchronized void transactionalWithSynchronized(){
        User user = userDao.findOne(2);
        user.setAge(user.getAge()+1);
        userDao.updateUser(user);

    }
Copy the code

Results 3

That’s normal. That’s normal. For this scenario, our problem is solved!

That is, the problem phenomenon is that when you combine the @Transactional annotation with the synchronized keyword, the concurrency problem remains

But, you have to wonder, why on earth?

Two, principle analysis

1. Cause Analysis

Spring declarative transactions, using Spring’S AOP idea, start a transaction before the target method executes, and commit or roll back the transaction after the target method executes

Due to Spring’s AOP transaction mechanism, transactions for methods annotated with the @Transactional annotation are handled by a spring-generated proxy class. The proxy class does not commit the transaction after a thread executes the method and releases the lock. That is, a thread opens a transaction before entering synchronized, and then locks a method using synchronized

Let’s examine the request process for a synchronized method with the @Transactional annotation

Therefore, for thread A in the figure, when it finishes executing the code and does not commit the transaction, in the case of concurrent requests, it is easy for thread B to also request. Thread A and thread B are in the same transaction

2 Solution

The cause of the problem has been found, so how can we solve it?

For this problem, we simply remove the @Transactional annotation that we can add to the method, because the Transactional annotation is the same for this method

However, without cutting corners, we need to consider a generic solution for scenarios where transactions must be added and concurrency must be controlled

The problem is that the thread releases the lock before committing the transaction, causing other threads to be in the same transaction situation with it

So, we only need the transactionalWithSynchronized before () method, is called the method of this method with a synchronized keyword. That is, locking a transaction before it is opened ensures thread synchronization

For example, the controller in the class testTransactionalWithSynchronized add synchronized keyword () method

private synchronized void testTransactionalWithSynchronized() { userService.transactionalWithSynchronized(); } @GetMapping("/testTransactionalWithSynchronized") public void invokeMethod() { final CountDownLatch latch = new CountDownLatch(1000); try { for (int i = 0; i < latch.getCount() ; i++) { new Thread(() -> { testTransactionalWithSynchronized(); latch.countDown(); }).start(); } }catch (Exception e){ System.out.println(e.getMessage()); }finally { latch.countDown(); }}Copy the code

Then in the invokeMethod () in the concurrent invocations testTransactionalWithSynchronized () method, observation:

Results 4

As expected, the age becomes 1000, and the log does not have the same value of age obtained by each thread

Third, summary

The Transactional thread releases the lock before committing the transaction, causing other threads to be in the same Transactional situation as it is

This can be solved by adding the synchronized keyword to the method that calls this method, causing the thread to commit the transaction before releasing the lock

As mentioned in previous articles, the @Transactional annotation is used in Spring

If people really understand this, this problem should be discovered very quickly, if not, then such problems will certainly exist and become a problem

In fact, the truth is that things outside the scope of knowledge will feel difficult; Those who are within the realm of knowledge will find it easy to feel fulfilled

If you want to make yourself feel more fulfilled, keep learning and broaden your knowledge with the main code of green plum