spring data-jpa的事务和锁

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的锁分为两种:

  1. 悲观锁
    悲观锁会假设资源的竞争激烈,所以在更新前先拿到锁,只能更新完释放锁之后,其他请求才能继续。
  2. 乐观锁
    乐观锁假设资源的竞争不是那么激烈,会尝试去更新数据,如果数据版本不一致,则报错,调用者可以尝试继续更新或放弃。使用乐观锁时,要在数据实体上加带@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一直重试到成功。

事务和锁的关系

数据库事务有不同的隔离级别,不同的隔离级别对锁的使用是不同的,锁的应用最终导致不同事务的隔离级别。

  1. 事务与锁是不同的。事务具有ACID(原子性、一致性、隔离性和持久性),锁是用于解决隔离性的一种机制。
  2. 事务的隔离级别通过锁的机制来实现。另外锁有不同的粒度,同时事务也是有不同的隔离级别的(一般有四种:读未提交Read uncommitted, 读已提交Read committed, 可重复读Repeatable read, 可串行化Serializable)。
  3. 开启事务就自动加锁。

隔离级别

隔离级别定义了一个事务和其他事务的关系。

隔离级别事务A其他事务
读未提交Read uncommitted可以读到其他事务未提交的内容可读,可写
读已提交Read committed只能读到其他事务已提交的内容可读,可写
可重复读Repeatable read每次读到的内容是相同的可读,不可写
可串行化Serializable本事务进行时其他事务无法开启不可读,不可写

传播特性

传播特性定义了事务和嵌套事务的关系。比如方法A调用方法B,且方法A和方法B都定义了事务,两个事务的传播特性决定了这两个事务如何进行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值