浅谈Spring中的@EventListener和@TransactionalEventListener

liujie
liujie
发布于 2023-11-01 / 49 阅读
0
0

浅谈Spring中的@EventListener和@TransactionalEventListener

问题描述:

方法A使用spring 的默认级别事务,在方法A中更新了mysql数据库数据,然后publish了一个event事件,方法B订阅了该事件,需要读取到A中更新的数据,但却读取不到,导致了业务异常。

以下文章来自掘金:


Spring中的发布/订阅模式非常好用,我个人经常将它用于监听程序中的事件并做相应的处理,这样有利于分离关注点和代码解耦。而本文的目的就在于描述@EventListener和@TransactionalEventListener的区别,帮助大家更好地使用发布/订阅模式。


代码示例

假设我们有一个用户服务,在用户注册后会自动为用户分配一名顾问。从业务角度来看,“分配顾问”不属于“用户注册”这个流程中,出于业务解耦的目的,我们把这个操作独立出来。 简单起见,我们的用户类中只包含需要的字段:

@Entity
@Table(name = "user")
public class User {
    @Id
    private Long id;
    private String username;
    private Long counselorId;
    
    public User(Long id, String username) {
        this.id = id;
        this.username = username;
    }
	
    // getter and setter 
	...
    // getter and setter 
}

用户DAO类:

public interface UserRepository extends JpaRepository<User, Long> {

}

用户Service类

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private ApplicationEventPublisher eventPublisher;

    public void save(User user) {
        eventPublisher.publishEvent(user);
        userRepository.save(user);
    }
}

顾问Service类,用于为用户分配顾问:

@Service
public class CouselorService {
    @Autowired
    private UserRepository userRepository;

    @EventListener
    public void userAllocation(User user) {
        user.setCounselorId(1L);
        userRepository.save(user);
    }
}

上述代码写好之后我们来测试一下:

@SpringBootTest
@ExtendWith(SpringExtension.class)
class UserServiceTest {
    @Autowired
    private UserService userService;
    @Autowired
    private UserRepository userRepository;

    @Test
    public void test() {
        userService.save(new User(1L, "lixiang"));
        User user = userRepository.findById(1L).get();
        assertThat(user.getId(), is(1L));
        assertThat(user.getUsername(), is("lixiang"));
        assertThat(user.getCounselorId(), is(1L));
        userRepository.deleteById(1L);
    }
}

测试通过,我们成功地利用@EventListener将业务解耦开来。

@EventListener

如上所述,我们通过@EventListener成功地将分配顾问的逻辑移到了一个独立模块中,但此时我们真的将业务完全解耦了吗?其实并没有。@EventListener将userAllocation注册为用户注册事件的监听者,但它的执行是和用户注册绑定在同一个事务里的,也就是说如果分配顾问的逻辑抛出了异常,那么这个用户也不会被注册,它们两个的逻辑还是捆绑在一起的。而我想要的结果是,无论分配顾问是否成功,用户必须先成功注册(即注册用户的事务已经提交,记录已经保存到数据库中)。这时就需要@TransactionalEventListener出马了。

@TransactionalEventListener

@TransactionalEventListener可以说是@EventListener的增强版,可以更好地配合数据库事务。通过该注解注册的监听者可以在以下几个事务阶段中执行(通过phase参数设置):

  • AFTER_COMMIT(默认):当事务成功提交后执行

  • AFTER_ROLLBACK:当事务失败回滚后执行

  • AFTER_COMPLETION:当事务完成后执行(无论事务是否成功)

  • BEFORE_COMMIT:在事务提交之前执行 除非将fallbackExecution设置为true,否则当没有处于一个事务中时,@TransactionalEventListener注册的监听方法不会被执行。

接下来我们将@EventListener替换为@TransactionalEventListener试一下:

@TransactionalEventListener
public void userAllocation(User user) {
    user.setCounselorId(1L);
    userRepository.save(user);
}

执行测试,发现测试失败:

java.lang.AssertionError: 
Expected: is <1L>
     but: was null
Expected :is <1L>
Actual   :null

我们本来期待用户的counselorId为1,但却是null。为什么会这样?我在TransactionSynchronization.afterCommint()方法的注释上找到了答案:

NOTE: The transaction will have been committed already, but the transactional resources
might still be active and accessible. As a consequence, any data access code triggered at 
this point will still "participate" in the original transaction, allowing to perform some 
cleanup (with no commit following anymore!), unless it explicitly declares that it needs to 
run in a separate transaction. Hence: Use PROPAGATION_REQUIRES_NEW for any transactional 
operation that is called from here.

大概意思是说:事务已经提交了,虽然后续的操作仍然会“参与到”原先的事务当中,但是不会被再commit一次,想要在监听者方法中做一些写入操作,需要显式声明一个新事务。 根据Spring作者给出的建议,我们需要在userAllocation方法上声明一个新事务:

@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void userAllocation(User user) {
    user.setCounselorId(1L);
    userRepository.save(user);
}

再次执行测试我们将看到测试通过。

补充

当监听者方法耗时很长时,我们可以考虑使用@Async注解来使方法异步执行:

@TransactionalEventListener
@Async
public void userAllocation(User user) {
    user.setCounselorId(1L);
    userRepository.save(user);
}

此时不再需要声明事务,因为Spring中的事务默认是和线程绑定的,当新启动一条线程时,将开启另一个独立的事务。

总结

@TransactionalEventListener在事务场景下是对@EventListener的一个很好的替代方案,并且你可以决定它是异步还是同步执行。需要注意的是在默认情况下监听者方法会被绑定到发布者所在的事务中,你不能在监听者方法中将任何数据保存到数据库中,因为事务已经被提交了,并且再也没有机会重新提交。要解决这个问题有三种方法:

  1. 在方法上声明一个新事务

  2. 使用@Async让方法异步执行,新线程中会自动开启一个新事务

  3. @TransactionalEventListener的phase参数设置为BEFORE_COMMIT,但是这种方法会导致之前所说的业务不能解耦的问题


作者:lixiangdude
链接:https://juejin.cn/post/6908302868218920974
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


评论