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

2019-06-02 19:13:59

阅读次数 - 4011

Java JPA

事务

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

限于篇幅原因,本文不会详细介绍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(); }

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

对查询语句设置readOnlytrue有什么好处呢?如果你设置了readOnlytrue,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是无锁,所以不作过多解释。主要介绍这五个锁:

  • OPTIMISTIC
  • OPTIMISTIC_FORCE_INCREMENT
  • PESSIMISTIC_READ
  • PESSIMISTIC_WRITE
  • 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

为什么不支持在更新/删除语句中使用Lock呢?参考:Why according to JPA are Update/Delete operations are not allowed to set lock?。悲观锁底层交给了数据库层面,而乐观锁基于Version,所以也不需要手动添加@Lock注解,你可以手动在update语句中实现乐观锁:

@Query("update Product p set p.stock = p.stock + 1, p.version = p.version + 1" + "where p.id = ?1 and p.version = ?2")

参考文章

Locking in JPA (LockModeType)

感谢阅读


觉得文章还不错?点击下方按钮分享吧!

评论

快来占个沙发吧~


TO: