環境: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.www897cc.com/showinfo-26-59646-0.html被簡單的用戶注冊坑了!出現用戶重復
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 教你如何使用 eval 函數解析和執行字符串代碼,讓你的程序更加智能!
下一篇: 性能工程成熟度模型