跳到主要内容

乐观锁和悲观锁

保存指令支持乐观锁和悲观锁。

乐观锁

Jimmer使用注解@org.babyfish.jimmer.sql.Version支持乐观锁。

修改实体类型

  • 修改BookStore

    BookStore.java
    @Entity
    public interface BookStore {

    @Version
    int version();

    ...省略其他属性...
    }
  • 修改Book

    Book.java
    @Entity
    public interface Book {

    @Version
    int version();

    ...省略其他属性...
    }

示范

乐观锁的特性

  • 当插入对象时 (无论明确地进行INSERT操作,还是UPSERT操作被判定为INSERT),将对象的version插入到数据库。

    例子如下

    BookStore savedData = sqlClient.save(
    Immutables.createBookStore(draft -> {
    draft.setName("TURING");
    draft.addIntoBooks(book -> {
    book.setName("Introduction to Algorithms");
    book.setEdition(3);
    book.setPrice(new BigDecimal("44.99"));
    });
    draft.addIntoBooks(book -> {
    book.setName("The Pragmatic Programmer");
    book.setEdition(2);
    book.setPrice(new BigDecimal("39.99"));
    });
    })
    ).getModifiedEntity();
    System.out.println(savedData);
    提示

    对插入操作而言,如果对象的version并未被赋值,Jimmer自动插入0

    如果你无法断言某个UPSERT模式的保存指令最终会被判定为INSERT还是UPDATE,则应该坚持指定version属性。

    下面的例子,基于一个假设,明确知道save操作一定会被判定为INSERT,而非UPDATE,所以未指定对象的version属性。

    所有对象都没有指定id属性,Jimmer根据每个对象的key去判断它们是否存在。

    假设所有对象都不存在,因此,三条新数据会被插入。

    所有对象都没有指定version属性,所以,它们会被自动填充为0。

    最终打印结果为 (为了便于阅读,这里进行了格式化)

    {
    "id":100,
    "name":"TURING",
    "version":0,
    "books":[
    {
    "id":100,
    "name":"Introduction to Algorithms",
    "edition":3,
    "price":44.99,
    "version":0,
    "store":{
    "id":100
    }
    },
    {
    "id":101,
    "name":"The Pragmatic Programmer",
    "edition":2,
    "price":39.99,
    "version":0,
    "store":{
    "id":100
    }
    }
    ]
    }
    信息

    当然,如果用户为这些对象指定了version属性,这时,会插入用户指定的值,而非0。

  • 当修改对象时 (无论明确地进行UPDATE操作,还是UPSERT操作被判定为UPDATE),Jimmer会为每个对象比较用户传递的version和数据库中现有的version,如果二者不一致,抛出异常。

    我们稍微修改一下代码,再次执行

    BookStore savedData = sqlClient.save(
    Immutables.createBookStore(draft -> {
    draft.setName("TURING");
    draft.setVersion(0);
    draft.addIntoBooks(book -> {
    book.setName("Introduction to Algorithms");
    book.setEdition(3);
    book.setPrice(new BigDecimal("54.99"));
    book.setVersion(0);
    });
    draft.addIntoBooks(book -> {
    book.setName("The Pragmatic Programmer");
    book.setEdition(2);
    book.setPrice(new BigDecimal("39.99"));

    // 非法version
    book.setVersion(9999);
    });
    })
    ).getModifiedEntity();
    System.out.println(savedData);
    警告

    对修改操作而言,如果对象的version并未被赋值,Jimmer会抛出异常。

    如果你无法判断某个UPSERT模式的保存指令最终会被判定为INSERT还是UPDATE,则应该坚持指定version属性。

    执行,由于数据库中已经存在数据,所以三个对象都会被UPDATE。

    很明显,最后一本书籍的version9999是非法的,上面的的代码将会得到如下异常:

    • 异常类型:org.babyfish.jimmer.sql.runtime.SaveException

    • 异常编码:org.babyfish.jimmer.sql.runtime.SaveErrorCode.ILLEGAL_VERSION

    • 异常消息:

      Save error caused by the path: "<root>.books": Cannot update the entity whose type is "org.doc.j.model.Book", id is "101" and version is "9999"

    让我们再修改一下代码,让所有对象都持有正确的version,如下

    BookStore savedData = sqlClient.save(
    Immutables.createBookStore(draft -> {
    draft.setName("TURING");
    draft.setVersion(0);
    draft.addIntoBooks(book -> {
    book.setName("Introduction to Algorithms");
    book.setEdition(3);
    book.setPrice(new BigDecimal("54.99"));
    book.setVersion(0);
    });
    draft.addIntoBooks(book -> {
    book.setName("The Pragmatic Programmer");
    book.setEdition(2);
    book.setPrice(new BigDecimal("39.99"));
    book.setVersion(0);
    });
    })
    ).getModifiedEntity();
    System.out.println(savedData);

    最终打印结果为 (为了便于阅读,这里进行了格式化)

    {
    "id":100,
    "name":"TURING",
    "version":1,
    "books":[
    {
    "id":100,
    "name":"Introduction to Algorithms",
    "edition":3,
    "price":54.99,
    "version":1,
    "store":{
    "id":100
    }
    },
    {
    "id":101,
    "name":"The Pragmatic Programmer",
    "edition":2,
    "price":39.99,
    "version":1,
    "store":{
    "id":100
    }
    }
    ]
    }
    信息

    可见数据被修改后,乐观锁会自动加1。

    实际项目中,乐观锁版本号往往是表单界面的隐藏字段。如果某个表单保存后不自动跳转,而是保持界面不变以支持多次提交,则应该在每次保存成功后利用这样的返回信息更新表单界面的隐藏字段。

