首页 > 解决方案 > 无法让@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());
    }
}

标签: spring-boothibernatejpaconcurrencyspring-data-jpa

解决方案


回答如何开始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,因为它不涉及应用程序锁定

推荐阅读