跳到主要内容

悲观锁

准备工作

本文所讨论的悲观锁,并非普通查询的悲观锁,而是保存指令内部的悲观锁。

面对一个保存操作,在不同的情况下,保存指令会采取两种截然不同的行为:

  1. 直接利用数据库本身的能力进行upsert操作

  2. 先通过select操作检查被保存的数据是否存在,再根据查询结果决定后续操作应该是insert还是update

Jimmer会尽可能执行1,如果无法做到,则执行2并向用户报告QueryReason

当Jimmer不得不执行2时,存在一个问题:先执行的select操作和后执行insert或update操作之间存在一个时间窗口,其他事务可能挤进这个时间窗口对相同的数据进行修改。

为了解决这个问题,保存指令允许用户启用悲观锁,在执行select操作时加上for update选项,直到事务提交为止。

在展示悲观锁之前,我们有必要让保存指令执行上面的2,而非1。一个有效的办法是启用事务内触发器:

事务内触发器是Jimmer所支持的触发器的一种,Jimmer总是通过select查询修改前的旧数据,并以此模拟出触发器的效果。

备注

事务内触发器是当前尚未介绍的功能,读者不用太在意它,这里,我们只是用它来确保Jimmer会采用先查再改的策略而已。

采用以下任何一种方法开启事务内触发器

  • 利用Jimmer的Spring Boot Starter

    修改application.ymlapplication.properties文件,完成配置

    application.yml
    jimmer:
    trigger-type: TRANSACTION_ONLY
  • 使用Jimmer的核心API

    JSqlClient sqlClient = JSqlClient
    .newBuilder()
    .setTriggerType(TriggerType.TRANSACTION_ONLY)
    ...省略其他配置...
    .build();

通过这样的配置,强迫Jimmer总是先查询再修改,以便于演示悲观锁。

悲观锁展示

我们可以通过setPessimisticLock方法为保存指令开启悲观锁

List<Book> books = Arrays.asList(
Immutables.createBook(draft -> {
draft.setName("GraphQL in Action");
draft.setEdition(3);
draft.setPrice(new BigDecimal("59.9"));
}),
Immutables.createBook(draft -> {
draft.setName("GraphQL in Action");
draft.setEdition(4);
draft.setPrice(new BigDecimal("59.9"));
})
);
sqlClient
.saveEntitiesCommand(books)
.setPessimisticLock(Book.class)
.execute();

这里,setPessimisticLockBook实体开启了悲观锁,如果保存指令需要查询Book类型的对象,相关select语句会启用for update选项。

信息

事实上,你也可以调用setPessimisticLockAll方法盲目地为所有实体类型开启悲观锁

最终生成如下三条SQL

  1. 查询被保存的对象是否存在,带上for update选项

    Purpose: COMMAND(TRIGGER)
    SQL: select
    tb_1_.ID,
    tb_1_.NAME,
    tb_1_.EDITION,
    tb_1_.PRICE,
    tb_1_.STORE_ID
    from BOOK tb_1_
    where
    (tb_1_.NAME, tb_1_.EDITION) in (
    (? /* GraphQL in Action */, ? /* 3 */),
    (? /* GraphQL in Action */, ? /* 4 */)
    ) for update
    信息

    这里,查询语句带有for update选项,悲观锁已经生效。

  2. 插入不存在的数据 (非本文重点,默认折叠)
    insert into BOOK(
    NAME, EDITION, PRICE
    ) values(
    ? /* GraphQL in Action */,
    ? /* 4 */,
    ? /* 59.9 */
    )
  3. 更新已经存在的数据 (非本文重点,默认折叠)
    update BOOK
    set
    PRICE = ? /* 59.9 */
    where
    ID = ? /* 12 */