当前位置:首页 > 科技  > 软件

被简单的用户注册坑了!出现用户重复

来源: 责编: 时间:2024-01-10 09:35:10 129观看
导读环境:SpringBoot3.0.91. 背景介绍简单介绍下出现问题的场景;用户注册后,系统需要发送一封确认邮件。一旦邮件发送成功,用户的状态应更新为“已发送”。但是,在使用Spring Data JPA时,出现了重复数据的问题,注册的用户有2条。

环境:SpringBoot3.0.9aXz28资讯网——每日最新资讯28at.com

1. 背景介绍

简单介绍下出现问题的场景;用户注册后,系统需要发送一封确认邮件。一旦邮件发送成功,用户的状态应更新为“已发送”。但是,在使用Spring Data JPA时,出现了重复数据的问题,注册的用户有2条。aXz28资讯网——每日最新资讯28at.com

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) ;  }  }

测试aXz28资讯网——每日最新资讯28at.com

@Resourceprivate UserService userService ;@Testpublic void testSave() {  User user = new User() ;  user.setName("张三") ;  user.setEmail("zs@qq.com") ;  userService.saveUser(user) ;}

控制台输出aXz28资讯网——每日最新资讯28at.com

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条结果aXz28资讯网——每日最新资讯28at.com

图片图片aXz28资讯网——每日最新资讯28at.com

3. 原因分析

在保存用户后打印User对象,同时在发邮件处再次查询数据aXz28资讯网——每日最新资讯28at.com

this.userRepository.save(user) ;System.out.println(user.getId() + " ---- ")  ;// 发送邮件处查询数据user.setState(1) ;System.out.println(userRepository.findById(user.getId()).orElseGet(() -> null)) ;

执行结果aXz28资讯网——每日最新资讯28at.com

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操作。也就是说在发送邮件操作时,上一步的保存用户的事务并没有提交。aXz28资讯网——每日最新资讯28at.com

4. 解决办法

在一个事务中如果你调用save方法,这时候并不会里面将数据插入到数据库中,而是会等到事务提交以后。aXz28资讯网——每日最新资讯28at.com

解决方法1:

在对应的UserRepository中重写findById的方法,然后在方法上添加共享锁    (lock in share modeaXz28资讯网——每日最新资讯28at.com

public interface UserRepository extends JpaRepository<User, Long> {  @Lock(LockModeType.PESSIMISTIC_READ)  Optional<User> findById(Long id);}

接下来在发送邮件的方法出调用上面的findById方法重新从数据库中拉取数据aXz28资讯网——每日最新资讯28at.com

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) ;}

控制台输出aXz28资讯网——每日最新资讯28at.com

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 modeaXz28资讯网——每日最新资讯28at.com

解决办法2:

缩小事务范围,不要在saveUser方法上加事务;调用的save方法内部实现是已经带有了@Transactional注解,如下:aXz28资讯网——每日最新资讯28at.com

SimpleJpaRepositoryaXz28资讯网——每日最新资讯28at.com

@Transactional@Overridepublic <S extends T> S save(S entity) {  // ...}

去掉了saveUser方法上的事务后,数据正常insert了一条,update一条。aXz28资讯网——每日最新资讯28at.com

该种方法实现非常的简单,但是如果saveUser方法中有多个事务操作,这时候你的通过别的方式实现。aXz28资讯网——每日最新资讯28at.com

解决方法3:

通过事件机制,该种方式有如下优点:aXz28资讯网——每日最新资讯28at.com

  • 解耦:通过事件,你可以将用户注册与发送邮件两个操作分离,使它们之间不存在直接的依赖关系。这样,如果以后需要更改邮件发送逻辑或替换为其他服务,只需要修改事件监听器,而不需要修改用户注册的代码。
  • 灵活性:事件机制提供了高度的灵活性。你可以在用户注册成功后触发多个不同的事件,每个事件可以有不同的处理逻辑。这样,你可以很容易地扩展功能,例如除了发送邮件外,还可以触发其他相关的业务逻辑。
  • 异步处理:事件处理通常是异步的,这意味着用户注册后,不需要等待邮件发送完成。这种异步处理可以提高应用的响应速度和吞吐量。
  • 可扩展性:由于事件处理是基于发布-订阅模式的,因此你可以轻松地添加新的事件监听器来扩展功能。如果以后需要集成其他服务或功能,例如发送短信、推送通知等,只需要创建相应的事件监听器即可。

实现方式如下aXz28资讯网——每日最新资讯28at.com

// 定义事件对象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)) ;}

测试aXz28资讯网——每日最新资讯28at.com

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=?

正确执行。aXz28资讯网——每日最新资讯28at.com

总结:在不同的线程上下文中对同一数据操作,要确保上一个事务正确的提交。否则会出现数据不一致的情况。在本例中是插入后再更新。如果是对已存在的数据做更新操作情况是一样的出现数据不一致的情况。aXz28资讯网——每日最新资讯28at.com

完毕!!!aXz28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-59646-0.html被简单的用户注册坑了!出现用户重复

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 教你如何使用 eval 函数解析和执行字符串代码,让你的程序更加智能!

下一篇: 性能工程成熟度模型

标签:
  • 热门焦点
Top