默认情况下,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(); }
如果同时在接口和方法上进行了配置,方法上的配置会具有更高的优先级。
对查询语句设置 readOnly
为 true
有什么好处呢?如果你设置了 readOnly
为 true
,hibernate的flush模式会自动设置为 NEVER
,这样就可以让hibernate跳过“脏检查”阶段,可以显著提升大对象(很多层子对象组成的对象树)的查询性能。
如果涉及到多个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
开头的代表“乐观锁”,以 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的乐观锁,而自己新增一个字段,在执行更新操作的时候手动去实现。
那 OPTIMISTIC 与 OPTIMISTIC_FORCE_INCREMENT 有什么区别呢?
悲观锁是在 数据库层面实现的 ,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")
来源:https://www.yasinshaw.com/articles/39