spring-boot - 无法让@Transactional 工作 - 需要同时增加/减少一个计数器
问题描述
我想计算评论文章的用户喜欢。有几个“评论”对象,每个对象都有一个“喜欢”计数器。用户可以通过网络请求增加/减少计数器。在增加/减少命令之后,新的“喜欢”计数器值将显示给用户。如果两个用户同时发送一个 Web 请求,那么其中一个请求必须等到另一个请求完成,并且两个用户都会获得不同的“喜欢”计数器值。
为了测试这种竞争条件,我调用了 Thread.sleep(3000) 以便我可以从两个不同的浏览器手动运行相同的 Web 请求。
我的第一种方法是使用 @Transactional(isolation = Isolation.SERIALIZABLE) 注释。代码检索 Review 实体,更新“likes”计数器值并保存它。
我希望如果我的 ReviewServiceImpl 方法是从我的两个手动 Web 请求中调用的,第二个调用会到达,而第一个调用仍在 sleep() 函数中,第一个事务仍然打开,第二个调用甚至无法启动事务直到第一次调用完成并提交第一个事务。所以第二个请求应该从第一个请求中获取更新的值。
但是,两个浏览器窗口都显示相同的“喜欢”计数器值,因此某处存在错误。
我的第二种方法是使用Spring、JPA 和 Hibernate 中建议的数据库更新查询 - 如何在没有并发问题的情况下增加计数器。这会更好,因为没有“喜欢”的更新会丢失。但是页面显示更新之前的值,因此控制器以某种方式检索过时的数据。
完整的示例代码可以在这里找到:https ://github.com/schirmacher/transactionproblem 。为方便起见,我复制了下面的基本代码。
请告知如何解决此问题。
实体审查.java:
@Entity
public class Review implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Long likes;
public String getName() {
return name;
}
public Long getLikes() {
return likes;
}
public void setLikes(Long likes) {
this.likes = likes;
}
}
存储库 ReviewRepository.java:
interface ReviewRepository extends JpaRepository<Review, Long> {
Review save(Review review);
List<Review> findAll();
Review findByName(String name);
@Transactional
@Modifying
@Query(value = "UPDATE Review c SET c.likes = c.likes + 1 WHERE c = :review")
void incrementLikes(Review review);
}
服务评论ServiceImpl.java:
@Component("categoryService")
class ReviewServiceImpl implements ReviewService {
...
// this code does not work
@Transactional(isolation = Isolation.SERIALIZABLE)
@Override
public void incrementLikes_variant1(String name) {
Review review = reviewRepository.findByName(name);
Long likes = review.getLikes();
likes = likes + 1;
review.setLikes(likes);
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// this code does not work either
@Override
public void incrementLikes_variant2(String name) {
Review review = reviewRepository.findByName(name);
reviewRepository.incrementLikes(review);
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
控制器 LikeController.java:
@Controller
public class LikeController {
@Autowired
private ReviewService categoryService;
@RequestMapping("/")
@ResponseBody
public String increaseLike() {
categoryService.incrementLikes_variant2("A random movie");
Review review = categoryService.findByName("A random movie");
return String.format("Movie '%s' has %d likes", review.getName(), review.getLikes());
}
}
解决方案
回答如何开始incrementLikes_variant1
工作
问题
- 我怀疑问题出在内存中 h2 数据库没有做
Isolation.SERIALIZABLE
表级锁定。但这只是我的怀疑
解决方案
- 无论如何,行级锁比表级锁好。
@Transactional
@Override
public void incrementLikes_variant1(String name) {
Review review = reviewRepository.findByNameForUpdate(name);
Long likes = review.getLikes();
likes = likes + 1;
review.setLikes(likes);
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
interface ReviewRepository extends JpaRepository<Review, Long> {
Review findByName(String name);
@Query(value = "Select r from Review r WHERE r.name = :name")
@Lock(LockModeType.PESSIMISTIC_READ)
Review findByNameForUpdate(String name);
...
}
笔记
- 并且使用支持的乐观并发方法
@Version
比这种行级锁要好。 - 您可以实现自动重试,因为它适合您的用例
OptimisticConcurrencyException
发生时
回答如何开始incrementLikes_variant2
工作
public interface ReviewRepositoryCustom {
void refresh(Review review);
}
public class ReviewRepositoryImpl implements ReviewRepositoryCustom {
@Autowired
EntityManager em;
@Override
public void refresh(Review review) {
em.refresh(review);
}
}
interface ReviewRepository extends JpaRepository<Review, Long>,
ReviewRepositoryCustom
@Override
@Transactional
public void incrementLikes_variant2(String name) {
Review review = reviewRepository.findByName(name);
reviewRepository.incrementLikes(review);
reviewRepository.refresh(review);
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
笔记:
- Variant2 优于 Variant1,因为它不涉及应用程序锁定
推荐阅读
- symfony - Symfony - 更新 OneToOne 学说关系
- python - 如何从纯文本(python)中提取特定的日期格式?
- android - 如何在 kotlin 协程中正确使用暂停而不是延迟?
- javascript - Angular 中的动态子组件
- java - 在指定的时间后运行测试
- c# - 如果您能够修改源代码,为什么还要使用扩展方法?
- powershell - 我的表单上的 OK 按钮在第一次启动时不显示,如果我重新启动它会显示
- docker - 使用 Golang SDK 运行需要用户输入的 Docker 命令
- reactjs - Jest & react-router-dom: React.createElement: type is invalid - 期望一个字符串(用于内置组件)......但得到:未定义
- android - 如何将 APK 的拆分副本从 Flutter 添加到 Google Play 商店?