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 )为 100 。 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除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以及锁的相关概念以及描述完成,如果有描述不清的地方,请指正!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • [2017第一篇] 2017年1月1日,今天是以“整理”的主题来度过的。 今天始终保持着觉察的状态,没有太起伏的情...
    娟娟Cyndi阅读 354评论 2 3
  • #跟易仁永澄老师学习制定年度目标#特别烧脑的一天,如何清晰认识我是谁?时光轴,对对碰,年目标,厘清重要优先级。告诉...
    trista_chow阅读 238评论 2 0
  • 昨日中午,我正低头吃饭,听到连声叫着“玉札”。回头看,是5号餐桌的查补师傅,我端着饭碗走到他们餐桌前,几个人,你一...
    金获阅读 648评论 0 2
  • 自从开始写日志以来,写妈妈的文字基本没有,反而写爸爸的文字有一些。可能是男孩子原因,更容易理解爸爸,心疼爸爸,也可...
    anghoo阅读 789评论 3 7
  • 他听了我的前几句话后 好像来了情绪 指着我的头对我说 “你有种再说一句” 于是我对着他说完了整段 比伯科斯
    土星小宝阅读 169评论 0 0