jpa锁
Posted vana
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jpa锁相关的知识,希望对你有一定的参考价值。
基础
首先需要清楚几个基本概念,锁定对于避免因两个并发用户同时更新相同数据而导致的更新冲突至关重要。
- 并发:系统能够同时并行处理很多请求;
- 同步和异步:同步是指服务一次指做一件事;异步是做某一件时,不需要一直等待结果,可以先去做其他事情,等到有结果了,在去继续其业务;在同步时针对某一数据,可以直接进行操作;
- 脏数据:脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是脏数据(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” 字段来实现。 读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
修改账本的例子
- 假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 50。
- 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 。
- 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
- 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。 这样,就避免了操作员 B 用基于version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。 从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销(操作员 A和操作员 B 操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系 统整体性能表现。
需要注意的是: 乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户余额更新操作不受我们系统的控制,因此可能
会造成脏数据被更新到数据库中。在系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。
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提供我们两种不同的乐观锁模式(以及两个别名):
- OPTIMISTIC / READ – 所有包含版本属性的实体获得乐观读锁
- OPTIMISTIC_FORCE_INCREMENT / WRITE – 与OPTIMISTIC一样获得锁并自动增加版本属性的值
上述所有类型在LockModeType 中有完整定义.
OPTIMISTIC (READ)
我们知道OPTIMISTIC以及Read锁,两者是同义词。但JPA规范建议我们在新应用中使用OPTIMISTIC。每当我们请求OPTIMISTIC模式时,总是在事务提交时进行版本检查,这样确保乐观锁可重复读的实现。
简单地说,它确保应该在下面情况下任何事务不能提交:
- 已经更新或删除但未提交
- 已经同时成功地更新或删除
OPTIMISTIC_INCREMENT (WRITE)
与前面一样,OPTIMISTIC_INCREMENT和WRITE也是同义词,但是前者更可取。 OPTIMISTIC_INCREMENT必须满足与OPTIMISTIC模式相同的条件。然而,它并没有明确规定是否应该立即完成,或者是否应该推迟到提交或刷新。总是累加实体版本(即使此时实体并没有改变) 并在事务提交时进行版本检查,这样确保乐观锁可重复读的实现。
JPA的锁类型
-
LockModeType.OPTIMISTIC_FORCE_INCREMENT:乐观锁定,默认的锁类型。
-
LockModeType.OPTIMISTIC_FORCE_INCREMENT:用另一个实体来锁住对当前实体的引用,就需要使用它。提交事务时,即使实体的状态在事务中没有更改,实体的版本列也会递增。
示例:假设我们有两个实体“书(Book)”和“书架(Shelf)”。我们可以将书添加到书架中,但是书不持有对其书架的引用。我们需要对所有移动书到其他书架的动作加锁,以避免一本书被放在2个书架上。为了锁住这个动作,光锁住当前的书架实体是不够的,因为书可能还没有放到某个书架上。锁住所有书架也不合理,因为他们在不同的事务中可能都是不同的。唯一合理的是锁住书实体本身,即使在我们这个例子中它并没有发生变化(因为它并不持有其书架的引用)。
-
LockModeType.PESSIMISTIC_READ: 悲观锁定。其他事务可以同时读取实体,但不能同时更新它。
-
LockModeType.PESSIMISTIC_WRITE:悲观锁定。是LockModeType.PESSIMISTIC_READ的增强版。其他事务不能同时读取或写入实体。
注意:OpenJPA 仅针对DB2数据库区分PESSIMISTIC_READ和 PESSIMISTIC_WRITE锁定模式。在与其他数据库一起运行时,这两种模式之间没有区别,因为 PESSIMISTIC_READ锁定模式已升级到PESSIMISTIC_WRITE。 -
LockModeType.PESSIMISTIC_FORCE_INCREMENT:悲观锁定。其他事务不能同时读取或写入实体。提交事务时,即使实体的状态在事务中没有更改,实体的版本列也会递增。是悲观锁和乐观锁的结合。
使用乐观锁
对拥有版本属性实体,缺省情况下乐观锁是有效的。但还有其他几种方式显示指定:
Find
通过给EntityManager的find方法传递适当的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 属性有如下可选取值:
- none:无乐观锁
- version:通过版本机制实现乐观锁
- dirty:通过检查发生变动过的属性实现乐观锁
- all:通过检查所有属性实现乐观锁
至此,JPA以及锁的相关概念以及描述完成,如果有描述不清的地方,请指正!
作者:机智小当家
链接:https://www.jianshu.com/p/dc954e09360d
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
以上是关于jpa锁的主要内容,如果未能解决你的问题,请参考以下文章
Spring27JPA 实现乐观锁@Version注解的使用
JUC并发编程 共享模式之工具 JUC CountdownLatch(倒计时锁) -- CountdownLatch应用(等待多个线程准备完毕( 可以覆盖上次的打印内)等待多个远程调用结束)(代码片段