5 个关于Spring @Transactional的坑

https://codete.com/blog/5-common-spring-transactional-pitfalls/

 

The article is a part of JPA & Spring pitfalls series. Below you can find the actual list of all articles of the series:

Spring

  • 5 Common Spring @Transactional pitfalls
  • Mixing Spring transaction propagation modes

JPA

  • JPA: 8 common pitfalls
  • JPA: N+1 SELECT problem
  • JPA: Pagination pitfalls

Preface

The @Transactional annotation is probably the most randomly used annotation in the whole Java development world – and it’s terrifying!

I noticed that, not knowing why, it is usually perceived as a kind of a magical annotation. How many times I have seen those StackOverflow answers suggesting: “Try adding @Transactional” with any further explanation or have heard developers debugging a piece of unworking code and saying to each other: “Eh, maybe something changes when we add this, you know, @Transactional thing here?” But the worst part is that sometimes it seems like it fixed the problem, so developers just leave it and don’t even try to find out what it really changed and whether it didn’t break anything somewhere else.

In this article I’ll try to outline some of the most common misunderstandings related to @Transactional. I’m assuming that you are familiar with Spring, JPA and Spring Data.

Hopefully after this read, @Transactional won’t be magical to you any longer.

Which @Transactional?

Although @Transactional is present in both Spring and JavaEE (javax.transaction package), we’ll be using the one from Spring Framework. It’s generally a better practice since it is more natural to Spring applications and at the same time it offers more options like timeoutisolation, etc.

@Transactional – quick recap

To recap, always when you see a method like this:

 

1

2

3

4

@Transactional

public void registerNewAccount() {

  // business code

}

you should remember that when you call such a method, the invocation in fact will be wrapped with transaction handling code similar to this:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

UserTransaction userTransaction = entityManager.getTransaction();

try {

 // begin a new transaction if expected

 // (depending on the current transaction context and/or propagation mode setting)

  userTransaction.begin();

 

  registerNewAccount(); // the actual method invocation

 

  userTransaction.commit();

} catch(RuntimeException e) {

  userTransaction.rollback(); // initiate rollback if business code fails

  throw e;

}

Of course this code is a simplification of what really happens in the background, but it should be good enough to visualize and memorize.

Pitfall #1: Redundant @Transactional or JPA calls

This isn’t actually a trap, but just a mistake that I see it very often (too often) during code review. Please look at this code first:

 

1

2

3

4

5

6

@Transactional

public void changeName(long id, String name) {

 User user = userRepository.getById(id);

 user.setName(name);

 userRepository.save(user);

}

At first sight, there’s nothing wrong with this code and indeed it works perfectly fine in terms of functionality. However, it instantly reveals that the author wasn’t sure about how @Transactional works.

When a method is transactional, then entities retrieved within this transaction are in managed state, which means that all changes made to them will be populated to the database automatically at the end of the transaction. Therefore either the save() call is redundant and the code should look like this:

 

1

2

3

4

5

6

@Transactional

public void changeName(long id, String name) {

 User user = userRepository.getById(id);

 user.setName(name);

 // userRepository.save(user);

}

or, if we don’t need to perform this within a transaction, it could be:

 

1

2

3

4

5

6

// @Transactional

public void changeName(long id, String name) {

 User user = userRepository.getById(id);

 user.setName(name);

 userRepository.save(user);

}

What is more important, besides only the general misconception, writing the code like this may lead to other problems in the future. Imagine that at some point someone would like to add user name validation and writes a code like this:

 

1

2

3

4

5

6

7

8

9

@Transactional

public void changeName(long id, String name) {

  User user = userRepository.getById(id);

  user.setName(name);

  

  if (StringUtils.isNotEmpty(name)) {

    userRepository.save(user);

  }

}

It looks okay, but it won’t work as someone expected. Since this is a transactional method, the user entity is in managed state and therefore name change will be populated to the database anyways. This if statement doesn’t change anything.

If @Transactional annotation wasn’t on the method-level, but on the class-level, then it would be even more difficult to catch that there’s something wrong with this code.

Pitfall #2: @Transactional ignored?

Have you ever annotated a method with @Transactional (or e.g. @Async) and it didn’t work? As if it was totally ignored by Spring? This is because annotations like these can’t be (at least by default) put just on any method, the following two conditions must be met to let Spring take an action:

  • The method visibility can’t be any other than public.
  • The invocation must come from outside of the bean.

This is due to how Spring proxying work by default (using CGLIB proxies). I won’t dive into details, because it’s a topic wide enough to write another article, but generally speaking, when you autowire a bean of type Sample, Spring in fact doesn’t provide you exactly with an instance of Sample. Instead it injects a generated proxy class which extends Sample (that’s the reason why you can’t make your spring bean classes final) and overrides its public methods to be able to add extra behaviour (like transactional support).

That’s why methods with @Transactional must be public (so Spring can easily override them) and also that’s why the invocation must come from outside (only then it may go through a proxy, Spring can’t replace this reference with a proxy reference).

