@Version 处理乐观锁的问题
@Version 乐观锁介绍
我们在研究 Auditing 的时候,发现了一个有趣的注解 @Version,源码如下:
package org.springframework.data.annotation;
/**
* Demarcates a property to be used as version field to implement optimistic locking on entities.
*/
@Retention(RUNTIME)
@Target(value = { FIELD, METHOD, ANNOTATION_TYPE })
public @interface Version {}
发现它帮我们处理了乐观锁的问题,什么是乐观锁,还有线程的安全性,在另外一本书《Java 并发编程从入门到精通》里面,或者看作者的另外一篇 Chat:Java 多线程与并发编程 · Java 工程师必知必会,作者做了深入的探讨。
对于数据来说,简单理解:在数据库并发操作时,为了保证数据的正确性,我们会做一些并发处理,主要就是加锁。在加锁的选择上,常见有两种方式:悲观锁和乐观锁。
- 悲观锁:简单的理解就是把需要的数据全部加锁,在事务提交之前,这些数据全部不可读取和修改。
- 乐观锁:使用对单条数据进行版本校验和比较,来对保证本次的更新是最新的,否则就失败,效率要高很多。在实际工作中,乐观锁不止在数据库层面,其实我们在做分布式系统的时候,为了实现分布式系统的数据一致性,分布式事物的一种做法就是乐观锁。
数据库操作举例说明
悲观锁的做法:
select * from user where id=1 for update;
update user set name='jack' where id=1;
通过使用 for update 给这条语句加锁,如果事务没有提交,其他任何读取和修改,都得排队等待。在代码中,我们加事务的 Java 方法就会自然的形成了一个锁。
乐观锁的做法:
select uid,name,version from user where id=1;
update user set name='jack', version=version+1 where id=1 and version=1
假设本次查询 version=1,在更新操作时,带上这次查出来的 Version,这样只有和我们上次版本一样的时候才会更新,就不会出现互相覆盖的问题,保证了数据的原子性。
@Version 用法
在没有 @Version 之前,我们都是自己手动维护这个 Version 的,这样很有可能做什么操作的时候给忘掉。或者是我们自己底层做框架,用 AOP 的思路做拦截底层维护这个 Version 的值。而 Spring Data JPA 的 @Version 就是通过 AOP 机制,帮我们动态维护这个 Version,从而更优雅的实现乐观锁。
(1)实体上的 Version 字段加上 @Version 注解即可。
我们对上面的实体 UserCustomerEntity 改进如下:
@Entity
@Table(name = "user_customer", schema = "test", catalog = "")
public class UserCustomerEntity extends AbstractAuditable {
//新增控制乐观锁的字段。并且加上@Version注解
@Version
@Column(name = "version", nullable = true)
private Long version;
......
}
(2)实际调用
userCustomerRepository.save(new UserCustomerEntity("1","Jack"));
UserCustomerEntity uc= userCustomerRepository.findOne(1);
uc.setCustomerName("Jack.Zhang");
userCustomerRepository.save(uc);
我们会发现 Insert 和 Update 的 SQL 语句都会带上 Version 的操作。当乐观锁更新失败的时候,会抛出异常 org.springframework.orm.ObjectOptimisticLockingFailureException。
实现原理关键代码
(1)SimpleJpaRepository.class 里面的 save 方法如下:
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
(2)如果我们在此处设置一个 debug 断点的话,我们一步一步往下面走会发现进入 JpaMetamodelEntityInformation.class 的关键代码如下:
@Override
public boolean isNew(T entity) {
if (!versionAttribute.isPresent()
|| versionAttribute.map(Attribute::getJavaType).map(Class::isPrimitive).orElse(false)) {
return super.isNew(entity);
}
BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity);
return versionAttribute.map(it -> wrapper.getPropertyValue(it.getName()) == null).orElse(true);
}
所以到这里,可以看出当我们更新的时候,若实体对象上面有 @Version 注解,那么就一定要带上 version,如果没带上 version 字段的值,只有 ID 字段的值,系统也会认为是新增。相反,如果我们没有 @Version 注解的字段,那么就会以 @ID 字段来判断是否是新增。其实这里我们也明白,省去了传统都需要我们自己去实现的 saveOrUpdate 方法。
(3)其实我们多看看代码,多 debug 几次就会发现,也可以在 @Entity 的类里面覆盖掉 isNew() 方法,这样可以实现自己的 isNew 的判断逻辑。
@Entity
@Table(name = "user")
public class UserEntity implements Persistable {
@Transient //这个注解表明这个字段不是持久化的
@JsonIgnore //json显示的时候我们也可忽略这个字段
@Override
public boolean isNew() {
return getId() == null;
}
....
}