成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

被簡單的用戶注冊坑了!出現用戶重復

開發 前端
在不同的線程上下文中對同一數據操作,要確保上一個事務正確的提交。否則會出現數據不一致的情況。在本例中是插入后再更新。如果是對已存在的數據做更新操作情況是一樣的出現數據不一致的情況。

環境:SpringBoot3.0.9

1. 背景介紹

簡單介紹下出現問題的場景;用戶注冊后,系統需要發送一封確認郵件。一旦郵件發送成功,用戶的狀態應更新為“已發送”。但是,在使用Spring Data JPA時,出現了重復數據的問題,注冊的用戶有2條。

2. 問題代碼

@Service
public 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) ;
  }
  
}

測試

@Resource
private UserService userService ;


@Test
public 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條結果

圖片圖片

3. 原因分析

在保存用戶后打印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操作。也就是說在發送郵件操作時,上一步的保存用戶的事務并沒有提交。

4. 解決辦法

在一個事務中如果你調用save方法,這時候并不會里面將數據插入到數據庫中,而是會等到事務提交以后。

解決方法1:

在對應的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 mode
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=?

執行的sql上自動添加了共享鎖lock in share mode

解決辦法2:

縮小事務范圍,不要在saveUser方法上加事務;調用的save方法內部實現是已經帶有了@Transactional注解,如下:

SimpleJpaRepository

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

去掉了saveUser方法上的事務后,數據正常insert了一條,update一條。

該種方法實現非常的簡單,但是如果saveUser方法中有多個事務操作,這時候你的通過別的方式實現。

解決方法3:

通過事件機制,該種方式有如下優點:

  • 解耦:通過事件,你可以將用戶注冊與發送郵件兩個操作分離,使它們之間不存在直接的依賴關系。這樣,如果以后需要更改郵件發送邏輯或替換為其他服務,只需要修改事件監聽器,而不需要修改用戶注冊的代碼。
  • 靈活性:事件機制提供了高度的靈活性。你可以在用戶注冊成功后觸發多個不同的事件,每個事件可以有不同的處理邏輯。這樣,你可以很容易地擴展功能,例如除了發送郵件外,還可以觸發其他相關的業務邏輯。
  • 異步處理:事件處理通常是異步的,這意味著用戶注冊后,不需要等待郵件發送完成。這種異步處理可以提高應用的響應速度和吞吐量。
  • 可擴展性:由于事件處理是基于發布-訂閱模式的,因此你可以輕松地添加新的事件監聽器來擴展功能。如果以后需要集成其他服務或功能,例如發送短信、推送通知等,只需要創建相應的事件監聽器即可。

實現方式如下

// 定義事件對象
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)
@Async
public 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方法中需要發送事件
@Transactional
public 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=?

正確執行。

總結:在不同的線程上下文中對同一數據操作,要確保上一個事務正確的提交。否則會出現數據不一致的情況。在本例中是插入后再更新。如果是對已存在的數據做更新操作情況是一樣的出現數據不一致的情況。

完畢!!!

責任編輯:武曉燕 來源: Spring全家桶實戰案例源碼
相關推薦

2009-12-23 10:46:38

WPF實現用戶界面

2018-09-26 07:33:31

2009-12-30 09:45:52

Silverlight

2010-01-28 10:00:54

linux用戶注銷logout

2015-11-11 17:20:48

2012-05-04 09:28:49

Linux

2016-05-17 10:03:39

用戶體驗運維可度量

2009-12-11 11:22:39

Linux用戶口令

2010-08-04 10:48:17

路由器

2023-07-28 07:43:55

2025-03-05 07:58:30

2012-02-15 09:43:43

iCloud云計算蘋果

2013-10-11 10:00:11

大數據應用icloud云服務

2021-10-12 16:44:01

微軟Windows 11Windows

2009-11-13 09:36:10

UNIX通訊命令操作系統

2023-07-31 10:38:07

2018-04-02 10:16:00

bug代碼安卓

2016-10-24 23:18:55

數據分析漏斗留存率

2017-08-31 15:57:53

數據Oracle用戶密碼

2024-09-22 10:46:33

數據飛輪算法
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产h在线 | 精品亚洲一区二区 | 欧美一区二区三区久久精品 | 国产一二区免费视频 | 一区二区国产精品 | sese视频在线观看 | 国产第一页在线观看 | 国产精品一区三区 | 亚洲欧洲成人av每日更新 | 欧美日韩中文字幕 | 国产精品亚洲精品日韩已方 | 日韩久久中文字幕 | 欧美成人免费在线视频 | 永久www成人看片 | 91美女在线观看 | 日韩成人在线网站 | 天天操欧美 | 一区二区三区观看视频 | 正在播放国产精品 | 99热热热热 | 在线观看视频一区 | 毛片链接| 中文字幕在线观看国产 | 欧美日在线 | 国产高清久久久 | 国产激情毛片 | 青久草视频 | 久久精品中文 | 国产视频中文字幕 | 欧美黄色性生活视频 | 国产69久久精品成人看动漫 | av免费电影在线 | 北条麻妃99精品青青久久主播 | 精品乱子伦一区二区三区 | www.日韩av.com | 国产精品久久久久久亚洲调教 | 亚洲一区二区三区四区视频 | 免费看国产片在线观看 | 欧美午夜视频 | 欧美在线视频网站 | 国产黄色在线观看 |