kingschan1204 / blog

个人技术blog
https://github.com/kingschan1204/blog/issues
1 stars 1 forks source link

jpa锁机制 #27

Open kingschan1204 opened 5 years ago

kingschan1204 commented 5 years ago

首先需要清楚几个基本概念,锁定对于避免因两个并发用户同时更新相同数据而导致的更新冲突至关重要。

并发

系统能够同时并行处理很多请求;

同步和异步

同步是指服务一次指做一件事;异步是做某一件时,不需要一直等待结果,可以先去做其他事情,等到有结果了,在去继续其业务;在同步时针对某一数据,可以直接进行操作;

脏数据

脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是脏数据(Dirty Data),依据脏数据所做的操作可能是不正确的。

不可重复读

不可重复读是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读

使用锁的场景

多台同一服务(分布式环境)/多个事务中可能同时对某一个数据进行操作,增改删等操作时;

锁的介绍

一种是代码层次上的,如java中的同步锁,典型的就是同步关键字synchronized,这里我不在做过多的讲解。 另外一种是数据库层次上的,比较典型的就是悲观锁乐观锁。这里我们重点讲解的就是悲观锁(传统的物理锁)和乐观锁。

悲观锁

正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此, 在整个数据处理过程中,将数据处于锁定状态。 悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系 统不会修改数据)。 一个典型的依赖数据库的悲观锁调用:

 select * from account where name=”Erica” for update

这条 sql 语句锁定了 account 表中所有符合检索条件( name=”Erica” )的记录。 本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。

乐观锁

相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。 如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过程中(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果。 乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version )记录机制实现。

update xxx,version+1  where xxxx and version=x

何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。 读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

需要注意的是

乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)

JPA基础

JPA在其数据访问引擎中内置了乐观锁实现。如果不用考虑外部系统对数据库的更新操作,利用JPA提供的透明化乐观锁实现,将大大提升我们的 生产力。 JPA为了保证 Update 过程中对象不会被外界修改,会在save方法实现中自动为目标对象加上 WRITE 锁。

乐观锁实现

好了,说了那么久锁的基本概念以及相关知识,那么我们来看下如何通过JPA实现数据锁的操作。 在JPA中,@Version注解,可以实现乐观锁功能 实体类User,version属性上加@Version注解

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Version;
import java.math.BigDecimal;

@Entity
public class Student{
    @Id
    private int id;
    private String name;
    private BigDecimal money;
    @Version
    private int version;

    // getters and setters
}

锁模式

JPA提供我们两种不同的乐观锁模式(以及两个别名):

上述所有类型在LockModeType 中有完整定义.

OPTIMISTIC (READ)

我们知道OPTIMISTIC以及Read锁,两者是同义词。但JPA规范建议我们在新应用中使用OPTIMISTIC。每当我们请求OPTIMISTIC模式时,总是在事务提交时进行版本检查,这样确保乐观锁可重复读的实现。 简单地说,它确保应该在下面情况下任何事务不能提交:

已经更新或删除但未提交 已经同时成功地更新或删除

OPTIMISTIC_INCREMENT (WRITE)

与前面一样,OPTIMISTIC_INCREMENT和WRITE也是同义词,但是前者更可取。 OPTIMISTIC_INCREMENT必须满足与OPTIMISTIC模式相同的条件。然而,它并没有明确规定是否应该立即完成,或者是否应该推迟到提交或刷新。总是累加实体版本(即使此时实体并没有改变) 并在事务提交时进行版本检查,这样确保乐观锁可重复读的实现。

JPA的锁类型

示例:假设我们有两个实体“书(Book)”和“书架(Shelf)”。我们可以将书添加到书架中,但是书不持有对其书架的引用。我们需要对所有移动书到其他书架的动作加锁,以避免一本书被放在2个书架上。为了锁住这个动作,光锁住当前的书架实体是不够的,因为书可能还没有放到某个书架上。锁住所有书架也不合理,因为他们在不同的事务中可能都是不同的。唯一合理的是锁住书实体本身,即使在我们这个例子中它并没有发生变化(因为它并不持有其书架的引用)。

使用乐观锁

对拥有版本属性实体,缺省情况下乐观锁是有效的。但还有其他几种方式显示指定:

Find

通过给EntityManagerfind方法传递适当的LockModeType作为参数,可以获取乐观锁: entityManager.find(Student.class, studentId, LockModeType.OPTIMISTIC);

Query

另一种方法是通过Query对象的SetLockMode方法:

Query query = entityManager.createQuery("from Student where id = :id");
query.setParameter("id", studentId);
query.setLockMode(LockModeType.OPTIMISTIC_INCREMENT);
query.getResultList();

显示指定锁

通过调用EntityManager的lock方法显示指定锁:

Student student = entityManager.find(Student.class, id);
entityManager.lock(student, LockModeType.OPTIMISTIC);

Refresh

与之前一样也可以调用refresh方法实现:

Student student = entityManager.find(Student.class, id);
entityManager.refresh(student, LockModeType.READ);

NamedQuery

最后一个使用@NamedQuery注解的lockMode属性:

@NamedQuery(name="optimisticLock",query="SELECT s FROM Student s WHERE s.id LIKE :id",lockMode = WRITE)

version 属性

在version中optimistic-lock 属性有如下可选取值: