环境:SpringBoot3.0.9
简单介绍下出现问题的场景;用户注册后,系统需要发送一封确认邮件。一旦邮件发送成功,用户的状态应更新为“已发送”。但是,在使用Spring Data JPA时,出现了重复数据的问题,注册的用户有2条。
@Servicepublic class UserService { @Resource private UserRepository userRepository ; private static final ThreadPoolExecutor POOL = new ThreadPoolExecutor(2, 2, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>()) ; private final Function<User, Runnable> action = user -> () -> { System.out.printf("给【%s】发送邮件%n", user.getEmail()) ; user.setState(1) ; userRepository.save(user) ; } ; @Transactional public void saveUser(User user) { this.userRepository.save(user) ; POOL.execute(action.apply(user)) ; // 模拟其它操作 TimeUnit.SECONDS.sleep(1) ; } }
测试
@Resourceprivate UserService userService ;@Testpublic void testSave() { User user = new User() ; user.setName("张三") ; user.setEmail("zs@qq.com") ; userService.saveUser(user) ;}
控制台输出
Hibernate: insert into t_user (email, name, state) values (?, ?, ?)给【zs@qq.com】发送邮件Hibernate: select u1_0.id,u1_0.email,u1_0.name,u1_0.state from t_user u1_0 where u1_0.id=?Hibernate: insert into t_user (email, name, state) values (?, ?, ?)Hibernate: update t_user set email=?, name=?, state=? where id=?
输出2条insert,数据库中有2条结果
图片
在保存用户后打印User对象,同时在发邮件处再次查询数据
this.userRepository.save(user) ;System.out.println(user.getId() + " ---- ") ;// 发送邮件处查询数据user.setState(1) ;System.out.println(userRepository.findById(user.getId()).orElseGet(() -> null)) ;
执行结果
Hibernate: insert into t_user (email, name, state) values (?, ?, ?)22 ---- 给【zs@qq.com】发送邮件Hibernate: select u1_0.id,u1_0.email,u1_0.name,u1_0.state from t_user u1_0 where u1_0.id=?null
打印出了User的id值,但是在发送邮件再次查询时打印的null,数据库并没有数据。既然没有数据,那么调用save方法当然会执行insert操作。也就是说在发送邮件操作时,上一步的保存用户的事务并没有提交。
在一个事务中如果你调用save方法,这时候并不会里面将数据插入到数据库中,而是会等到事务提交以后。
在对应的UserRepository中重写findById的方法,然后在方法上添加共享锁 (lock in share mode)
public interface UserRepository extends JpaRepository<User, Long> { @Lock(LockModeType.PESSIMISTIC_READ) Optional<User> findById(Long id);}
接下来在发送邮件的方法出调用上面的findById方法重新从数据库中拉取数据
private final Function<User, Runnable> action = user -> () -> { System.out.printf("给【%s】发送邮件%n", user.getEmail()) ; // 由于加了锁,所以这里会一直等待另外一个线程的事务结束或才会继续执行 User ret = userRepository.findById(user.getId()).get() ; ret.setState(1) ; userRepository.save(ret) ;}
控制台输出
Hibernate: insert into t_user (email, name, state) values (?, ?, ?)26 ---- 给【zs@qq.com】发送邮件Hibernate: select u1_0.id,u1_0.email,u1_0.name,u1_0.state from t_user u1_0 where u1_0.id=? lock in share modeHibernate: select u1_0.id,u1_0.email,u1_0.name,u1_0.state from t_user u1_0 where u1_0.id=?Hibernate: update t_user set email=?, name=?, state=? where id=?
执行的sql上自动添加了共享锁lock in share mode
缩小事务范围,不要在saveUser方法上加事务;调用的save方法内部实现是已经带有了@Transactional注解,如下:
SimpleJpaRepository
@Transactional@Overridepublic <S extends T> S save(S entity) { // ...}
去掉了saveUser方法上的事务后,数据正常insert了一条,update一条。
该种方法实现非常的简单,但是如果saveUser方法中有多个事务操作,这时候你的通过别的方式实现。
通过事件机制,该种方式有如下优点:
实现方式如下
// 定义事件对象class UserCreatedEvent extends ApplicationEvent { private static final long serialVersionUID = 1L; private User source ; public UserCreatedEvent(User user) { super(user); this.source = user ; }}// 定义事件监听器// 在事务提交完成以后执行@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)@Asyncpublic void sendMail(UserCreatedEvent event) { User user = event.getUser(); System.out.printf("%s - 给【%s】发送邮件%n", Thread.currentThread().getName(), user.getEmail()) ; user.setState(1); userRepository.save(user) ;}// 在saveUser方法中需要发送事件@Transactionalpublic void saveUser(User user) { this.userRepository.save(user) ; eventMulticaster.multicastEvent(new UserCreatedEvent(user)) ;}
测试
Hibernate: insert into t_user (email, name, state) values (?, ?, ?)40 ---- task-1 - 给【zs@qq.com】发送邮件Hibernate: select u1_0.id,u1_0.email,u1_0.name,u1_0.state from t_user u1_0 where u1_0.id=?Hibernate: update t_user set email=?, name=?, state=? where id=?
正确执行。
总结:在不同的线程上下文中对同一数据操作,要确保上一个事务正确的提交。否则会出现数据不一致的情况。在本例中是插入后再更新。如果是对已存在的数据做更新操作情况是一样的出现数据不一致的情况。
完毕!!!
本文链接:http://www.28at.com/showinfo-26-59646-0.html被简单的用户注册坑了!出现用户重复
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: 教你如何使用 eval 函数解析和执行字符串代码,让你的程序更加智能!
下一篇: 性能工程成熟度模型