JPA是Spring框架下优秀的持久化框架,以仓库作为核心概念玩转各大关系型数据库。
下面要介绍的便是数据库一般都有的事务和锁。
事务
事务是恢复和并发控制的基本单位。一般用作一组不能分割的逻辑单元操作。
- 原子性(atomicity):一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。
- 一致性(consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
- 隔离性(isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
- 持久性(durability):持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
那么,JPA如何声明事务呢?答案是@Transactional。下面是这个注解的详情:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
readOnly置为true时,只进行查询操作,能够提高查询效率。也能用timeout设置超时时间。
默认情况下,仓库实例上的CUD方法是事务的。可以使用@Transactional在多个仓库的调用的方法上开启事务:
@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);
}
}
锁
锁是和事务相关的。锁必须在事务里使用。不存在离开事务的锁。
在某些情况下,比如要更新数据库的总人数,而这些更新是在不同地点发生的,会有并发的产生,导致最终的人数少于实际更新的人数。这时,就需要在事务的基础上加锁了。把数据行锁住,这样每次就只有一个更新能够成功。
锁的类型在javax.persistence.LockModeType枚举中列出:
READ, //等效OPTIMISTIC
WRITE, //等效OPTIMISTIC_FORCE_INCREMENT
OPTIMISTIC, //乐观锁
OPTIMISTIC_FORCE_INCREMENT, //乐观锁,加版本
PESSIMISTIC_READ, //悲观读锁
PESSIMISTIC_WRITE, //悲观写锁
PESSIMISTIC_FORCE_INCREMENT, //悲观写锁,带版本
NONE //没有锁
总的来说,JPA的锁分为两种:
- 悲观锁
悲观锁会假设资源的竞争激烈,所以在更新前先拿到锁,只能更新完释放锁之后,其他请求才能继续。 - 乐观锁
乐观锁假设资源的竞争不是那么激烈,会尝试去更新数据,如果数据版本不一致,则报错,调用者可以尝试继续更新或放弃。使用乐观锁时,要在数据实体上加带@Version注解的字段来标记数据版本。
只要在方法上加@Lock,即可给方法加锁。如前面所说,加锁@Lock的方法要在加事务@Transactional的方法里面调用,这样,加的锁才能被正确地释放。
下面演示乐观锁的使用:
定义一个实体:
@ApiModel
@Setter
@Getter
@ToString
@Entity
public class QuiltHello {
@Id
@GeneratedValue
private Long id;
@Column
private String name;
@Column
private Integer number;
@Version //@Version注解的字段
@Column
private Long version;
}
给查询方法加锁:
@Repository
public interface QuiltHelloJpa extends JpaRepositoryImplementation<QuiltHello, Long> {
boolean existsByName(String name);
@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
QuiltHello findByName(String name);
}
在@Transactional里面调用加锁方法:
@Component
public class HelloLock {
private final QuiltHelloJpa quiltHelloJpa;
public HelloLock(QuiltHelloJpa quiltHelloJpa) {
this.quiltHelloJpa = quiltHelloJpa;
}
@Transactional
public void reset() {
System.err.println("reset");
QuiltHello zlb = quiltHelloJpa.findByName("zlb");
if (zlb != null) {
zlb.setNumber(0);
zlb = quiltHelloJpa.save(zlb);
System.err.println(zlb);
}
}
@Async //标记为异步,模拟不同时空
@Transactional
public ListenableFuture<QuiltHello> add1() {
System.err.println("current thread: " + Thread.currentThread().getName());
QuiltHello zlb = quiltHelloJpa.findByName("zlb"); //调用加锁方法
if (zlb != null) {
zlb.setNumber(zlb.getNumber() + 1);
zlb = quiltHelloJpa.save(zlb);
System.err.println(zlb);
}
return new AsyncResult<>(zlb);
}
}
程序启动时,尝试100次异步自增:
helloLock.reset();
for (int i = 0; i < 100; i++) {
System.err.println("add times " + i);
ListenableFuture<QuiltHello> res = helloLock.add1();
res.addCallback(new ListenableFutureCallback<QuiltHello>() {
@Override
public void onFailure(Throwable ex) {
System.err.println("fail"); //出错重试或放弃
}
@Override
public void onSuccess(QuiltHello result) {
System.err.println("success");
}
});
}
可以看到,最终的结果是几十,小于100的数字。但是哪些报错了是知道的,由用户决定是重试还是放弃。可以用rxjava或reactor一直重试到成功。
事务和锁的关系
数据库事务有不同的隔离级别,不同的隔离级别对锁的使用是不同的,锁的应用最终导致不同事务的隔离级别。
- 事务与锁是不同的。事务具有ACID(原子性、一致性、隔离性和持久性),锁是用于解决隔离性的一种机制。
- 事务的隔离级别通过锁的机制来实现。另外锁有不同的粒度,同时事务也是有不同的隔离级别的(一般有四种:读未提交Read uncommitted, 读已提交Read committed, 可重复读Repeatable read, 可串行化Serializable)。
- 开启事务就自动加锁。
隔离级别
隔离级别定义了一个事务和其他事务的关系。
隔离级别 | 事务A | 其他事务 |
---|---|---|
读未提交Read uncommitted | 可以读到其他事务未提交的内容 | 可读,可写 |
读已提交Read committed | 只能读到其他事务已提交的内容 | 可读,可写 |
可重复读Repeatable read | 每次读到的内容是相同的 | 可读,不可写 |
可串行化Serializable | 本事务进行时其他事务无法开启 | 不可读,不可写 |
传播特性
传播特性定义了事务和嵌套事务的关系。比如方法A调用方法B,且方法A和方法B都定义了事务,两个事务的传播特性决定了这两个事务如何进行。