需求
用户注册完成以后,异步给用户发送邮件,代码如下:
// 用户注册的controller入口
@GetMapping(value="/register")
public int register() {
return userService.register();
}
// 用户注册的service
@Transactional(rollbackFor = Exception.class)
public int register() {
// 注册用户
User emp = new User(id.incrementAndGet(), "若鱼1919", 1);
int result = userMapper.insert(emp);
// 为了不阻塞主线程,异步发送邮件
emailService.sendEmail(emp.getId());
// 模拟代码执行受塞
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return result;
}
// emailService
@Async
public void sendEmail(int userId) {
User user = userMapper.selectById(userId);
if(user == null){
throw new RuntimeException("user信息不存在");
}
logger.info("向id是{}的用户{}发送邮件成功", userId, user.getName());
}
用户注册的方法上添加了@Transactional
注解用于事务控制,用户注册完成以后,为了不阻塞主线程,使用异步的方式向用户发邮件,同时在用户注册方法的最后加了一个Thread.sleep(1000);
模拟代码执行受阻的情形。这个时候因为发邮件是异步执行的,因此会先于注册方法执行,此时事务还没提交,因此根据id查询不到数据,导致抛出异常:user信息不存在。
解决办法
1)在异步方法中sleep一下
这种方式最简单,但是sleep的时间不好控制,不推荐
2)编程式事务处理
这种方式可行,但是,代码的侵入性太高,不推荐
public int register() {
User emp = null;
int result = 0;
// 手动开启事务
TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
TransactionStatus transaction = txManager.getTransaction(transactionDefinition);
try{
// 注册用户
emp = new User(id.incrementAndGet(), "若鱼1919", 1);
result = userMapper.insert(emp);
// 提交事务
txManager.commit(transaction);
}catch(Exception e){
// 回滚事务
txManager.rollback(transaction);
throw new RuntimeException(e);
}
// 异步发送邮件
emailService.sendEmail(emp.getId());
// 模拟代码执行受塞
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return result;
}
因为在发送邮件之前,已经把事务提交掉了,因此不会出现查不到数据的情况。
3)使用事务事件监听器(推荐)
首先定义一个事件,注册成功以后,把这个事件发送出来:
public class UserRegisterEvent extends ApplicationEvent {
private Integer userId;
public UserRegisterEvent(Object source, Integer userId) {
super(source);
this.userId = userId;
}
public Integer getUserId() {
return userId;
}
}
用户注册的逻辑还是使用声明式事务注解@Transactional
,但是注册完成以后,改成发UserRegisterEvent
消息:
@Transactional(rollbackFor = Exception.class)
public int register() {
// 注册用户
User emp = new User(id.incrementAndGet(), "若鱼1919", 1);
int result = userMapper.insert(emp);
// 异步发送邮件
applicationContext.publishEvent(new UserRegisterEvent(this, emp.getId()));
//emailService.sendEmail(emp.getId());
// 模拟代码执行受塞
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return result;
}
然后写一个监听器,监听这个事件:
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, classes = UserRegisterEvent.class)
public void onRegisterEvent(UserRegisterEvent event) {
sendEmail(event.getUserId());
}
需要在监听的处理方法上添加@TransactionalEventListener
注解,phase的取值有以下几种:
public enum TransactionPhase {
BEFORE_COMMIT, //事务提交之前
AFTER_COMMIT, // 事务提交以后
AFTER_ROLLBACK, // 事务回滚以后
AFTER_COMPLETION; // 事务执行完以后
private TransactionPhase() {
}
}
以上代码的含义就是当事务提交以后会回调到这个监听方法中,因为事务已经提交,此时是可以正常查询到数据的。
既没有代码的侵入,也不需要担心sleep时间太久,推荐!
源码下载