Spring Data JPA进阶(五):事务和锁

x33g5p2x  于2021-12-19 转载在 其他  
字(4.2k)|赞(0)|评价(0)|浏览(431)
事务

默认情况下,Spring Data JPA提供的CRUD方法都添加了事务,这里的事务使用的是Spring的事务管理机制。对于读操作来说,事务的readOnly属性是设置的true(默认值是false),而其他操作都是设置的一个空的@Transactional注解,所以使用的都是Spring事务的默认配置。

如何在持久层使用事务

如果你想覆盖某个方法的事务配置,可以在自己的接口里面覆盖那个方法,然后加上Transactional注解。

public interface UserRepository extends CrudRepository<User, Long> {

  @Override
  @Transactional(timeout = 10)
  public List<User> findAll();
    
}
统一配置一个接口的事务

你也可以在接口上面使用@Transactional注解进行统一配置:

@Transactional(readOnly = true)
public interface UserRepository extends JpaRepository<User, Long> {

  List<User> findByLastname(String lastname);

  @Modifying
  @Transactional
  @Query("delete from User u where u.active = false")
  void deleteInactiveUsers();
}

如果同时在接口和方法上进行了配置,方法上的配置会具有更高的优先级。

对查询语句设置readOnly为true有什么好处呢?如果你设置了readOnly为true,hibernate的flush模式会自动设置为NEVER,这样就可以让hibernate跳过“脏检查”阶段,可以显著提升大对象(很多层子对象组成的对象树)的查询性能。

多个Repository组成的事务

如果涉及到多个Repository的事务,可以在Service层做处理,或者使用门面模式。这里可能涉及到“事务的传播级别”方面的知识点。

示例代码:

@Service
class UserManagementImpl implements UserManagement {

  private final UserRepository userRepository;
  private final RoleRepository roleRepository;

  @Autowired
  public UserManagementImpl(UserRepository userRepository, RoleRepository roleRepository) {
    this.userRepository = userRepository;
    this.roleRepository = roleRepository;
  }

  @Transactional
  public void addRoleToAllUsers(String roleName) {

    Role role = roleRepository.findByName(roleName);

    for (User user : userRepository.findAll()) {
      user.addRole(role);
      userRepository.save(user);
    }
}
  • 如何使用锁
    使用非常简单,只需要在持久层的方法上添加@Lock注解并选择锁的类型即可:
interface UserRepository extends Repository<User, Long> {

  @Lock(LockModeType.READ)
  List<User> findByLastname(String lastname);
}

这个注解是在org.springframework.data.jpa.repository包下,里面只有LockModeType value这一个属性。

需要注意的是,要在事务里面使用锁,否则会报TransactionRequiredException异常。

同时,我们也可以在EntityManager中使用锁:

// find的时候指定锁
entityManager.find(Department.class, 1, LockModeType.PESSIMISTIC_READ);

// find后锁住一个实体
Department department = entityManager.find(Department.class, 1);
entityManager.lock(department, LockModeType.PESSIMISTIC_READ);

// 锁住query results
TypedQuery<Department> query = entityManager
        .createQuery("select d from Department d", Department.class);
query.setLockMode(LockModeType.PESSIMISTIC_READ);
List<Department> departments = query.getResultList();
  • 锁的类型
    LockModeType是一个枚举类,这个类是在javax.persistence包下的。打开这个枚举即可看到所有锁的类型。
public enum LockModeType
{
    READ, // 与下面的OPTIMISTIC同义

    WRITE, // 与下面的OPTIMISTIC_FORCE_INCREMENT同义

    OPTIMISTIC,

    OPTIMISTIC_FORCE_INCREMENT,

    PESSIMISTIC_READ,

    PESSIMISTIC_WRITE,

    PESSIMISTIC_FORCE_INCREMENT,

    NONE // 无锁
}

下面分别介绍这些锁。首先READ与WRITE是别名,NONE是无锁,所以不作过多解释。主要介绍这五个锁:

  1. OPTIMISTIC
  2. OPTIMISTIC_FORCE_INCREMENT
  3. PESSIMISTIC_READ
  4. PESSIMISTIC_WRITE
  5. PESSIMISTIC_FORCE_INCREMENT
乐观锁和悲观锁

这两种锁都可以避免两个事务中的其中一个,在不知情的情况下覆盖另一个事务的数据。

OPTIMISTIC开头的代表“乐观锁”,以PESSIMISTIC开头的是“悲观锁”。乐观锁和悲观锁有什么区别?

  • 乐观锁
    乐观锁是在JPA层面实现的。顾名思义,乐观锁比较“乐观”,它假设冲突很少发生,即使发生,抛出一个错误也比想办法避免它们更容易接受和简单。核心思想就是在实体中定义一个“version”字段,可以是数值类型或时间戳类型。在读取的时候,就取到这个字段。然后在修改了实体的数值,保存的时候,就会去数据库对比这个version,如果version一致,就执行更新,否则就不执行更新。
@Entity
public class Student{
    @Id
    private int id;
    private String name;
    private BigDecimal money;
    @Version
    private int version;

    // getters and setters
}

对于实体中有添加了@Version注解的列,JPA会自动对该实体使用乐观锁OPTIMISTIC_FORCE_INCREMENT。你不需要使用任何锁命令。但是你也可以在Repository里面通过@Lock注解去自定义自己想要的锁。

使用乐观锁,打印sql,会发现在执行save方法的时候,会把version作为条件:

update ... whereid=? and version=?

所以乐观锁是Spring Data JPA在控制。你也可以不使用Spring Data JPA的乐观锁,而自己新增一个字段,在执行更新操作的时候手动去实现。

OPTIMISTICOPTIMISTIC_FORCE_INCREMENT有什么区别呢?
OPTIMISTIC在读操作时不会更新Version字段的值,只有在写操作的时候会
OPTIMISTIC_FORCE_INCREMENT在读和写操作时都会更新Version字段的值

  • 悲观锁
    悲观锁是在数据库层面实现的,JPA只是把请求交给数据库。悲观锁比较“悲观”,意思是只要开启事务的时候,就会对数据进行加锁操作,在事务结束后才解锁。

而有些数据库是不支持PESSIMISTIC_READ的,JPA规范中说到,不需要提供PESSIMISTIC_READ(因为许多DB只支持WRITE锁):
允许JPA实现用LockModeType.PESSIMISTIC_WRITE来代替LockModeType.PESSIMISTIC_READ,但是反之不可。

悲观写锁PESSIMISTIC_WRITE是基于“SELECT … FOR UPDATE”这种SQL语法来实现的:

select
        id 
    from
        product 
    where
        id =? 
        and version =? for update

悲观读锁PESSIMISTIC_READ很多数据库不支持。在MYSQL中,会生成“SELECT …. LOCK INSHARE MODE”。

PESSIMISTIC_FORCE_INCREMENT会使用悲观写锁,但不管有没有修改数据,都会更新Version字段的值。

  • 只能在查询语句上使用锁
    而且Spring Data JPA目前只支持在“查询语句”(SELECT语句)中使用@Lock注解。否则会抛异常:
// 尝试在非SELECT定义锁:
public interface ProductRepository extends JpaRepository<Product,Long>{
    
    @Modifying
    @Query("update Product p set p.stock = p.stock + 1 where p.id = ?1")
    @Lock(LockModeType.OPTIMISTIC)
    Integer addStockById(Long id);
}
// 调用会抛出异常:
IllegalStateException: Illegal attempt to set lock mode on a non-SELECT query

相关文章