悲观锁

和乐观锁不同,悲观锁生命周期很短,仅在一个jdbc事务内有效。

通常,Jimmer会生成一些查询SQL以辅助保存指令的执行,比如

  • 判断UPSERT操作最终应该判定为INSERT还是UPDATE

  • 判断哪些关联对象需要被脱钩

接下来,我们对比不使用悲观锁和使用悲观锁两种情况,来观察这些查询SQL有何不同。

信息

在前面的例子中,为了介绍乐观锁,假设BookStoreBook类型都定义了version属性。

后续例子为了介绍悲观锁,不再有此假设。

未启用悲观锁

sqlClient.save(
Immutables.createBookStore(draft -> {
draft.setName("TURING");
draft.addIntoBooks(book -> {
book.setName("Introduction to Algorithms");
book.setEdition(3);
book.setPrice(new BigDecimal("44.99"));
});
draft.addIntoBooks(book -> {
book.setName("The Pragmatic Programmer");
book.setEdition(2);
book.setPrice(new BigDecimal("39.99"));
});
})
);

生成6条SQL,如下

  1. 判断书店是否存在

    select
    tb_1_.ID,
    tb_1_.NAME
    from BOOK_STORE tb_1_
    where
    tb_1_.NAME = ? /* TURING */
  2. 根据前一条查询的结果,决定INSERT还是UPDATE

    insert或update,略

  3. 判断第1本书籍是否存在

    select
    tb_1_.ID,
    tb_1_.NAME,
    tb_1_.EDITION
    from BOOK tb_1_
    where
    tb_1_.NAME = ? /* Introduction to Algorithms */
    and
    tb_1_.EDITION = ? /* 3 */
  4. 根据前一条查询的结果,决定INSERT还是UPDATE

    insert或update,略

  5. 判断第2本书籍是否存在

    select
    tb_1_.ID,
    tb_1_.NAME,
    tb_1_.EDITION
    from BOOK tb_1_
    where
    tb_1_.NAME = ? /* The Pragmatic Programmer */
    and
    tb_1_.EDITION = ? /* 2 */
  6. 根据前一条查询的结果,决定INSERT还是UPDATE

    insert或update,略

信息

这些查询语句用于进行条件判断,以决定后续SQL该如何生成。

然而,这些查询没有使用悲观锁,经它们判断而成立的条件和假设,有可能被其它并发行为破坏,从而导致后续SQL执行出现异常。

为了避免这种并发问题,可以启用悲观锁。接下来,我们讨论悲观锁如何实现。

启用悲观锁

有两种启用悲观锁的方法

  • 全局配置

    有两种方法可以通过全局配置启用悲观锁

    • Spring Boot Starter的配置

      修改application.yml (或application.properties)

      jimmer:
      default-lock-mode: PESSIMISTIC
    • 底层API的配置

      JSqlClient sqlClient = JSqlClient
      .newBuilder()
      .setDefaultLockMode(LockMode.PESSIMISTIC)
      ...省略其他配置...
      .build();
    注意

    此举修改了全局设置,原本的默认值OPTIMISTIC被破坏。这意味着,除非将某个保存指令设置为乐观锁模式,前文所讲述的乐观锁的功能消失。

    因此,大部分情况下,不推荐全局配置,而更推荐下文即将介绍的指令级配置。

  • 指令级配置

    和影响所有保存指令的全局配置不同,指令级配置仅仅影响当前保存指令

    信息

    如果已经通过全局配置打开了悲观锁控制,就不再需要指令级配置了

    调用保存指令的配置方法setLockMode,即可启用悲观锁,如下

    sqlClient
    .getEntities()
    .saveCommand(
    Immutables.createBookStore(draft -> {
    draft.setName("TURING");
    draft.addIntoBooks(book -> {
    book.setName("Introduction to Algorithms");
    book.setEdition(3);
    book.setPrice(new BigDecimal("44.99"));
    });
    draft.addIntoBooks(book -> {
    book.setName("The Pragmatic Programmer");
    book.setEdition(2);
    book.setPrice(new BigDecimal("39.99"));
    });
    })
    )
    .setLockMode(LockMode.PESSIMISTIC)
    .execute();

一旦启用了悲观锁,生成的查询语句会有显著变化,如下

  1. 判断书店是否存在

    select
    tb_1_.ID,
    tb_1_.NAME
    from BOOK_STORE tb_1_
    where
    tb_1_.NAME = ? /* TURING */
    /* highlight-next-line */
    for update
  2. 根据前一条查询的结果,决定INSERT还是UPDATE

    insert或update,略

  3. 判断第1本书籍是否存在

    select
    tb_1_.ID,
    tb_1_.NAME,
    tb_1_.EDITION
    from BOOK tb_1_
    where
    tb_1_.NAME = ? /* Introduction to Algorithms */
    and
    tb_1_.EDITION = ? /* 3 */
    /* highlight-next-line */
    for update
  4. 根据前一条查询的结果,决定INSERT还是UPDATE

    insert或update,略

  5. 判断第2本书籍是否存在

    select
    tb_1_.ID,
    tb_1_.NAME,
    tb_1_.EDITION
    from BOOK tb_1_
    where
    tb_1_.NAME = ? /* The Pragmatic Programmer */
    and
    tb_1_.EDITION = ? /* 2 */
    /* highlight-next-line */
    for update
  6. 根据前一条查询的结果,决定INSERT还是UPDATE

    insert或update,略