Solution 1

Extract the method to another class and make it public.

Solution 2

Use AspectJ weaving instead of default proxy-based Spring AOP. AspectJ is capable of working with both: non-public methods and self-invocations.

Solution 3 (only for self-invocation)

Disclaimer: I wouldn’t use this “solution” in the production code, because it’s more error-prone, harder to understand and forces using field-injection. Anyways, I find it interesting enough to give you at least an overview.

The following code fails when we call userService.createUser(“test”) because of ignored @Transactional (due to self-invocation):

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

@Service

public class UserService {

 

   @PersistenceContext

   private EntityManager entityManager;

  

   public User createUser(String name) {

       User newUser = new User(name);

       return this.saveUser(newUser); // self-invocation, "this" is not a proxy

   }

 

   @Transactional

   public User saveUser(User newUser) {

       entityManager.persist(newUser);

       return newUser;

   }

 

}

Since you can’t use self-invocation, because Spring is not able to intercept this, you can autowire a “self” proxy reference and use it instead of this:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

@Service

public class UserService {

 

   @PersistenceContext

   private EntityManager entityManager;

 

   @Autowired

   private UserService _self; // proxy reference injected

 

   public User createUser(String name) {

       User newUser = new User(name);

       return _self.saveUser(newUser);

   }

 

   @Transactional

   public User saveUser(User newUser) {

       entityManager.persist(newUser);

       return newUser;

   }

 

}

 

Pitfall #3: @Transactional(readOnly = true)

It’s only a hint

Firstly, the readOnly parameter doesn’t guarantee its behaviour, is only a hint that may or may not be taken into account. From documentation:

This just serves as a hint for the actual transaction subsystem; it will not necessarily cause failure of write access attempts. A transaction manager which cannot interpret the read-only hint will not throw an exception when asked for a read-only transaction but rather silently ignore the hint.

Therefore, its behaviour may vary between JPA implementations and even between their versions – and it really happens, at least in Hibernate it has changed a few times in the last few years. So more than relying on it too much, better make sure that your code does just pure reads within a specific transaction if you expect that.

In Hibernate, currently, it causes setting Session’s FlushType to MANUAL, which means that the transaction won’t be committed and thus any modifying operations will be silently ignored.

Depends on propagation setting

Another important thing to remember is that readOnly hint will be applied only when the corresponding @Transactionalcauses starting a completely new transaction.

Therefore, it’s closely related to the propagation setting. For example: for SUPPORT, readOnlyflag won’t ever be used; for REQUIRES_NEW always; for REQUIRED it depends on whether we already are in the transactional context or not, etc.

If you don’t feel comfortable with transaction propagation settings, you might be interested in my previous article .

Is it worth using this param for truly read-only transactions?

Generally we should simply avoid starting DB transactions for read-only operations as they are unnecessary, can lead to database deadlocks, worsen performance and throughput.

The only argument for starting read-only transactions that I can possibly see is the recent cache memory consumption optimization since Spring 5.1 proposed by Vlad Mihalcea.

Edit 2019-08-19: 

As Юрий pointed out in the comments, it’s worth mantioning that we can’t avoid read transactions when using Spring Data JPA since read methods on CRUD Repository are already marked with @Transactional(readOnly = true) (see docs).

Pitfall #4: Rollbacks

The rule when transaction rollbacks are triggered automatically is very simple, but worth reminding: by default a transaction will be rolled back if any unchecked exception is thrown within it, whereas checked exceptions don’t trigger rollbacks.

We can customize this behaviour with parameters:

  • noRollbackFor – to specify runtime exception, which shouldn’t cause rollback
  • rollbackFor – to indicate which checked exception should trigger rollbacks

Pitfall #5: Propagation modes and isolation levels

Propagation and isolation are two advanced and essential topics related to transactions. If you don’t feel at least comfortable with basic use cases of each, I firmly recommend you making up for it, because experimenting with them randomly (what I sometimes see on StackOverflow) may cost you much more time spent on debugging, since this kind of bugs is exceptionally hard to figure out.

If you already know about propagation, make sure to check out my another article with a few practical examples of what may happen if you mix propagation modes not carefully enough.

Essence in a nutshell

  • Any change made within a transaction to an entity (retrieved within the same transaction) will automatically be populated to the database at the end of the transaction, without the need of explicit manual updates.
  • Don’t write redundant JPA calls or @Transactionaljust “for more safety”. It may bring more risk than safety.
  • @Transactionalworks only when the annotated method is public and invoked from another bean. Otherwise, the annotation will be silently ignored.
  • As a rule of thumb: don’t use readOnly = true parameter until it’s really necessary.
  • By default, only unchecked exceptions trigger rollbacks, checked exceptions do not. It can be customized with rollbackFor and noRollbackFor parameters.
  • Learn how different isolation levels and propagation modes work. It may save you a lot of time one day.

Thanks for reaching that far, I hope that you’ve enjoyed reading!

你可能感兴趣的:(spring)