多JVM部署下基于JPA限制单用户数据库request记录数的方案问询
解决方案
以下几个方案均兼容JPA使用,且能避免跨JVM的并发竞争问题:
方案1:带条件的原子INSERT(最推荐,无额外锁开销)
直接利用数据库的原子操作特性,将「计数检查+插入」合并为单条SQL执行,数据库会保证该操作的原子性,不存在并发穿透问题。
你可以通过JPA的@Modifying+@Query注解实现:
@Modifying @Transactional @Query(value = "INSERT INTO request (user_id, text) " + "SELECT :userId, :text " + "WHERE (SELECT COUNT(*) FROM request WHERE user_id = :userId) < 3", nativeQuery = true) int insertRequestIfAllowed(@Param("userId") Long userId, @Param("text") String text);
调用该方法后判断返回值即可:
- 返回1:插入成功,符合条数限制
- 返回0:该用户已达3条上限,插入失败
目前主流关系型数据库(MySQL 5.7+、PostgreSQL、Oracle等)均支持该写法。
方案2:悲观显式行锁(逻辑简单,适合并发不高的场景)
如果不想写原生SQL,可以在查询计数的时候加排他锁,阻塞其他事务对同userId的计数查询,保证事务内操作的串行化:
@Transactional public boolean addRequest(Long userId, String text) { // 加排他锁查询该用户的request数量,此时其他同userId的同类查询会被阻塞 TypedQuery<Long> countQuery = em.createQuery( "SELECT COUNT(r.id) FROM Request r WHERE r.userId = :userId", Long.class); countQuery.setParameter("userId", userId); // 加悲观写锁 countQuery.setLockMode(LockModeType.PESSIMISTIC_WRITE); Long count = countQuery.getSingleResult(); if (count >= 3) { return false; } Request request = new Request(); request.setUserId(userId); request.setText(text); em.persist(request); return true; }
该方案依赖数据库的行锁机制,跨JVM的同userId操作会串行执行,不会出现并发穿透。建议提前给
userId字段加索引,避免锁升级为表锁影响全局性能。
方案3:唯一约束兜底(防护补充,可搭配前两个方案使用)
可以额外给Request表新增一个seq字段,存储该用户的第N条请求,值固定为1/2/3,然后给(userId, seq)加联合唯一约束。
插入的时候给seq赋值为当前用户的条数+1,如果插入触发唯一约束异常,说明用户已达上限,捕获异常返回失败即可。该方案可以作为兜底防护,避免任何逻辑漏洞导致的超发。
内容的提问来源于stack exchange,提问作者Taz
相关产品推荐
相关产品推荐

